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.
21 '"Todd Larsen" <tlarsen@google.com>',
22 '"Sverre Rabbelier" <sverre@rabbelier.nl>',
23 '"Lennard de Rijk" <ljvderijk@gmail.com>',
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.
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.
71 from functools
import wraps
75 """Decorator wrapper method.
77 field_content
= self
.cleaned_data
.get(field_name
)
80 # field has no content so bail out
90 def clean_empty_field(field_name
):
91 """Incorporates the check_field_is_empty as regular cleaner.
94 @check_field_is_empty(field_name
)
96 """Decorator wrapper method.
98 return self
.cleaned_data
.get(field_name
)
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
)
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.")
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
)
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.")
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
)
142 """Decorator wrapper method.
144 agrees_to_tos
= self
.cleaned_data
.get(field_name
)
146 if not site_logic
.getToS(site_logic
.getSingleton()):
149 # Site settings specify a site-wide ToS, so agreement is *required*
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'
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
)
167 """Decorator wrapper method.
169 link_id
= clean_link_id(field_name
)(self
)
171 user_entity
= user_logic
.getFromKeyFields({'link_id': link_id
})
174 # user does not exist
175 raise forms
.ValidationError("This user does not exist.")
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
186 @check_field_is_empty(field_name
)
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
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
)
209 """Decorator wrapper method.
211 link_id
= clean_link_id(field_name
)(self
)
213 user_entity
= user_logic
.getFromKeyFields({'link_id': link_id
})
216 # user exists already
217 raise forms
.ValidationError("There is already a user with this link id.")
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
)
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():
239 raise forms
.ValidationError("You cannot enter yourself here.")
245 def clean_user_account(field_name
):
246 """Returns the User with the given field_name value.
249 @check_field_is_empty(field_name
)
251 """Decorator wrapper method.
253 email_adress
= self
.cleaned_data
[field_name
]
254 return users
.User(email_adress
)
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
)
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.")
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
)
290 """Decorator wrapper method.
293 value
= self
.cleaned_data
.get(field_name
)
297 value
= value
.encode("ascii")
298 except UnicodeEncodeError:
299 # can not encode as ASCII
300 raise forms
.ValidationError("Only ASCII characters are allowed")
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.
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
)
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
))
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
)
342 """Decorator wrapped method.
345 value
= self
.cleaned_data
.get(field_name
)
347 # allow for a '+' prefix which means '00'
349 value
= '00' + value
[1:]
351 if not value
.isdigit():
352 raise forms
.ValidationError("Only numerical characters are allowed")
358 def clean_feed_url(self
):
359 """Clean method for cleaning feed url.
362 feed_url
= self
.cleaned_data
.get('feed_url')
365 # feed url not supplied (which is OK), so do not try to validate it
368 if not validate
.isFeedURLValid(feed_url
):
369 raise forms
.ValidationError('This URL is not a valid ATOM or RSS feed.')
374 def clean_html_content(field_name
):
375 """Clean method for cleaning HTML content.
378 @check_field_is_empty(field_name
)
380 """Decorator wrapper method.
383 content
= self
.cleaned_data
.get(field_name
)
385 if user_logic
.isDeveloper():
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')
399 def clean_url(field_name
):
400 """Clean method for cleaning a field belonging to a LinkProperty.
403 @check_field_is_empty(field_name
)
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
)
416 def clean_refs(params
, fields
):
417 """Cleans all references to make sure they are valid.
420 logic
= params
['logic']
423 """Decorator wrapper method.
426 scope_path
= logic
.getKeyNameFromFields(self
.cleaned_data
)
429 'scope_path': scope_path
,
430 'prefix': params
['document_prefix'],
434 link_id
= self
.cleaned_data
.get(field
)
439 key_fields
['link_id'] = link_id
440 ref
= document_logic
.logic
.getFromKeyFields(key_fields
)
443 self
._errors
[field
] = ErrorList([DEF_NO_SUCH_DOCUMENT_MSG
])
444 del self
.cleaned_data
[field
]
446 self
.cleaned_data
['resolved_%s' % field
] = ref
448 return self
.cleaned_data
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
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
,
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.")
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
504 """Decorator wrapper method.
506 cleaned_data
= self
.cleaned_data
510 link_id
= cleaned_data
.get(link_id_field
)
513 fields
['link_id'] = link_id
515 scope_path
= cleaned_data
.get(scope_path_field
)
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
535 # check if there is already a group for the given fields
536 group_entity
= group_logic
.logic
.getForFields(fields
, unique
=True)
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
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
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
,
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
,
581 org_entity
= org_logic
.logic
.getForFields(filter, unique
=True)
584 #raise validation error, non valid organization entered
585 self
._errors
['organization'] = ErrorList(
586 [DEF_ORGANZIATION_NOT_ACTIVE_MSG
])
587 del cleaned_data
['organization']
592 def validate_student_project(org_field
, mentor_field
, student_field
):
593 """Validates the form of a student proposal.
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
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
623 org_entity
= org_logic
.getFromKeyName(org_key_name
)
627 raise forms
.ValidationError(
628 ugettext("The given Organization is not valid."))
630 fields
= {'link_id': mentor_link_id
,
634 mentor_entity
= mentor_logic
.getForFields(fields
, unique
=True,)
636 if not mentor_entity
:
638 raise forms
.ValidationError(
639 ugettext("The given Mentor is not valid."))
641 fields
= {'link_id': student_link_id
,
642 'scope': org_entity
.scope
,
645 student_entity
= student_logic
.getForFields(fields
, unique
=True)
647 if not student_entity
:
649 raise forms
.ValidationError(
650 ugettext("The given Student is not valid."))
652 # successfully validated
657 def validate_document_acl(view
, creating
=False):
658 """Validates that the document ACL settings are correct.
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
)):
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.")
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
)
707 'scope_path': scope_path
,
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
]