Fix user_self calling editGet with a wrong parameter
[Melange.git] / app / soc / logic / cleaning.py
blob9eac748a3ea40b1ba13e3911bb635096b0e7105d
1 #!/usr/bin/python2.5
3 # Copyright 2008 the Melange authors.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Generic cleaning methods.
18 """
20 __authors__ = [
21 '"Todd Larsen" <tlarsen@google.com>',
22 '"Sverre Rabbelier" <sverre@rabbelier.nl>',
23 '"Lennard de Rijk" <ljvderijk@gmail.com>',
27 import feedparser
29 from google.appengine.api import users
31 from django import forms
32 from django.forms.util import ErrorList
33 from django.utils.translation import ugettext
35 from soc.logic import rights as rights_logic
36 from soc.logic import validate
37 from soc.logic.models import document as document_logic
38 from soc.logic.models.site import logic as site_logic
39 from soc.logic.models.user import logic as user_logic
40 from soc.models import document as document_model
43 DEF_LINK_ID_IN_USE_MSG = ugettext(
44 'This link ID is already in use, please specify another one')
46 DEF_NO_RIGHTS_FOR_ACL_MSG = ugettext(
47 'You do not have the required rights for that ACL.')
49 DEF_ORGANZIATION_NOT_ACTIVE_MSG = ugettext(
50 "This organization is not active or doesn't exist.")
52 DEF_NO_SUCH_DOCUMENT_MSG = ugettext(
53 "There is no such document with that link ID under this entity.")
55 DEF_MUST_BE_ABOVE_LIMIT_FMT = ugettext(
56 "Must be at least %d characters, it has %d characters.")
58 DEF_MUST_BE_UNDER_LIMIT_FMT = ugettext(
59 "Must be under %d characters, it has %d characters.")
62 def check_field_is_empty(field_name):
63 """Returns decorator that bypasses cleaning for empty fields.
64 """
66 def decorator(fun):
67 """Decorator that checks if a field is empty if so doesn't do the cleaning.
69 Note Django will capture errors concerning required fields that are empty.
70 """
71 from functools import wraps
73 @wraps(fun)
74 def wrapper(self):
75 """Decorator wrapper method.
76 """
77 field_content = self.cleaned_data.get(field_name)
79 if not field_content:
80 # field has no content so bail out
81 return None
82 else:
83 # field has contents
84 return fun(self)
85 return wrapper
87 return decorator
90 def clean_empty_field(field_name):
91 """Incorporates the check_field_is_empty as regular cleaner.
92 """
94 @check_field_is_empty(field_name)
95 def wrapper(self):
96 """Decorator wrapper method.
97 """
98 return self.cleaned_data.get(field_name)
100 return wrapper
103 def clean_link_id(field_name):
104 """Checks if the field_name value is in a valid link ID format.
107 @check_field_is_empty(field_name)
108 def wrapper(self):
109 """Decorator wrapper method.
111 # convert to lowercase for user comfort
112 link_id = self.cleaned_data.get(field_name).lower()
113 if not validate.isLinkIdFormatValid(link_id):
114 raise forms.ValidationError("This link ID is in wrong format.")
115 return link_id
116 return wrapper
119 def clean_scope_path(field_name):
120 """Checks if the field_name value is in a valid scope path format.
123 @check_field_is_empty(field_name)
124 def wrapper(self):
125 """Decorator wrapper method.
127 # convert to lowercase for user comfort
128 scope_path = self.cleaned_data.get(field_name).lower()
129 if not validate.isScopePathFormatValid(scope_path):
130 raise forms.ValidationError("This scope path is in wrong format.")
131 return scope_path
132 return wrapper
135 def clean_agrees_to_tos(field_name):
136 """Checks if there is a ToS to see if it is allowed to leave
137 the field_name field false.
140 @check_field_is_empty(field_name)
141 def wrapper(self):
142 """Decorator wrapper method.
144 agrees_to_tos = self.cleaned_data.get(field_name)
146 if not site_logic.getToS(site_logic.getSingleton()):
147 return agrees_to_tos
149 # Site settings specify a site-wide ToS, so agreement is *required*
150 if agrees_to_tos:
151 return True
153 # there was no agreement made so raise an error
154 raise forms.ValidationError(
155 'The site-wide Terms of Service must be accepted to participate'
156 ' on this site.')
158 return wrapper
161 def clean_existing_user(field_name):
162 """Check if the field_name field is a valid user.
165 @check_field_is_empty(field_name)
166 def wrapped(self):
167 """Decorator wrapper method.
169 link_id = clean_link_id(field_name)(self)
171 user_entity = user_logic.getFromKeyFields({'link_id': link_id})
173 if not user_entity:
174 # user does not exist
175 raise forms.ValidationError("This user does not exist.")
177 return user_entity
178 return wrapped
181 def clean_user_is_current(field_name, as_user=True):
182 """Check if the field_name value is a valid link_id and resembles the
183 current user.
186 @check_field_is_empty(field_name)
187 def wrapped(self):
188 """Decorator wrapper method.
190 link_id = clean_link_id(field_name)(self)
192 user_entity = user_logic.getForCurrentAccount()
194 if not user_entity or user_entity.link_id != link_id:
195 # this user is not the current user
196 raise forms.ValidationError("This user is not you.")
198 return user_entity if as_user else link_id
199 return wrapped
202 def clean_user_not_exist(field_name):
203 """Check if the field_name value is a valid link_id and a user with the
204 link id does not exist.
207 @check_field_is_empty(field_name)
208 def wrapped(self):
209 """Decorator wrapper method.
211 link_id = clean_link_id(field_name)(self)
213 user_entity = user_logic.getFromKeyFields({'link_id': link_id})
215 if user_entity:
216 # user exists already
217 raise forms.ValidationError("There is already a user with this link id.")
219 return link_id
220 return wrapped
223 def clean_users_not_same(field_name):
224 """Check if the field_name field is a valid user and is not
225 equal to the current user.
228 @check_field_is_empty(field_name)
229 def wrapped(self):
230 """Decorator wrapper method.
232 clean_user_field = clean_existing_user(field_name)
233 user_entity = clean_user_field(self)
235 current_user_entity = user_logic.getForCurrentAccount()
237 if user_entity.key() == current_user_entity.key():
238 # users are equal
239 raise forms.ValidationError("You cannot enter yourself here.")
241 return user_entity
242 return wrapped
245 def clean_user_account(field_name):
246 """Returns the User with the given field_name value.
249 @check_field_is_empty(field_name)
250 def wrapped(self):
251 """Decorator wrapper method.
253 email_adress = self.cleaned_data[field_name]
254 return users.User(email_adress)
256 return wrapped
259 def clean_user_account_not_in_use(field_name):
260 """Check if the field_name value contains an email
261 address that hasn't been used for an existing account.
264 @check_field_is_empty(field_name)
265 def wrapped(self):
266 """Decorator wrapper method.
268 email_adress = self.cleaned_data.get(field_name).lower()
270 # get the user account for this email and check if it's in use
271 user_account = users.User(email_adress)
273 fields = {'account': user_account}
274 user_entity = user_logic.getForFields(fields, unique=True)
276 if user_entity or user_logic.isFormerAccount(user_account):
277 raise forms.ValidationError("There is already a user "
278 "with this email adress.")
280 return user_account
281 return wrapped
284 def clean_ascii_only(field_name):
285 """Clean method for cleaning a field that may only contain ASCII-characters.
288 @check_field_is_empty(field_name)
289 def wrapper(self):
290 """Decorator wrapper method.
293 value = self.cleaned_data.get(field_name)
295 try:
296 # encode to ASCII
297 value = value.encode("ascii")
298 except UnicodeEncodeError:
299 # can not encode as ASCII
300 raise forms.ValidationError("Only ASCII characters are allowed")
302 return value
303 return wrapper
306 def clean_content_length(field_name, min_length=0, max_length=500):
307 """Clean method for cleaning a field which must contain at least min and
308 not more then max length characters.
310 Args:
311 field_name: the name of the field needed cleaning
312 min_length: the minimum amount of allowed characters
313 max_length: the maximum amount of allowed characters
316 @check_field_is_empty(field_name)
317 def wrapper(self):
318 """Decorator wrapper method.
321 value = self.cleaned_data[field_name]
322 value_length = len(value)
324 if value_length < min_length:
325 raise forms.ValidationError(DEF_MUST_BE_ABOVE_LIMIT_FMT %(
326 min_length, value_length))
328 if value_length > max_length:
329 raise forms.ValidationError(DEF_MUST_BE_UNDER_LIMIT_FMT %(
330 max_length, value_length))
332 return value
333 return wrapper
336 def clean_phone_number(field_name):
337 """Clean method for cleaning a field that may only contain numerical values.
340 @check_field_is_empty(field_name)
341 def wrapper(self):
342 """Decorator wrapped method.
345 value = self.cleaned_data.get(field_name)
347 # allow for a '+' prefix which means '00'
348 if value[0] == '+':
349 value = '00' + value[1:]
351 if not value.isdigit():
352 raise forms.ValidationError("Only numerical characters are allowed")
354 return value
355 return wrapper
358 def clean_feed_url(self):
359 """Clean method for cleaning feed url.
362 feed_url = self.cleaned_data.get('feed_url')
364 if feed_url == '':
365 # feed url not supplied (which is OK), so do not try to validate it
366 return None
368 if not validate.isFeedURLValid(feed_url):
369 raise forms.ValidationError('This URL is not a valid ATOM or RSS feed.')
371 return feed_url
374 def clean_html_content(field_name):
375 """Clean method for cleaning HTML content.
378 @check_field_is_empty(field_name)
379 def wrapped(self):
380 """Decorator wrapper method.
383 content = self.cleaned_data.get(field_name)
385 if user_logic.isDeveloper():
386 return content
388 sanitizer = feedparser._HTMLSanitizer('utf-8')
389 sanitizer.feed(content)
390 content = sanitizer.output()
391 content = content.decode('utf-8')
392 content = content.strip().replace('\r\n', '\n')
394 return content
396 return wrapped
399 def clean_url(field_name):
400 """Clean method for cleaning a field belonging to a LinkProperty.
403 @check_field_is_empty(field_name)
404 def wrapped(self):
405 """Decorator wrapper method.
408 value = self.cleaned_data.get(field_name)
410 # call the Django URLField cleaning method to
411 # properly clean/validate this field
412 return forms.URLField.clean(self.fields[field_name], value)
413 return wrapped
416 def clean_refs(params, fields):
417 """Cleans all references to make sure they are valid.
420 logic = params['logic']
422 def wrapped(self):
423 """Decorator wrapper method.
426 scope_path = logic.getKeyNameFromFields(self.cleaned_data)
428 key_fields = {
429 'scope_path': scope_path,
430 'prefix': params['document_prefix'],
433 for field in fields:
434 link_id = self.cleaned_data.get(field)
436 if not link_id:
437 continue
439 key_fields['link_id'] = link_id
440 ref = document_logic.logic.getFromKeyFields(key_fields)
442 if not ref:
443 self._errors[field] = ErrorList([DEF_NO_SUCH_DOCUMENT_MSG])
444 del self.cleaned_data[field]
445 else:
446 self.cleaned_data['resolved_%s' % field] = ref
448 return self.cleaned_data
450 return wrapped
453 def validate_user_edit(link_id_field, account_field):
454 """Clean method for cleaning user edit form.
456 Raises ValidationError if:
457 -Another User has the given email address as account
458 -Another User has the given email address in it's FormerAccounts list
461 def wrapper(self):
462 """Decorator wrapper method.
464 cleaned_data = self.cleaned_data
466 link_id = cleaned_data.get(link_id_field)
467 user_account = cleaned_data.get(account_field)
469 # if both fields were valid do this check
470 if link_id and user_account:
471 # get the user from the link_id in the form
473 user_entity = user_logic.getFromKeyFields({'link_id': link_id})
475 # if it's not the user's current account
476 if user_entity.account != user_account:
478 # get the user having the given account
479 fields = {'account': user_account}
480 user_from_account_entity = user_logic.getForFields(fields,
481 unique=True)
483 # if there is a user with the given account or it's a former account
484 if user_from_account_entity or \
485 user_logic.isFormerAccount(user_account):
486 # raise an error because this email address can't be used
487 raise forms.ValidationError("There is already a user with "
488 "this email address.")
490 return cleaned_data
491 return wrapper
494 def validate_new_group(link_id_field, scope_path_field,
495 group_logic, group_app_logic):
496 """Clean method used to clean the group application or new group form.
498 Raises ValidationError if:
499 -A application with this link id and scope path already exists
500 -A group with this link id and scope path already exists
503 def wrapper(self):
504 """Decorator wrapper method.
506 cleaned_data = self.cleaned_data
508 fields = {}
510 link_id = cleaned_data.get(link_id_field)
512 if link_id:
513 fields['link_id'] = link_id
515 scope_path = cleaned_data.get(scope_path_field)
516 if scope_path:
517 fields['scope_path'] = scope_path
519 # get the application
520 group_app_entity = group_app_logic.logic.getForFields(fields, unique=True)
522 # get the current user
523 user_entity = user_logic.getForCurrentAccount()
525 # if the proposal has not been accepted or it's not the applicant
526 # creating the new group then show link ID in use message
527 if group_app_entity and (group_app_entity.status != 'accepted' or (
528 group_app_entity.applicant.key() != user_entity.key())):
529 # add the error message to the link id field
530 self._errors[link_id_field] = ErrorList([DEF_LINK_ID_IN_USE_MSG])
531 del cleaned_data[link_id_field]
532 # return the new cleaned_data
533 return cleaned_data
535 # check if there is already a group for the given fields
536 group_entity = group_logic.logic.getForFields(fields, unique=True)
538 if group_entity:
539 # add the error message to the link id field
540 self._errors[link_id_field] = ErrorList([DEF_LINK_ID_IN_USE_MSG])
541 del cleaned_data[link_id_field]
542 # return the new cleaned_data
543 return cleaned_data
545 return cleaned_data
546 return wrapper
548 def validate_student_proposal(org_field, scope_field,
549 student_logic, org_logic):
550 """Validates the form of a student proposal.
552 Raises ValidationError if:
553 -The organization link_id does not match an active organization
554 -The hidden scope path is not a valid active student
557 def wrapper(self):
558 """Decorator wrapper method.
560 cleaned_data = self.cleaned_data
562 org_link_id = cleaned_data.get(org_field)
563 scope_path = cleaned_data.get(scope_field)
565 # only if both fields are valid
566 if org_link_id and scope_path:
567 filter = {'scope_path': scope_path,
568 'status': 'active'}
570 student_entity = student_logic.logic.getFromKeyName(scope_path)
572 if not student_entity or student_entity.status != 'active':
573 # raise validation error, access checks should have prevented this
574 raise forms.ValidationError(
575 ugettext("The given student is not valid."))
577 filter = {'link_id': org_link_id,
578 'scope': student_entity.scope,
579 'status': 'active'}
581 org_entity = org_logic.logic.getForFields(filter, unique=True)
583 if not org_entity:
584 #raise validation error, non valid organization entered
585 self._errors['organization'] = ErrorList(
586 [DEF_ORGANZIATION_NOT_ACTIVE_MSG])
587 del cleaned_data['organization']
589 return cleaned_data
590 return wrapper
592 def validate_student_project(org_field, mentor_field, student_field):
593 """Validates the form of a student proposal.
595 Args:
596 org_field: Field containing key_name for org
597 mentor_field: Field containing the link_id of the mentor
598 student_field: Field containing the student link_id
600 Raises ValidationError if:
601 -A valid Organization does not exist for the given keyname
602 -The mentor link_id does not match a mentor for the active organization
603 -The student link_id does not match a student in the org's Program
606 def wrapper(self):
607 """Decorator wrapper method.
609 from soc.logic.models.mentor import logic as mentor_logic
610 from soc.logic.models.organization import logic as org_logic
611 from soc.logic.models.student import logic as student_logic
613 cleaned_data = self.cleaned_data
615 org_key_name = cleaned_data.get(org_field)
616 mentor_link_id = cleaned_data.get(mentor_field)
617 student_link_id = cleaned_data.get(student_field)
619 if not (org_key_name and mentor_link_id and student_link_id):
620 # we can't do the check the other cleaners will pickup empty fields
621 return cleaned_data
623 org_entity = org_logic.getFromKeyName(org_key_name)
625 if not org_entity:
626 # show error message
627 raise forms.ValidationError(
628 ugettext("The given Organization is not valid."))
630 fields = {'link_id': mentor_link_id,
631 'scope': org_entity,
632 'status': 'active'}
634 mentor_entity = mentor_logic.getForFields(fields, unique=True,)
636 if not mentor_entity:
637 # show error message
638 raise forms.ValidationError(
639 ugettext("The given Mentor is not valid."))
641 fields = {'link_id': student_link_id,
642 'scope': org_entity.scope,
643 'status': 'active'}
645 student_entity = student_logic.getForFields(fields, unique=True)
647 if not student_entity:
648 #show error message
649 raise forms.ValidationError(
650 ugettext("The given Student is not valid."))
652 # successfully validated
653 return cleaned_data
655 return wrapper
657 def validate_document_acl(view, creating=False):
658 """Validates that the document ACL settings are correct.
661 def wrapper(self):
662 """Decorator wrapper method.
664 cleaned_data = self.cleaned_data
665 read_access = cleaned_data.get('read_access')
666 write_access = cleaned_data.get('write_access')
668 if not (read_access and write_access and ('prefix' in cleaned_data)):
669 return cleaned_data
671 if read_access != 'public':
672 ordening = document_model.Document.DOCUMENT_ACCESS
673 if ordening.index(read_access) < ordening.index(write_access):
674 raise forms.ValidationError(
675 "Read access should be less strict than write access.")
677 params = view.getParams()
678 rights = params['rights']
680 user = user_logic.getForCurrentAccount()
682 rights.setCurrentUser(user.account, user)
684 prefix = self.cleaned_data['prefix']
685 scope_path = self.cleaned_data['scope_path']
687 validate_access(self, view, rights, prefix, scope_path, 'read_access')
688 validate_access(self, view, rights, prefix, scope_path, 'write_access')
690 if creating and not has_access(rights, 'restricted', scope_path, prefix):
691 raise forms.ValidationError(
692 "You do not have the required access to create this document.")
694 return cleaned_data
696 return wrapper
699 def has_access(rights, access_level, scope_path, prefix):
700 """Checks whether the current user has the required access.
703 checker = rights_logic.Checker(prefix)
704 roles = checker.getMembership(access_level)
706 django_args = {
707 'scope_path': scope_path,
708 'prefix': prefix,
711 return rights.hasMembership(roles, django_args)
713 def validate_access(self, view, rights, prefix, scope_path, field):
714 """Validates that the user has access to the ACL for the specified fields.
717 access_level = self.cleaned_data[field]
719 if not has_access(rights, access_level, scope_path, prefix):
720 self._errors[field] = ErrorList([DEF_NO_RIGHTS_FOR_ACL_MSG])
721 del self.cleaned_data[field]