blob: 42f234f07930302763f0728f5e9e2765209ef2dc [file] [log] [blame]
# Copyright 2008 the Melange authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Generic cleaning methods.
"""
import re
from HTMLParser import HTMLParseError
from html5lib import HTMLParser
from html5lib import sanitizer
from html5lib.html5parser import ParseError
from google.appengine.api import users
from django import forms
from django.core import validators
from django.utils.translation import ugettext
from soc.models.user import User
from soc.logic import validate
from soc.logic import user
DEF_VALID_SHIPPING_CHARS = re.compile('^[A-Za-z0-9\s-]+$')
DEF_LINK_ID_IN_USE = ugettext(
'This username is already in use, please specify another one')
DEF_NO_RIGHTS_FOR_ACL = ugettext(
'You do not have the required rights for that ACL.')
DEF_ORGANZIATION_NOT_ACTIVE = ugettext(
"This organization is not active or doesn't exist.")
DEF_NO_SUCH_DOCUMENT = ugettext(
"There is no such document with that username under this entity.")
DEF_MUST_BE_ABOVE_AGE_LIMIT = ugettext(
"To sign up as a student for this program, you "
"must be at least %d years of age, as of %s.")
DEF_MUST_BE_ABOVE_LIMIT = ugettext(
"Must be at least %d characters, it has %d characters.")
DEF_MUST_BE_UNDER_LIMIT = ugettext(
"Must be under %d characters, it has %d characters.")
DEF_2_LETTER_STATE = ugettext(
"State should be 2-letter field since country is '%s'.")
DEF_INVALID_SHIPPING_CHARS = ugettext(
'Invalid characters, only A-z, 0-9, - and whitespace are allowed. '
'See also <a href="http://code.google.com/p/soc/issues/detail?id=903">'
'Issue 903</a>, in particular <a href="'
'http://code.google.com/p/soc/issues/detail?id=903#c16">comment 16</a>. '
'Please <em>do not</em> create a new issue about this.')
DEF_ROLE_TARGET_COUNTRY = "United States"
DEF_ROLE_COUNTRY_PAIRS = [("res_country", "res_state"),
("ship_country", "ship_state")]
def check_field_is_empty(field_name):
"""Returns decorator that bypasses cleaning for empty fields.
"""
def decorator(fun):
"""Decorator that checks if a field is empty if so doesn't do the cleaning.
Note Django will capture errors concerning required fields that are empty.
"""
from functools import wraps
@wraps(fun)
def wrapper(self):
"""Decorator wrapper method.
"""
field_content = self.cleaned_data.get(field_name)
if not field_content:
# field has no content so bail out
return u''
else:
# field has contents
return fun(self)
return wrapper
return decorator
def clean_empty_field(field_name):
"""Incorporates the check_field_is_empty as regular cleaner.
"""
@check_field_is_empty(field_name)
def wrapper(self):
"""Decorator wrapper method.
"""
return self.cleaned_data.get(field_name)
return wrapper
def clean_email(field_name):
"""Checks if the field_name value is in an email format.
"""
@check_field_is_empty(field_name)
def wrapper(self):
"""Decorator wrapper method.
"""
# convert to lowercase for user comfort
email = self.cleaned_data.get(field_name)
validator = validators.validate_email
try:
validator(email)
except forms.ValidationError, e:
if e.code == 'invalid':
msg = ugettext(u'The email address %s is not valid.' % email)
raise forms.ValidationError(msg, code='invalid')
return email
return wrapper
def clean_link_id(field_name):
"""Checks if the field_name value is in a valid link ID format.
"""
@check_field_is_empty(field_name)
def wrapper(self):
"""Decorator wrapper method.
"""
# convert to lowercase for user comfort
link_id = self.cleaned_data.get(field_name).lower()
if not validate.isLinkIdFormatValid(link_id):
raise forms.ValidationError(
"The username %s is in wrong format." % link_id,
code='invalid')
return link_id
return wrapper
def clean_existing_user(field_name):
"""Check if the field_name field is a valid user.
"""
@check_field_is_empty(field_name)
def wrapped(self):
"""Decorator wrapper method.
"""
link_id = clean_link_id(field_name)(self)
user_entity = User.get_by_key_name(link_id)
if not user_entity:
# user does not exist
raise forms.ValidationError("This user does not exist.")
return user_entity
return wrapped
def clean_user_is_current(field_name, as_user=True):
"""Check if the field_name value is a valid link_id and resembles the
current user.
"""
@check_field_is_empty(field_name)
def wrapped(self):
"""Decorator wrapper method.
"""
link_id = clean_link_id(field_name)(self)
user_entity = user.current()
# pylint: disable=E1103
if not user_entity or user_entity.link_id != link_id:
# this user is not the current user
raise forms.ValidationError("This user is not you.")
return user_entity if as_user else link_id
return wrapped
def clean_user_not_exist(field_name):
"""Check if the field_name value is a valid link_id and a user with the
link id does not exist.
"""
@check_field_is_empty(field_name)
def wrapped(self):
"""Decorator wrapper method.
"""
link_id = clean_link_id(field_name)(self)
user_entity = User.get_by_key_name(link_id)
if user_entity:
# user exists already
raise forms.ValidationError("There is already a user with this username.")
return link_id
return wrapped
def clean_users_not_same(field_name):
"""Check if the field_name field is a valid user and is not
equal to the current user.
"""
@check_field_is_empty(field_name)
def wrapped(self):
"""Decorator wrapper method.
"""
clean_user_field = clean_existing_user(field_name)
user_entity = clean_user_field(self)
current_user_entity = user.current()
# pylint: disable=E1103
if user_entity.key() == current_user_entity.key():
# users are equal
raise forms.ValidationError("You cannot enter yourself here.")
return user_entity
return wrapped
def clean_user_account(field_name):
"""Returns the User with the given field_name value.
"""
@check_field_is_empty(field_name)
def wrapped(self):
"""Decorator wrapper method.
"""
email_adress = self.cleaned_data[field_name]
return users.User(email_adress)
return wrapped
def clean_user_account_not_in_use(field_name):
"""Check if the field_name value contains an email
address that hasn't been used for an existing account.
"""
@check_field_is_empty(field_name)
def wrapped(self):
"""Decorator wrapper method.
"""
email_adress = self.cleaned_data.get(field_name).lower()
# get the user account for this email and check if it's in use
user_account = users.User(email_adress)
user_entity = User.all().filter('account', user_account).get()
if user_entity or user.isFormerAccount(user_account):
raise forms.ValidationError("There is already a user "
"with this email address.")
return user_account
return wrapped
def clean_valid_shipping_chars(field_name):
"""Clean method for cleaning a field that must comply with Google's character
requirements for shipping.
"""
@check_field_is_empty(field_name)
def wrapper(self):
"""Decorator wrapper method.
"""
value = self.cleaned_data.get(field_name)
if value and not DEF_VALID_SHIPPING_CHARS.match(value):
raise forms.ValidationError(DEF_INVALID_SHIPPING_CHARS)
return value
return wrapper
def clean_content_length(field_name, min_length=0, max_length=500):
"""Clean method for cleaning a field which must contain at least min and
not more then max length characters.
Args:
field_name: the name of the field needed cleaning
min_length: the minimum amount of allowed characters
max_length: the maximum amount of allowed characters
"""
@check_field_is_empty(field_name)
def wrapper(self):
"""Decorator wrapper method.
"""
value = self.cleaned_data[field_name]
value_length = len(value)
if value_length < min_length:
raise forms.ValidationError(DEF_MUST_BE_ABOVE_LIMIT %(
min_length, value_length))
if value_length > max_length:
raise forms.ValidationError(DEF_MUST_BE_UNDER_LIMIT %(
max_length, value_length))
return value
return wrapper
def clean_phone_number(field_name):
"""Clean method for cleaning a field that may only contain numerical values.
"""
@check_field_is_empty(field_name)
def wrapper(self):
"""Decorator wrapped method.
"""
value = self.cleaned_data.get(field_name)
# allow for a '+' prefix which means '00'
if value[0] == '+':
value = '00' + value[1:]
if not value.isdigit():
raise forms.ValidationError("Only numerical characters are allowed")
return value
return wrapper
def clean_feed_url(field_name):
"""Clean method for cleaning feed url.
"""
def wrapper(self):
"""Decorator wrapped method.
"""
feed_url = self.cleaned_data.get(field_name)
if feed_url == '':
# feed url not supplied (which is OK), so do not try to validate it
return None
if not validate.isFeedURLValid(feed_url):
raise forms.ValidationError('This URL is not a valid ATOM or RSS feed.')
return feed_url
return wrapper
def clean_birth_date(field_name):
"""Clean method for cleaning birth date.
Args:
field_name: the name of the field needed cleaning
program: program entity that the field refers to
check_age: whether the birth date should be checked against the minimal
date for the program
"""
def wrapper(form):
"""Decorator wrapped method.
"""
birth_date = form.cleaned_data.get(field_name)
if form.program and not validate.isAgeSufficientForProgram(
birth_date, form.program):
raise forms.ValidationError(
'Your age does not allow you to participate in the program.')
return birth_date
return wrapper
def sanitize_html_string(content):
"""Sanitizes the given html string.
Raises:
forms.ValidationError in case of an error.
"""
try:
parser = HTMLParser(tokenizer=sanitizer.HTMLSanitizer)
parsed = parser.parseFragment(content, encoding='utf-8')
cleaned_content = ''.join([tag.toxml() for tag in parsed.childNodes])
except (HTMLParseError, ParseError), msg:
raise forms.ValidationError(msg)
return cleaned_content
def clean_html_content(field_name):
"""Clean method for cleaning HTML content.
"""
@check_field_is_empty(field_name)
def wrapped(self):
"""Decorator wrapper method.
"""
content = self.cleaned_data.get(field_name)
# clean_html_content is called when writing data into GAE rather than
# when reading data from GAE. This short-circuiting of the sanitizer
# only affects html authored by developers. The isDeveloper test for
# example allows developers to add javascript.
if user.isDeveloper():
return content
content = sanitize_html_string(content)
return content
return wrapped
def clean_url(field_name):
"""Clean method for cleaning a field belonging to a LinkProperty.
"""
@check_field_is_empty(field_name)
def wrapped(self):
"""Decorator wrapper method.
"""
value = self.cleaned_data.get(field_name)
validator = validators.URLValidator()
# call the Django URLField cleaning method to
# properly clean/validate this field
try:
validator(value)
except forms.ValidationError, e:
if e.code == 'invalid':
msg = ugettext(u'Enter a valid URL.')
raise forms.ValidationError(msg, code='invalid')
return value
return wrapped
def clean_irc(field_name):
"""Clean method for cleaning an irc field.
"""
@check_field_is_empty(field_name)
def wrapped(self):
"""Decorator wrapper method.
"""
value = self.cleaned_data.get(field_name)
validator = validators.URLValidator()
to_clean = value
if value.startswith("irc://"):
to_clean = value.replace("irc://", "http://", 1)
# call the Django URLField cleaning method to
# properly clean/validate this field
try:
validator(to_clean)
except forms.ValidationError, e:
if e.code == 'invalid':
msg = ugettext(u'Enter a valid URL or irc:// url.')
raise forms.ValidationError(msg, code='invalid')
return value
return wrapped
def clean_mailto(field_name):
@check_field_is_empty(field_name)
def wrapped(self):
"""Decorator wrapper method.
"""
value = self.cleaned_data.get(field_name)
validator = validators.URLValidator()
to_clean = value
if value.startswith("mailto:"):
to_clean = value.replace("mailto:", "", 1)
validator = validators.validate_email
# call the Django URLField cleaning method to
# properly clean/validate this field
try:
validator(to_clean)
except forms.ValidationError, e:
if e.code == 'invalid':
msg = ugettext(u'Enter a valid URL or mailto: link.')
raise forms.ValidationError(msg, code='invalid')
return value
return wrapped
def str2set(string_field, separator=','):
"""Clean method for cleaning comma separated strings.
Obtains the separated string from the form and returns it as
a set of strings.
"""
def wrapper(self):
"""Decorator wrapper method.
"""
cleaned_data = self.cleaned_data
string_data = cleaned_data.get(string_field)
list_data = []
for string in string_data.split(separator):
string_strip = string.strip()
if string_strip and string_strip not in list_data:
list_data.append(string_strip)
return list_data
return wrapper