blob: dc43d028e7ccad81d2c6ab8d643bfe7220f40e08 [file] [log] [blame]
# Copyright 2014 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.
"""Module with generalized profile related views.
The classes defined here are supposed to be instantiated with dependencies that
are specific to actual programs.
"""
from google.appengine.ext import ndb
from django import http
from django import forms as django_forms
from django.core import validators
from django.utils import html
from django.utils import translation
from melange import types
from melange.appengine import db as melange_db
from melange.logic import address as address_logic
from melange.logic import contact as contact_logic
from melange.logic import education as education_logic
from melange.logic import profile as profile_logic
from melange.logic import signature as signature_logic
from melange.logic import user as user_logic
from melange.models import education as education_model
from melange.models import profile as profile_model
from melange.request import access
from melange.request import exception
from melange.request import links
from melange.templates import profile_form_below_header
from melange.utils import countries
from melange.utils import rich_bool
from melange.views import student_forms as student_forms_view
from melange.views.helper import form_handler
from soc.logic import cleaning
from soc.logic import program as program_logic
from soc.logic import validate
from soc.models import universities
from soc.views import base
from soc.views import forms as soc_forms
from soc.views import toggle_button
from soc.views.helper import url_patterns
# TODO(daniel): tabs make sense only for Summer Of Code
from summerofcode.templates import tabs
# TODO(daniel): move these messages out of summerofcode package
from summerofcode.templates import top_message
_INELIGIBLE_TO_REGISTER_AS_STUDENT_HAS_ROLE_ALREADY = translation.ugettext(
'This page is accessible only to users without roles for any '
'organizations.')
_INELIGIBLE_TO_REGISTER_AS_STUDENT_IS_STUDENT_ALREADY = translation.ugettext(
'You have already registered as a student. You can\'t register again.')
_ALPHANUMERIC_CHARACTERS_ONLY = unicode(
'Use alphanumeric characters (A-z, 0-9) and whitespaces only.')
_INVALID_EDUCATION_DATA = unicode(
'Invalid education data submitted in the form.')
PROFILE_ORG_MEMBER_CREATE_PAGE_NAME = translation.ugettext(
'Create mentor profile')
PROFILE_STUDENT_CREATE_PAGE_NAME = translation.ugettext(
'Create student profile')
PROFILE_EDIT_PAGE_NAME = translation.ugettext(
'Edit profile')
PROFILE_DELETE_PAGE_NAME = translation.ugettext('Delete profile')
PROFILE_LOOKUP_PAGE_NAME = translation.ugettext('Lookup profile')
# names of structures to group related fields together
_BASIC_INFORMATION_GROUP = translation.ugettext('1. Basic information')
_CONTACT_GROUP = translation.ugettext('2. Contact information')
_RESIDENTIAL_ADDRESS_GROUP = translation.ugettext('3. Residential address')
_SHIPPING_ADDRESS_GROUP = translation.ugettext('4. Shipping address')
_OTHER_INFORMATION_GROUP = translation.ugettext('5. Other information')
_EDUCATION_GROUP = translation.ugettext('6. Education')
_FORMS_GROUP = translation.ugettext('7. Forms')
_TERMS_OF_SERVICE_GROUP = translation.ugettext('8. Terms and conditions')
USER_ID_HELP_TEXT = translation.ugettext(
'Used as part of various URL links throughout the site. '
'ASCII lower case letters, digits, and underscores only.')
PUBLIC_NAME_HELP_TEXT = translation.ugettext(
'Name that will be displayed publicly on the site.')
WEB_PAGE_HELP_TEXT = translation.ugettext(
'URL to your personal web page, if you have one.')
BLOG_HELP_TEXT = translation.ugettext(
'URL to a page with your personal blog, if you have one.')
PHOTO_URL_HELP_TEXT = translation.ugettext(
'URL to 64x64 pixel thumbnail image.')
FIRST_NAME_HELP_TEXT = translation.ugettext(
'First name of the participant. ' + _ALPHANUMERIC_CHARACTERS_ONLY)
LAST_NAME_HELP_TEXT = translation.ugettext(
'Last name of the participant. ' + _ALPHANUMERIC_CHARACTERS_ONLY)
EMAIL_HELP_TEXT = translation.ugettext(
'Email address of the participant. All program related emails '
'will be sent to this address. Visible only to program administrators '
'and organizations you participate in.')
PHONE_HELP_TEXT = translation.ugettext(
'Phone number of the participant. Use digits only and remember '
'to include the country code. This information is kept '
'private and used only for shipping purposes by program administrators.')
RESIDENTIAL_STREET_HELP_TEXT = translation.ugettext(
'Street number and name information plus optional suite/apartment number. '
+ _ALPHANUMERIC_CHARACTERS_ONLY)
RESIDENTIAL_CITY_HELP_TEXT = translation.ugettext(
'City information. ' + _ALPHANUMERIC_CHARACTERS_ONLY)
RESIDENTIAL_PROVINCE_HELP_TEXT = translation.ugettext(
'State or province information. In case you live in the United States, '
'type the two letter state abbreviation. ' + _ALPHANUMERIC_CHARACTERS_ONLY)
RESIDENTIAL_COUNTRY_HELP_TEXT = translation.ugettext('Country information.')
RESIDENTIAL_POSTAL_CODE_HELP_TEXT = translation.ugettext(
'ZIP/Postal code information. ' + _ALPHANUMERIC_CHARACTERS_ONLY)
IS_SHIPPING_ADDRESS_DIFFERENT_HELP_TEXT = translation.ugettext(
'Check this box if your shipping address is different than '
'the residential address provided above.')
SHIPPING_NAME_HELP_TEXT = translation.ugettext(
'Fill in the name of the person who should be receiving your packages. '
+ _ALPHANUMERIC_CHARACTERS_ONLY)
SHIPPING_STREET_HELP_TEXT = translation.ugettext(
'Street number and name information plus optional suite/apartment number. '
+ _ALPHANUMERIC_CHARACTERS_ONLY)
SHIPPING_CITY_HELP_TEXT = translation.ugettext(
'City information. ' + _ALPHANUMERIC_CHARACTERS_ONLY)
SHIPPING_PROVINCE_HELP_TEXT = translation.ugettext(
'State or province information. In case packages should be sent to '
'the United States, type the two letter state abbreviation. '
+ _ALPHANUMERIC_CHARACTERS_ONLY)
SHIPPING_COUNTRY_HELP_TEXT = translation.ugettext('Country information.')
SHIPPING_POSTAL_CODE_HELP_TEXT = translation.ugettext(
'ZIP/Postal code information. ' + _ALPHANUMERIC_CHARACTERS_ONLY)
BIRTH_DATE_HELP_TEXT = translation.ugettext(
'Birth date of the participant. Use YYYY-MM-DD format. This '
'information is kept private and visible only to program administrators '
'in order to determine program eligibility.')
TEE_STYLE_HELP_TEXT = translation.ugettext(
'Style of a T-Shirt that may be sent to you upon program completion.')
TEE_SIZE_HELP_TEXT = translation.ugettext(
'Size of a T-Shirt that may be sent to you upon program completion.')
GENDER_HELP_TEXT = translation.ugettext(
'Gender information of the participant. This information '
'is kept private and visible only to program administrators for '
'statistical purposes.')
PROGRAM_KNOWLEDGE_HELP_TEXT = translation.ugettext(
'Be as specific as possible, e.g. blog post (include URL '
'if possible), mailing list (please include list address), information '
'session (please include location and speakers if you can), etc.')
SCHOOL_COUNTRY_HELP_TEXT = translation.ugettext(
'Country in which the school to which you are enrolled is located.')
SCHOOL_NAME_HELP_TEXT = translation.ugettext(
'Start typing your school name and use the autocomplete dropdown. '
'If your school, college or university is not listed, '
'enter the full English name. '
'Use the complete formal name of your school, e.g. '
'"University of California at Berkeley", instead of "Cal" or "UCB".')
SCHOOL_WEB_PAGE_HELP_TEXT = translation.ugettext(
'URL to the home page of your school.')
MAJOR_HELP_TEXT = translation.ugettext('Your major at the university.')
DEGREE_HELP_TEXT = translation.ugettext(
'Select degree that is the one you are working towards today.')
GRADE_HELP_TEXT = translation.ugettext(
'Enter your grade in the school as a number, e.g. 8 if you are '
'in the 8th grade.')
EXPECTED_GRADUATION_HELP_TEXT = translation.ugettext(
'Provide the year in which you are expected to graduate '
'from your current program.')
USER_ID_LABEL = translation.ugettext('Username')
PUBLIC_NAME_LABEL = translation.ugettext('Public name')
WEB_PAGE_LABEL = translation.ugettext('Home page URL')
BLOG_LABEL = translation.ugettext('Blog URL')
PHOTO_URL_LABEL = translation.ugettext('Photo URL')
FIRST_NAME_LABEL = translation.ugettext('First name')
LAST_NAME_LABEL = translation.ugettext('Last name')
EMAIL_LABEL = translation.ugettext('Email')
PHONE_LABEL = translation.ugettext('Phone number')
RESIDENTIAL_STREET_LABEL = translation.ugettext('Street address')
RESIDENTIAL_CITY_LABEL = translation.ugettext('City')
RESIDENTIAL_PROVINCE_LABEL = translation.ugettext('State/Province')
RESIDENTIAL_COUNTRY_LABEL = translation.ugettext('Country/Territory')
COUNTRY_DEFAULT = translation.ugettext('Please select a country:')
RESIDENTIAL_POSTAL_CODE_LABEL = translation.ugettext('ZIP/Postal code')
IS_SHIPPING_ADDRESS_DIFFERENT_LABEL = translation.ugettext(
'Shipping address is different than residential address')
SHIPPING_NAME_LABEL = translation.ugettext('Full recipient name')
SHIPPING_STREET_LABEL = translation.ugettext('Street address')
SHIPPING_CITY_LABEL = translation.ugettext('City')
SHIPPING_PROVINCE_LABEL = translation.ugettext('State/Province')
SHIPPING_COUNTRY_LABEL = translation.ugettext('Country/Territory')
SHIPPING_POSTAL_CODE_LABEL = translation.ugettext('ZIP/Postal code')
BIRTH_DATE_LABEL = translation.ugettext('Birth date')
TEE_STYLE_LABEL = translation.ugettext('T-Shirt style')
TEE_SIZE_LABEL = translation.ugettext('T-Shirt size')
GENDER_LABEL = translation.ugettext('Gender')
PROGRAM_KNOWLEDGE_LABEL = translation.ugettext(
'How did you hear about the program?')
TERMS_OF_SERVICE_LABEL = translation.ugettext(
'Read the terms and conditions and acknowledge by selecting the checkbox '
'at the end.')
TERMS_OF_SERVICE_READONLY_LABEL = translation.ugettext(
'You have already agreed to the terms and conditions.')
TERMS_OF_SERVICE_AGREE_TEXT = translation.ugettext(
'I have read, understand, and agree to these terms and conditions.')
SCHOOL_COUNTRY_LABEL = translation.ugettext('School country')
SCHOOL_NAME_LABEL = translation.ugettext('School name')
SCHOOL_WEB_PAGE_LABEL = translation.ugettext('School web page')
MAJOR_LABEL = translation.ugettext('Major')
DEGREE_LABEL = translation.ugettext('Degree')
GRADE_LABEL = translation.ugettext('Grade')
EXPECTED_GRADUATION_LABEL = translation.ugettext('Expected graduation')
ENROLLMENT_FORM_LABEL = translation.ugettext('Enrollment form')
TAX_FORM_LABEL = translation.ugettext('Tax form')
_NO_FORM_SUBMITTED = translation.ugettext('No form submitted.')
_NO_FORM_REQUIRED = translation.ugettext('No form required at this time.')
_FORM_SUBMITTED_NO_UPLOAD_FORM_OPTION = translation.ugettext(
'%s <a href="%s">Download</a>')
_FORM_SUBMITTED_UPLOAD_FORM_OPTION = translation.ugettext(
'%s <a href="%s">Re-upload</a> <a href="%s">Download</a>')
_NO_FORM_SUBMITTED_UPLOAD_FORM_OPTION = translation.ugettext(
'No form submitted. <a href="%s">Upload</a>')
_STREET_VALUE = '%s<br>%s'
TERMS_OF_SERVICE_NOT_ACCEPTED = translation.ugettext(
'You cannot register without agreeing to the terms and conditions')
INSUFFICIENT_AGE = translation.ugettext(
'Your age does not allow you to participate in the program.')
AGE_ALREADY_SET = translation.ugettext(
'Age is already set for your profile.')
INVALID_SHIPPING_ADDRESS_PART = translation.ugettext(
'This field cannot be specified if the shipping address is the same '
'as the residential address.')
MISSING_REQUIRED_FIELD = 'This field is required.'
_TEE_DEFAULT_VERBOSE = translation.ugettext('Please Select')
TEE_DEFAULT_CHOICE = (('', _TEE_DEFAULT_VERBOSE),)
_TEE_STYLE_FEMALE_ID = 'female'
_TEE_STYLE_MALE_ID = 'male'
TEE_STYLE_CHOICES = (
(_TEE_STYLE_FEMALE_ID, 'Female'),
(_TEE_STYLE_MALE_ID, 'Male'))
_TEE_SIZE_XS_ID = 'xs'
_TEE_SIZE_S_ID = 's'
_TEE_SIZE_M_ID = 'm'
_TEE_SIZE_L_ID = 'l'
_TEE_SIZE_XL_ID = 'xl'
_TEE_SIZE_XXL_ID = 'xxl'
_TEE_SIZE_XXXL_ID = 'xxxl'
TEE_SIZE_CHOICES = (
(_TEE_SIZE_XS_ID, 'XS'),
(_TEE_SIZE_S_ID, 'S'),
(_TEE_SIZE_M_ID, 'M'),
(_TEE_SIZE_L_ID, 'L'),
(_TEE_SIZE_XL_ID, 'XL'),
(_TEE_SIZE_XXL_ID, 'XXL'),
(_TEE_SIZE_XXXL_ID, 'XXXL'))
_GENDER_FEMALE_ID = 'female'
_GENDER_MALE_ID = 'male'
_GENDER_OTHER_ID = 'other'
_GENDER_NOT_DISCLOSED_ID = 'not_answered'
_GENDER_DEFAULT_VERBOSE = translation.ugettext('Please select a gender')
_GENDER_FEMALE_VERBOSE = translation.ugettext('Female')
_GENDER_MALE_VERBOSE = translation.ugettext('Male')
_GENDER_OTHER_VERBOSE = translation.ugettext('Other')
_GENDER_NOT_DISCLOSED_VERBOSE = translation.ugettext(
'I would prefer not to answer')
GENDER_DEFAULT_CHOICE = (('', _GENDER_DEFAULT_VERBOSE),)
GENDER_CHOICES = (
(_GENDER_FEMALE_ID, _GENDER_FEMALE_VERBOSE),
(_GENDER_MALE_ID, _GENDER_MALE_VERBOSE),
(_GENDER_OTHER_ID, _GENDER_OTHER_VERBOSE),
(_GENDER_NOT_DISCLOSED_ID, _GENDER_NOT_DISCLOSED_VERBOSE))
_PROGRAM_KNOWLEDGE_SCHOOL_ID = 'school'
_PROGRAM_KNOWLEDGE_SITE_ID = 'site'
_PROGRAM_KNOWLEDGE_FRIEND_ID = 'friend'
_PROGRAM_KNOWLEDGE_INTERNET_ID = 'internet'
_PROGRAM_KNOWLEDGE_OTHER_ID = 'other'
_PROGRAM_KNOWLEDGE_DEFAULT_VERBOSE = translation.ugettext('Please select')
_PROGRAM_KNOWLEDGE_SCHOOL_VERBOSE = translation.ugettext(
'At school/from my professor or advisor')
_PROGRAM_KNOWLEDGE_SITE_VERBOSE = translation.ugettext(
'Google site / blog post')
_PROGRAM_KNOWLEDGE_FRIEND_VERBOSE = translation.ugettext(
'From a friend / classmate / family member')
_PROGRAM_KNOWLEDGE_INTERNET_VERBOSE = translation.ugettext(
'Internet')
_PROGRAM_KNOWLEDGE_OTHER_VERBOSE = translation.ugettext(
'Other')
PROGRAM_KNOWLEDGE_DEFAULT_CHOICE = (('', _PROGRAM_KNOWLEDGE_DEFAULT_VERBOSE),)
PROGRAM_KNOWLEDGE_CHOICES = (
(_PROGRAM_KNOWLEDGE_SCHOOL_ID, _PROGRAM_KNOWLEDGE_SCHOOL_VERBOSE),
(_PROGRAM_KNOWLEDGE_SITE_ID, _PROGRAM_KNOWLEDGE_SITE_VERBOSE),
(_PROGRAM_KNOWLEDGE_FRIEND_ID, _PROGRAM_KNOWLEDGE_FRIEND_VERBOSE),
(_PROGRAM_KNOWLEDGE_INTERNET_ID, _PROGRAM_KNOWLEDGE_INTERNET_VERBOSE),
(_PROGRAM_KNOWLEDGE_OTHER_ID, _PROGRAM_KNOWLEDGE_OTHER_VERBOSE))
_DEGREE_UNDERGRADUATE_ID = 'undergraduate'
_DEGREE_MASTERS_ID = 'masters'
_DEGREE_PHD_ID = 'phd'
DEGREE_CHOICES = (
(_DEGREE_UNDERGRADUATE_ID, translation.ugettext('Undergraduate')),
(_DEGREE_MASTERS_ID, translation.ugettext('Master\'s')),
(_DEGREE_PHD_ID, translation.ugettext('PhD')))
_USER_PROPERTIES_FORM_KEYS = ['user_id']
_PROFILE_PROPERTIES_FORM_KEYS = [
'public_name', 'photo_url', 'first_name', 'last_name', 'birth_date',
'tee_style', 'tee_size', 'gender', 'terms_of_service', 'program_knowledge',
'program_knowledge_other']
_CONTACT_PROPERTIES_FORM_KEYS = ['web_page', 'blog', 'email', 'phone']
_RESIDENTIAL_ADDRESS_PROPERTIES_FORM_KEYS = [
'residential_street', 'residential_street_extra', 'residential_city',
'residential_province', 'residential_country', 'residential_postal_code']
_SHIPPING_ADDRESS_PROPERTIES_FORM_KEYS = [
'shipping_name', 'shipping_street', 'shipping_street_extra',
'shipping_city', 'shipping_province', 'shipping_country',
'shipping_postal_code']
_STUDENT_DATA_PROPERTIES_FORM_FIELDS = [
'school_country', 'school_name', 'school_web_page', 'major', 'degree',
'grade', 'expected_graduation']
def streetValue(address):
"""Returns full street information for the given address.
Args:
address: address_model.Address entity.
Returns:
A string containing the street information.
"""
if not address.street_extra:
return address.street
else:
return html.format_html(
_STREET_VALUE % (address.street, address.street_extra))
def cleanUserId(user_id):
"""Cleans user_id field.
Args:
user_id: The submitted user ID.
Returns:
Cleaned value for user_id field.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
if not user_id:
raise django_forms.ValidationError(MISSING_REQUIRED_FIELD)
cleaning.cleanLinkID(user_id)
return user_id
def _cleanShippingAddressPart(
is_shipping_address_different, value, is_required):
"""Cleans a field that represents a part of the shipping address.
Args:
is_shipping_address_different: A bool indicating if the shipping address
to provide is different than the residential address.
value: The actual submitted value for the cleaned field.
is_required: Whether a value for the cleaned field is required or not.
Returns:
Cleaned value for the field.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
if not is_shipping_address_different and value:
raise django_forms.ValidationError(INVALID_SHIPPING_ADDRESS_PART)
elif is_shipping_address_different and not value and is_required:
raise django_forms.ValidationError(MISSING_REQUIRED_FIELD)
else:
return cleaning.cleanValidAddressCharacters(value)
def cleanTermsOfService(is_accepted, terms_of_service):
"""Cleans terms_of_service field.
Args:
is_accepted: A bool determining whether the user has accepted the terms
of service of not.
terms_of_service: Document entity that contains the terms of service that
need to be accepted.
Returns:
Cleaned value of terms_of_service field. Specifically, it is a key
of a document entity that contains the accepted terms of service.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
if not terms_of_service:
return None
elif not is_accepted:
raise django_forms.ValidationError(TERMS_OF_SERVICE_NOT_ACCEPTED)
else:
return ndb.Key.from_old_key(terms_of_service.key())
def cleanBirthDate(birth_date, program, is_student):
"""Cleans birth_date field.
Args:
birth_date: datetime.date that represents the examined birth date.
program: Program entity for which the specified birth date is checked.
is_student: Whether age is checked for a student or not.
Returns:
Cleaned value of birth_date field. Specifically, datetime.date object
that represents the birth date.
Raises:
django_forms.ValidationError if the submitted value is not valid, i.e.
the birth date does not permit to register or participate in the program.
"""
if is_student and not validate.isAgeSufficientForStudent(birth_date, program):
raise django_forms.ValidationError(INSUFFICIENT_AGE)
elif (not is_student and
not validate.isAgeSufficientForOrgMember(birth_date, program)):
raise django_forms.ValidationError(INSUFFICIENT_AGE)
else:
return birth_date
class _UserProfileForm(soc_forms.ModelForm):
"""Form to set profile properties by a user."""
user_id = django_forms.CharField(
required=True, label=USER_ID_LABEL, help_text=USER_ID_HELP_TEXT)
public_name = django_forms.CharField(
required=True, label=PUBLIC_NAME_LABEL, help_text=PUBLIC_NAME_HELP_TEXT)
web_page = django_forms.URLField(
required=False, label=WEB_PAGE_LABEL, help_text=WEB_PAGE_HELP_TEXT)
blog = django_forms.URLField(
required=False, label=BLOG_LABEL, help_text=BLOG_HELP_TEXT)
photo_url = django_forms.URLField(
required=False, label=PHOTO_URL_LABEL, help_text=PHOTO_URL_HELP_TEXT)
first_name = django_forms.CharField(
required=True, label=FIRST_NAME_LABEL, help_text=FIRST_NAME_HELP_TEXT)
last_name = django_forms.CharField(
required=True, label=LAST_NAME_LABEL, help_text=LAST_NAME_HELP_TEXT)
email = django_forms.EmailField(
required=True, label=EMAIL_LABEL, help_text=EMAIL_HELP_TEXT)
phone = django_forms.CharField(
required=True, label=PHONE_LABEL, help_text=PHONE_HELP_TEXT)
residential_street = django_forms.CharField(
required=True, label=RESIDENTIAL_STREET_LABEL)
residential_street_extra = django_forms.CharField(
required=False, help_text=RESIDENTIAL_STREET_HELP_TEXT)
residential_city = django_forms.CharField(
required=True, label=RESIDENTIAL_CITY_LABEL,
help_text=RESIDENTIAL_CITY_HELP_TEXT)
residential_province = django_forms.CharField(
required=False, label=RESIDENTIAL_PROVINCE_LABEL,
help_text=RESIDENTIAL_PROVINCE_HELP_TEXT)
residential_country = django_forms.CharField(
required=True, label=RESIDENTIAL_COUNTRY_LABEL,
help_text=RESIDENTIAL_COUNTRY_HELP_TEXT,
widget=django_forms.Select(
choices=[('', COUNTRY_DEFAULT)] + [
(country, country)
for country in countries.COUNTRIES_AND_TERRITORIES]))
residential_postal_code = django_forms.CharField(
required=True, label=RESIDENTIAL_POSTAL_CODE_LABEL,
help_text=RESIDENTIAL_POSTAL_CODE_HELP_TEXT)
is_shipping_address_different = django_forms.BooleanField(
required=False, label=IS_SHIPPING_ADDRESS_DIFFERENT_LABEL,
help_text=IS_SHIPPING_ADDRESS_DIFFERENT_HELP_TEXT)
shipping_name = django_forms.CharField(
required=False, label=SHIPPING_NAME_LABEL,
help_text=SHIPPING_NAME_HELP_TEXT)
shipping_street = django_forms.CharField(
required=False, label=SHIPPING_STREET_LABEL)
shipping_street_extra = django_forms.CharField(
required=False, help_text=SHIPPING_STREET_HELP_TEXT)
shipping_city = django_forms.CharField(
required=False, label=SHIPPING_CITY_LABEL,
help_text=SHIPPING_CITY_HELP_TEXT)
shipping_province = django_forms.CharField(
required=False, label=SHIPPING_PROVINCE_LABEL,
help_text=SHIPPING_PROVINCE_HELP_TEXT)
shipping_country = django_forms.CharField(
required=False, label=SHIPPING_COUNTRY_LABEL,
help_text=SHIPPING_COUNTRY_HELP_TEXT,
widget=django_forms.Select(
choices=[('', COUNTRY_DEFAULT)] + [
(country, country)
for country in countries.COUNTRIES_AND_TERRITORIES]))
shipping_postal_code = django_forms.CharField(
required=False, label=SHIPPING_POSTAL_CODE_LABEL,
help_text=SHIPPING_POSTAL_CODE_HELP_TEXT)
birth_date = django_forms.DateField(
required=True, label=BIRTH_DATE_LABEL, help_text=BIRTH_DATE_HELP_TEXT)
tee_style = django_forms.CharField(
required=False, label=TEE_STYLE_LABEL, help_text=TEE_STYLE_HELP_TEXT,
widget=django_forms.Select(
choices=TEE_DEFAULT_CHOICE + TEE_STYLE_CHOICES))
tee_size = django_forms.CharField(
required=False, label=TEE_SIZE_LABEL, help_text=TEE_SIZE_HELP_TEXT,
widget=django_forms.Select(choices=TEE_DEFAULT_CHOICE + TEE_SIZE_CHOICES))
gender = django_forms.CharField(
required=True, label=GENDER_LABEL, help_text=GENDER_HELP_TEXT,
widget=django_forms.Select(
choices=GENDER_DEFAULT_CHOICE + GENDER_CHOICES))
program_knowledge = django_forms.CharField(
required=True, label=PROGRAM_KNOWLEDGE_LABEL,
help_text=PROGRAM_KNOWLEDGE_HELP_TEXT,
widget=django_forms.Select(choices=(
PROGRAM_KNOWLEDGE_DEFAULT_CHOICE + PROGRAM_KNOWLEDGE_CHOICES)))
program_knowledge_other = django_forms.CharField(
required=False, widget=django_forms.Textarea())
terms_of_service = django_forms.BooleanField(
required=True, label=TERMS_OF_SERVICE_LABEL)
school_country = django_forms.CharField(
required=True, label=SCHOOL_COUNTRY_LABEL,
help_text=SCHOOL_COUNTRY_HELP_TEXT,
widget=django_forms.Select(
choices=[('', COUNTRY_DEFAULT)] + [
(country, country)
for country in countries.COUNTRIES_AND_TERRITORIES]))
school_name = django_forms.CharField(
required=True, label=SCHOOL_NAME_LABEL, help_text=SCHOOL_NAME_HELP_TEXT)
school_web_page = django_forms.URLField(
required=True, label=SCHOOL_WEB_PAGE_LABEL,
help_text=SCHOOL_WEB_PAGE_HELP_TEXT)
major = django_forms.CharField(
required=True, label=MAJOR_LABEL, help_text=MAJOR_HELP_TEXT)
degree = django_forms.CharField(
required=True, label=DEGREE_LABEL, help_text=DEGREE_HELP_TEXT,
widget=django_forms.Select(choices=DEGREE_CHOICES))
grade = django_forms.IntegerField(
required=True, label=GRADE_LABEL, help_text=GRADE_HELP_TEXT,
min_value=1, max_value=15)
# TODO(daniel): add better, dynamic validation
expected_graduation = django_forms.IntegerField(
required=True, label=EXPECTED_GRADUATION_LABEL,
help_text=EXPECTED_GRADUATION_HELP_TEXT, min_value=2009,
max_value=2025, error_messages={'invalid': 'Enter a year.'})
Meta = object
def __init__(self, bound_field_class, request_data, terms_of_service=None,
has_student_data=None, skip_fields=None, **kwargs):
"""Initializes a new form.
Args:
bound_field_class: Subclass of BoundField class to be used for the from.
request_data: request_data.Request data for the current request.
terms_of_service: Document with Terms of Service that has to be accepted
by the user.
has_student_data: If specified to True, the form will contain fields
related to student data for the profile.
skip_fields: Fields to remove from the form.
"""
super(_UserProfileForm, self).__init__(bound_field_class, **kwargs)
self.request_data = request_data
self.terms_of_service = terms_of_service
self.has_student_data = has_student_data
# group contact information related fields together
self.fields['first_name'].group = _CONTACT_GROUP
self.fields['last_name'].group = _CONTACT_GROUP
self.fields['email'].group = _CONTACT_GROUP
self.fields['phone'].group = _CONTACT_GROUP
# group residential address related fields together
self.fields['residential_street'].group = _RESIDENTIAL_ADDRESS_GROUP
self.fields['residential_street_extra'].group = _RESIDENTIAL_ADDRESS_GROUP
self.fields['residential_city'].group = _RESIDENTIAL_ADDRESS_GROUP
self.fields['residential_province'].group = _RESIDENTIAL_ADDRESS_GROUP
self.fields['residential_country'].group = _RESIDENTIAL_ADDRESS_GROUP
self.fields['residential_postal_code'].group = _RESIDENTIAL_ADDRESS_GROUP
# group residential address related fields together
self.fields['is_shipping_address_different'].group = _SHIPPING_ADDRESS_GROUP
self.fields['shipping_name'].group = _SHIPPING_ADDRESS_GROUP
self.fields['shipping_street'].group = _SHIPPING_ADDRESS_GROUP
self.fields['shipping_street_extra'].group = _SHIPPING_ADDRESS_GROUP
self.fields['shipping_city'].group = _SHIPPING_ADDRESS_GROUP
self.fields['shipping_province'].group = _SHIPPING_ADDRESS_GROUP
self.fields['shipping_country'].group = _SHIPPING_ADDRESS_GROUP
self.fields['shipping_postal_code'].group = _SHIPPING_ADDRESS_GROUP
# group other information related fields together
self.fields['birth_date'].group = _OTHER_INFORMATION_GROUP
self.fields['tee_style'].group = _OTHER_INFORMATION_GROUP
self.fields['tee_size'].group = _OTHER_INFORMATION_GROUP
self.fields['gender'].group = _OTHER_INFORMATION_GROUP
self.fields['program_knowledge'].group = _OTHER_INFORMATION_GROUP
self.fields['program_knowledge_other'].group = _OTHER_INFORMATION_GROUP
# remove terms of service field if no document is defined
if not self.terms_of_service:
del self.fields['terms_of_service']
else:
if (request_data.ndb_profile and
(ndb.Key.from_old_key(self.terms_of_service.key())
in request_data.ndb_profile.accepted_tos)):
self._makeTermsOfServiceReadonlyField()
else:
self.fields['terms_of_service'].widget = soc_forms.TOSWidget(
tos_popout=request_data.redirect.document(
self.terms_of_service).url(),
tos_text=self.terms_of_service.content,
tos_agree_text=TERMS_OF_SERVICE_AGREE_TEXT)
self.fields['terms_of_service'].group = _TERMS_OF_SERVICE_GROUP
if not self.has_student_data:
# remove all fields associated with student data
for field_name in _STUDENT_DATA_PROPERTIES_FORM_FIELDS:
del self.fields[field_name]
else:
# group education related fields together
self.fields['school_country'].group = _EDUCATION_GROUP
self.fields['school_name'].group = _EDUCATION_GROUP
self.fields['school_web_page'].group = _EDUCATION_GROUP
self.fields['major'].group = _EDUCATION_GROUP
self.fields['degree'].group = _EDUCATION_GROUP
self.fields['grade'].group = _EDUCATION_GROUP
self.fields['expected_graduation'].group = _EDUCATION_GROUP
if skip_fields:
for field in skip_fields:
self.fields.pop(field, None)
def _makeTermsOfServiceReadonlyField(self):
"""Inserts a readonly field to display Terms Of Service document
and removes the initial editable field.
"""
# create a new readonly field that displays content of the original
# terms of service document along with a disabled checkbox whose value
# is always set to True
tos_readonly = django_forms.BooleanField(
required=False, label=TERMS_OF_SERVICE_READONLY_LABEL)
tos_readonly.widget = soc_forms.TOSWidget(
tos_popout=self.request_data.redirect.document(
self.terms_of_service).url(),
tos_text=self.terms_of_service.content,
tos_agree_text=TERMS_OF_SERVICE_AGREE_TEXT)
tos_readonly.widget.attrs['disabled'] = 'disabled'
tos_readonly.widget.attrs['checked'] = 'checked'
tos_readonly.group = _TERMS_OF_SERVICE_GROUP
# find the index of terms_of_service field and insert the readonly
# field just above that field.
index = self.fields.keyOrder.index('terms_of_service')
self.fields.insert(index, 'readonly_tos', tos_readonly)
# get rid of the regular field; there is no need to keep it
del self.fields['terms_of_service']
def clean(self):
"""Perform validation that require access to multiple fields
from the form at once.
Returns:
Cleaned data dictionary
"""
# Check if program knowledge is other and
# program_knowledge_other is empty
if (self.cleaned_data.get('program_knowledge')
== _PROGRAM_KNOWLEDGE_OTHER_ID):
if ('program_knowledge_other' not in self.cleaned_data or
self.cleaned_data['program_knowledge_other'] == ''):
self._errors['program_knowledge'] = (
self.error_class(['Answer is required.']))
del self.cleaned_data['program_knowledge']
else:
self.cleaned_data['program_knowledge'] = (
self.cleaned_data['program_knowledge_other'])
return self.cleaned_data
def clean_user_id(self):
"""Cleans user_id field.
Returns:
Cleaned value for user_id field.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
return cleanUserId(self.cleaned_data['user_id'])
def clean_first_name(self):
"""Cleans first_name field.
Returns:
Cleaned value for first_name field.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
return cleaning.cleanValidAddressCharacters(
self.cleaned_data['first_name'])
def clean_last_name(self):
"""Cleans last_name field.
Returns:
Cleaned value for last_name field.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
return cleaning.cleanValidAddressCharacters(
self.cleaned_data['last_name'])
def clean_residential_street(self):
"""Cleans residential_street field.
Returns:
Cleaned value for residential_street field.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
return cleaning.cleanValidAddressCharacters(
self.cleaned_data['residential_street'])
def clean_residential_street_extra(self):
"""Cleans residential_street_extra field.
Returns:
Cleaned value for residential_street_extra field.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
return cleaning.cleanValidAddressCharacters(
self.cleaned_data['residential_street_extra'])
def clean_residential_city(self):
"""Cleans residential_city field.
Returns:
Cleaned value for residential_city field.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
return cleaning.cleanValidAddressCharacters(
self.cleaned_data['residential_city'])
def clean_residential_province(self):
"""Cleans residential_province field.
Returns:
Cleaned value for residential_province field.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
return cleaning.cleanValidAddressCharacters(
self.cleaned_data['residential_province'])
def clean_residential_postal_code(self):
"""Cleans residential_postal_code field.
Returns:
Cleaned value for residential_postal_code field.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
return cleaning.cleanValidAddressCharacters(
self.cleaned_data['residential_postal_code'])
def clean_residential_country(self):
"""Cleans residential_country field.
Returns:
Cleaned value for residential_country field.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
return cleaning.clean_country(
self.cleaned_data['residential_country'])
def clean_shipping_name(self):
"""Cleans shipping_name field.
Returns:
Cleaned value for shipping_name field.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
return _cleanShippingAddressPart(
self.cleaned_data['is_shipping_address_different'],
self.cleaned_data['shipping_name'], True)
def clean_shipping_street(self):
"""Cleans shipping_street field.
Returns:
Cleaned value for shipping_street field.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
return _cleanShippingAddressPart(
self.cleaned_data['is_shipping_address_different'],
self.cleaned_data['shipping_street'], True)
def clean_shipping_street_extra(self):
"""Cleans shipping_street_extra field.
Returns:
Cleaned value for shipping_street_extra field.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
return _cleanShippingAddressPart(
self.cleaned_data['is_shipping_address_different'],
self.cleaned_data['shipping_street_extra'], False)
def clean_shipping_city(self):
"""Cleans shipping_city field.
Returns:
Cleaned value for shipping_city field.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
return _cleanShippingAddressPart(
self.cleaned_data['is_shipping_address_different'],
self.cleaned_data['shipping_city'], True)
def clean_shipping_province(self):
"""Cleans shipping_province field.
Returns:
Cleaned value for shipping_province field.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
return _cleanShippingAddressPart(
self.cleaned_data['is_shipping_address_different'],
self.cleaned_data['shipping_province'], False)
def clean_shipping_country(self):
"""Cleans shipping_country field.
Copies some functionality from _cleanShippingAddressPart.
Returns:
Cleaned value for shipping_country field.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
is_shipping_address_different = (
self.cleaned_data['is_shipping_address_different'])
country = self.cleaned_data['shipping_country']
if not is_shipping_address_different and country:
# Should not specify a shipping address unless the 'is
# different' box is checked.
raise django_forms.ValidationError(INVALID_SHIPPING_ADDRESS_PART)
elif is_shipping_address_different and not country:
# This field is required.
raise django_forms.ValidationError(MISSING_REQUIRED_FIELD)
elif not is_shipping_address_different and not country:
# field not required and is empty
return country
else:
return cleaning.clean_country(country)
def clean_shipping_postal_code(self):
"""Cleans shipping_postal_code field.
Returns:
Cleaned value for shipping_postal_code field.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
return _cleanShippingAddressPart(
self.cleaned_data['is_shipping_address_different'],
self.cleaned_data['shipping_postal_code'], True)
def clean_terms_of_service(self):
"""Cleans terms_of_service_field.
Returns:
Cleaned value of terms_of_service field. Specifically, it is a key
of a document entity that contains the accepted terms of service.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
return cleanTermsOfService(
self.cleaned_data['terms_of_service'], self.terms_of_service)
def clean_birth_date(self):
"""Cleans birth_date field.
Returns:
Cleaned value of birth_date field. Specifically, datetime.date object
that represents the submitted birth date.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
return cleanBirthDate(
self.cleaned_data['birth_date'],
self.request_data.program, self.has_student_data)
def getUserProperties(self):
"""Returns properties of the user that were submitted in this form.
Returns:
A dict mapping user properties to the corresponding values.
"""
return self._getPropertiesForFields(_USER_PROPERTIES_FORM_KEYS)
def getProfileProperties(self):
"""Returns properties of the profile that were submitted in this form.
Returns:
A dict mapping profile properties to the corresponding values.
"""
return self._getPropertiesForFields(_PROFILE_PROPERTIES_FORM_KEYS)
def getContactProperties(self):
"""Returns properties of the contact information that were submitted
in this form.
Returns:
A dict mapping profile properties to the corresponding values.
"""
return self._getPropertiesForFields(_CONTACT_PROPERTIES_FORM_KEYS)
def getResidentialAddressProperties(self):
"""Returns properties of the residential address that were submitted
in this form.
Returns:
A dict mapping residential address properties to the corresponding values.
"""
return self._getPropertiesForFields(
_RESIDENTIAL_ADDRESS_PROPERTIES_FORM_KEYS)
def getShippingAddressProperties(self):
"""Returns properties of the shipping address that were submitted in
this form.
Returns:
A dict mapping shipping address properties to the corresponding values
or None, if the shipping address has not been specified.
"""
return (self._getPropertiesForFields(_SHIPPING_ADDRESS_PROPERTIES_FORM_KEYS)
if self.cleaned_data['is_shipping_address_different'] else None)
def getStudentDataProperties(self):
"""Returns properties of the student data that were submitted in this form.
Returns:
A dict mapping student data properties to the corresponding values
or None, if student data does not apply to this form.
"""
return (self._getPropertiesForFields(_STUDENT_DATA_PROPERTIES_FORM_FIELDS)
if self.has_student_data else None)
_TEE_STYLE_ID_TO_ENUM_LINK = (
(_TEE_STYLE_FEMALE_ID, profile_model.TeeStyle.FEMALE),
(_TEE_STYLE_MALE_ID, profile_model.TeeStyle.MALE)
)
_TEE_STYLE_ID_TO_ENUM_MAP = dict(_TEE_STYLE_ID_TO_ENUM_LINK)
_TEE_STYLE_ENUM_TO_ID_MAP = dict(
(v, k) for (k, v) in _TEE_STYLE_ID_TO_ENUM_LINK)
_TEE_SIZE_ID_TO_ENUM_LINK = (
(_TEE_SIZE_XS_ID, profile_model.TeeSize.XS),
(_TEE_SIZE_S_ID, profile_model.TeeSize.S),
(_TEE_SIZE_M_ID, profile_model.TeeSize.M),
(_TEE_SIZE_L_ID, profile_model.TeeSize.L),
(_TEE_SIZE_XL_ID, profile_model.TeeSize.XL),
(_TEE_SIZE_XXL_ID, profile_model.TeeSize.XXL),
(_TEE_SIZE_XXXL_ID, profile_model.TeeSize.XXXL)
)
_TEE_SIZE_ID_TO_ENUM_MAP = dict(_TEE_SIZE_ID_TO_ENUM_LINK)
_TEE_SIZE_ENUM_TO_ID_MAP = dict((v, k) for (k, v) in _TEE_SIZE_ID_TO_ENUM_LINK)
_GENDER_ID_TO_ENUM_LINK = (
(_GENDER_FEMALE_ID, profile_model.Gender.FEMALE),
(_GENDER_MALE_ID, profile_model.Gender.MALE),
(_GENDER_OTHER_ID, profile_model.Gender.OTHER),
(_GENDER_NOT_DISCLOSED_ID, profile_model.Gender.NOT_DISCLOSED)
)
_GENDER_ID_TO_ENUM_MAP = dict(_GENDER_ID_TO_ENUM_LINK)
_GENDER_ENUM_TO_ID_MAP = dict((v, k) for (k, v) in _GENDER_ID_TO_ENUM_LINK)
_GENDER_ENUM_TO_ID_MAP[None] = _GENDER_NOT_DISCLOSED_ID
_GENDER_ENUM_TO_VERBOSE_MAP = {
profile_model.Gender.FEMALE: _GENDER_FEMALE_VERBOSE,
profile_model.Gender.MALE: _GENDER_MALE_VERBOSE,
profile_model.Gender.OTHER: _GENDER_OTHER_VERBOSE,
profile_model.Gender.NOT_DISCLOSED: _GENDER_NOT_DISCLOSED_VERBOSE,
None: _GENDER_NOT_DISCLOSED_VERBOSE,
}
_PROGRAM_KNOWLEDGE_ENUM_TO_VERBOSE_MAP = dict(PROGRAM_KNOWLEDGE_CHOICES)
_DEGREE_ID_TO_ENUM_LINK = (
(_DEGREE_UNDERGRADUATE_ID, education_model.Degree.UNDERGRADUATE),
(_DEGREE_MASTERS_ID, education_model.Degree.MASTERS),
(_DEGREE_PHD_ID, education_model.Degree.PHD),
(None, None),
)
_DEGREE_ID_TO_ENUM_MAP = dict(_DEGREE_ID_TO_ENUM_LINK)
_DEGREE_ENUM_TO_ID_MAP = dict((v, k) for (k, v) in _DEGREE_ID_TO_ENUM_LINK)
def _adaptProfilePropertiesForDatastore(form_data, profile=None):
"""Adapts properties corresponding to profile's properties, which
have been submitted in a form, to the format that is compliant with
profile_model.Profile model.
Args:
form_data: A dict containing data submitted in a form.
profile: Optional profile_model.Profile entity whose properties are
updated.
Returns:
A dict mapping properties of profile model to values based on
data submitted in a form.
"""
properties = {
profile_model.Profile.public_name._name: form_data.get('public_name'),
profile_model.Profile.first_name._name: form_data.get('first_name'),
profile_model.Profile.last_name._name: form_data.get('last_name'),
profile_model.Profile.photo_url._name: form_data.get('photo_url'),
profile_model.Profile.birth_date._name: form_data.get('birth_date'),
profile_model.Profile.program_knowledge._name:
form_data.get('program_knowledge'),
}
if 'tee_style' in form_data:
properties[profile_model.Profile.tee_style._name] = (
_TEE_STYLE_ID_TO_ENUM_MAP[form_data['tee_style']])
if 'tee_size' in form_data:
properties[profile_model.Profile.tee_size._name] = (
_TEE_SIZE_ID_TO_ENUM_MAP[form_data['tee_size']])
if 'gender' in form_data:
properties[profile_model.Profile.gender._name] = (
_GENDER_ID_TO_ENUM_MAP[form_data['gender']])
properties[profile_model.Profile.accepted_tos._name] = (
_adaptAcceptedTosForDatastore(
form_data, list(profile.accepted_tos) if profile else []))
return properties
def _adaptAcceptedTosForDatastore(form_data, current_tos=None):
"""Adapts accepted_tos data for datastore.
Args:
form_data: A dict containing data submitted in a form.
current_tos: Optional list of ndb.Keys of ToS documents which have been
signed by the profile before the from was submitted.
Returns:
List of ndb.Keys of ToS documents which have been signed by the profile.
"""
current_tos = current_tos or []
if 'terms_of_service' in form_data:
current_tos.append(form_data.get('terms_of_service'))
return list(set(current_tos))
def _adaptStudentDataPropertiesForDatastore(form_data):
"""Adapts properties corresponding to profile's student data properties, which
have been submitted in a form, to the format that is compliant with
profile_model.StudentData model.
Args:
form_data: A dict containing data submitted in a form.
Returns:
A dict mapping properties of student data model to values based on
data submitted in a form.
"""
school_id = form_data.get('school_name')
school_country = form_data.get('school_country')
degree = _DEGREE_ID_TO_ENUM_MAP[form_data.get('degree')]
grade = form_data.get('grade')
expected_graduation = form_data.get('expected_graduation')
major = form_data.get('major')
web_page = form_data.get('school_web_page')
# if both degree and major are present in the form data, we are dealing
# with post secondary education for which grade level is not collected
if degree and major and not grade:
result = education_logic.createPostSecondaryEducation(
school_id, school_country, expected_graduation, major, degree, web_page)
# if grade is present in the form data, we are dealing with secondary
# education. Degree and major are irrelevant in this case
elif not degree and not major:
result = education_logic.createSecondaryEducation(
school_id, school_country, expected_graduation, grade, web_page)
# every other combination is invalid
else:
result = rich_bool.RichBool(False, _INVALID_EDUCATION_DATA)
if not result:
raise exception.BadRequest(message=result.extra)
else:
return {profile_model.StudentData.education._name: result.extra}
def _adaptContactPropertiesForForm(contact_properties):
"""Adapts properties of a contact entity, which are persisted in datastore,
to representation which may be passed to populate _UserProfileForm.
Args:
contact_properties: A dict containing contact properties as persisted
in datastore.
Returns:
A dict mapping properties of contact model to values which can be
populated to a user profile form.
"""
return {
key: contact_properties.get(key) for key in _CONTACT_PROPERTIES_FORM_KEYS
}
def _adaptResidentialAddressPropertiesForForm(address_properties):
"""Adapts properties of a address entity, which are persisted in datastore
as residential address, to representation which may be passed to
populate _UserProfileForm.
Args:
address_properties: A dict containing residential address properties
as persisted in datastore.
Returns:
A dict mapping properties of address model to values which can be
populated to a user profile form.
"""
return {
'residential_street': address_properties['street'],
'residential_street_extra': address_properties['street_extra'],
'residential_city': address_properties['city'],
'residential_country': address_properties['country'],
'residential_postal_code': address_properties['postal_code'],
'residential_province': address_properties['province'],
}
def _adaptShippingAddressPropertiesForForm(address_properties):
"""Adapts properties of an address entity, which are persisted in datastore
as shipping address, to representation which may be passed to
populate _UserProfileForm.
Args:
address_properties: A dict containing shipping address properties
as persisted in datastore or None, if no shipping address is specified.
Returns:
A dict mapping properties of address model to values which can be
populated to a user profile form.
"""
address_properties = address_properties or {}
return {
'is_shipping_address_different': bool(address_properties),
'shipping_name': address_properties.get('name'),
'shipping_street': address_properties.get('street'),
'shipping_street_extra': address_properties.get('street_extra'),
'shipping_city': address_properties.get('city'),
'shipping_country': address_properties.get('country'),
'shipping_postal_code': address_properties.get('postal_code'),
'shipping_province': address_properties.get('province'),
}
def _adaptStudentDataPropertiesForForm(student_data_properties):
"""Adapts properties of a student data entity, which are persisted in
datastore, to representation which may be passed to populate _UserProfileForm.
Args:
student_data_properties: A dict containing student data properties as
persisted in datastore.
Returns:
A dict mapping properties of student profile model to values which can be
populated to a user profile form.
"""
student_data_properties = student_data_properties or {}
education = student_data_properties[profile_model.StudentData.education._name]
return {
'school_country': education.get(
education_model.Education.school_country._name),
'school_name': education.get(education_model.Education.school_id._name),
'school_web_page': education.get(
education_model.Education.web_page._name),
'major': education.get(
education_model.Education.major._name),
'degree': _DEGREE_ENUM_TO_ID_MAP.get(
education.get(education_model.Education.degree._name)),
'grade': education.get(education_model.Education.grade._name),
'expected_graduation': education.get(
education_model.Education.expected_graduation._name),
}
def _adaptProfilePropertiesForForm(profile_properties, terms_of_service):
"""Adapts properties of a profile entity, which are persisted in datastore,
to representation which may be passed to populate _UserProfileForm.
Args:
profile_properties: A dict containing profile properties as
persisted in datastore.
terms_of_service: Document entity that contains the terms of service that
need to be accepted.
Returns:
A dict mapping properties of profile model to values which can be
populated to a user profile form.
"""
form_data = {
key: profile_properties.get(key)
for key in [
'first_name', 'last_name', 'photo_url', 'birth_date',
'public_name']
}
# terms of service information
form_data['terms_of_service'] = (
terms_of_service and
(ndb.Key.from_old_key(terms_of_service.key())
in profile_properties.get('accepted_tos', [])))
# residential address information
form_data.update(
_adaptResidentialAddressPropertiesForForm(
profile_properties[profile_model.Profile.residential_address._name]))
# shipping address information
form_data.update(
_adaptShippingAddressPropertiesForForm(
profile_properties[profile_model.Profile.shipping_address._name]))
# contact information
if profile_model.Profile.contact._name in profile_properties:
form_data.update(_adaptContactPropertiesForForm(
profile_properties[profile_model.Profile.contact._name]))
if profile_properties.get('tee_style') is not None:
form_data['tee_style'] = (
_TEE_STYLE_ENUM_TO_ID_MAP[profile_properties['tee_style']])
if profile_properties.get('tee_size') is not None:
form_data['tee_size'] = (
_TEE_SIZE_ENUM_TO_ID_MAP[profile_properties['tee_size']])
form_data['gender'] = (
_GENDER_ENUM_TO_ID_MAP[profile_properties['gender']])
program_knowledge = profile_properties.get('program_knowledge')
if program_knowledge not in (
[choice_id for choice_id, _ in PROGRAM_KNOWLEDGE_CHOICES]):
form_data['program_knowledge'] = _PROGRAM_KNOWLEDGE_OTHER_ID
form_data['program_knowledge_other'] = program_knowledge
else:
form_data['program_knowledge'] = program_knowledge
# student information
if profile_properties.get(profile_model.Profile.student_data._name):
form_data.update(_adaptStudentDataPropertiesForForm(
profile_properties[profile_model.Profile.student_data._name]))
return form_data
def _getProfileEntityPropertiesFromForm(form, models, profile=None):
"""Extracts properties for a profile entity from the specified form.
Args:
form: Instance of _UserProfileForm.
models: instance of types.Models that represent appropriate models.
profile: Optional profile_model.Profile entity whose properties are
updated.
Returns:
A dict with complete set of properties of profile entity.
"""
profile_properties = _adaptProfilePropertiesForDatastore(
form.getProfileProperties(), profile=profile)
address_properties = form.getResidentialAddressProperties()
result = address_logic.createAddress(
address_properties['residential_street'],
address_properties['residential_city'],
address_properties['residential_country'],
address_properties['residential_postal_code'],
province=address_properties.get('residential_province'),
street_extra=address_properties.get('residential_street_extra')
)
if not result:
raise exception.BadRequest(message=result.extra)
else:
profile_properties['residential_address'] = result.extra
address_properties = form.getShippingAddressProperties()
if address_properties:
result = address_logic.createAddress(
address_properties['shipping_street'],
address_properties['shipping_city'],
address_properties['shipping_country'],
address_properties['shipping_postal_code'],
province=address_properties.get('shipping_province'),
name=address_properties.get('shipping_name'),
street_extra=address_properties.get('shipping_street_extra'))
if not result:
raise exception.BadRequest(message=result.extra)
else:
profile_properties['shipping_address'] = result.extra
else:
profile_properties['shipping_address'] = None
contact_properties = form.getContactProperties()
result = contact_logic.createContact(**contact_properties)
if not result:
raise exception.BadRequest(message=result.extra)
else:
profile_properties['contact'] = result.extra
student_data_properties = form.getStudentDataProperties()
if student_data_properties:
profile_properties['student_data'] = (
melange_db.toDict(profile.student_data, exclude_computed=True)
if profile and profile.is_student
else {})
profile_properties['student_data'].update(
_adaptStudentDataPropertiesForDatastore(student_data_properties))
return profile_properties
class ProfileFormFactory(object):
"""Interface that defines a factory to create a form to register a profile."""
def create(self, request_data, terms_of_service=None,
include_user_fields=None, include_student_fields=None, **kwargs):
"""Creates a new instance of _UserProfileForm to register a new profile
for the specified parameters.
Args:
request_data: Program entity for which a profile form is constructed.
terms_of_service: Optional document entity with Terms of Service that
has to be accepted by the user.
include_user_fields: If set to True, the constructed form will also include
fields to be used to create a new User entity along with a new
Profile entity.
include_student_fields: If set to True, the form will include fields
related to student data for the profile.
Returns:
_UserProfileForm adjusted to create a new profile.
"""
raise NotImplementedError
class ProfileRegisterAsOrgMemberPage(base.RequestHandler):
"""View to create organization member profile.
It will be used by prospective organization members. Users with such profiles
will be eligible to connect with organizations and participate in the program
as administrators or mentors.
"""
access_checker = access.ConjunctionAccessChecker([
access.HAS_NO_PROFILE_ACCESS_CHECKER,
access.ORG_SIGNUP_STARTED_ACCESS_CHECKER,
access.PROGRAM_ACTIVE_ACCESS_CHECKER])
def __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names, template_path, form_factory):
"""Initializes a new instance of the request handler for the specified
parameters.
Args:
initializer: Implementation of initialize.Initializer interface.
linker: Instance of links.Linker class.
renderer: Implementation of render.Renderer interface.
error_handler: Implementation of error.ErrorHandler interface.
url_pattern_constructor:
Implementation of url_patterns.UrlPatternConstructor.
url_names: Instance of url_names.UrlNames.
template_path: The path of the template to be used.
form_factory: Implementation of ProfileFormFactory interface.
"""
super(ProfileRegisterAsOrgMemberPage, self).__init__(
initializer, linker, renderer, error_handler)
self.url_pattern_constructor = url_pattern_constructor
self.url_names = url_names
self.template_path = template_path
self.form_factory = form_factory
def templatePath(self):
"""See base.RequestHandler.templatePath for specification."""
return self.template_path
def djangoURLPatterns(self):
"""See base.RequestHandler.djangoURLPatterns for specification."""
return [
self.url_pattern_constructor.construct(
r'profile/register/org_member/%s$' % url_patterns.PROGRAM,
self, name=self.url_names.PROFILE_REGISTER_AS_ORG_MEMBER)]
def context(self, data, check, mutator):
"""See base.RequestHandler.context for specification."""
form = self.form_factory.create(
data, terms_of_service=data.program.mentor_agreement,
include_user_fields=data.ndb_user is None, data=data.POST)
return {
'page_name': PROFILE_ORG_MEMBER_CREATE_PAGE_NAME,
'forms': [form],
'error': bool(form.errors),
'form_top_msg': top_message.orgMemberRegistrationTopMessage(data),
'form_below_header_msg': profile_form_below_header.Create(
data, agreement=data.program.mentor_agreement),
}
def post(self, data, check, mutator):
"""See base.RequestHandler.post for specification."""
form = self.form_factory.create(
data, terms_of_service=data.program.mentor_agreement,
include_user_fields=data.ndb_user is None, data=data.POST)
# TODO(daniel): eliminate passing self object.
handler = CreateProfileFormHandler(self, form)
return handler.handle(data, check, mutator)
class IsEligibleToRegisterAsStudent(access.AccessChecker):
"""AccessChecker that ensures that the currently logged in user is eligible
to register as a student.
"""
def checkAccess(self, data, check):
"""See AccessChecker.checkAccess for specification."""
access.ensureLoggedIn(data)
if data.ndb_profile:
# make sure that the user does not have a role for any organization
if data.ndb_profile.is_mentor:
raise exception.Forbidden(
message=_INELIGIBLE_TO_REGISTER_AS_STUDENT_HAS_ROLE_ALREADY)
# make sure that the user is not a student already
if data.ndb_profile.is_student:
raise exception.Forbidden(
message=_INELIGIBLE_TO_REGISTER_AS_STUDENT_IS_STUDENT_ALREADY)
IS_ELIGIBLE_TO_REGISTER_AS_STUDENT = IsEligibleToRegisterAsStudent()
class ProfileRegisterAsStudentPage(base.RequestHandler):
"""View to create student profile.
It will be used by prospective students. Users with such profiles will be
eligible to submit proposals to organizations and work on projects
upon acceptance.
"""
def __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names, template_path,
access_checker, form_factory, persist_age_only=None):
"""Initializes a new instance of the request handler for the specified
parameters.
Args:
initializer: Implementation of initialize.Initializer interface.
linker: Instance of links.Linker class.
renderer: Implementation of render.Renderer interface.
error_handler: Implementation of error.ErrorHandler interface.
url_pattern_constructor:
Implementation of url_patterns.UrlPatternConstructor.
url_names: Instance of url_names.UrlNames.
template_path: The path of the template to be used.
form_factory: Implementation of ProfileFormFactory interface.
persist_age_only: If specified and equals True, only age of the student is
going to be persisted instead of the full birth date.
"""
super(ProfileRegisterAsStudentPage, self).__init__(
initializer, linker, renderer, error_handler)
self.url_pattern_constructor = url_pattern_constructor
self.url_names = url_names
self.template_path = template_path
self.access_checker = access_checker
self.form_factory = form_factory
self.persist_age_only = persist_age_only
def templatePath(self):
"""See base.RequestHandler.templatePath for specification."""
return self.template_path
def djangoURLPatterns(self):
"""See base.RequestHandler.djangoURLPatterns for specification."""
return [
self.url_pattern_constructor.construct(
r'profile/register/student/%s$' % url_patterns.PROGRAM,
self, name=self.url_names.PROFILE_REGISTER_AS_STUDENT)]
def context(self, data, check, mutator):
"""See base.RequestHandler.context for specification."""
form = self.form_factory.create(
data, terms_of_service=data.program.student_agreement,
include_user_fields=data.ndb_user is None,
include_student_fields=True, data=data.POST)
return {
'page_name': PROFILE_STUDENT_CREATE_PAGE_NAME,
'forms': [form],
'error': bool(form.errors),
'form_below_header_msg': profile_form_below_header.Create(
data, agreement=data.program.student_agreement),
}
def post(self, data, check, mutator):
"""See base.RequestHandler.post for specification."""
form = self.form_factory.create(
data, terms_of_service=data.program.student_agreement,
include_user_fields=data.ndb_user is None,
include_student_fields=True, data=data.POST)
# TODO(daniel): eliminate passing self object.
handler = CreateProfileFormHandler(
self, form, persist_age_only=self.persist_age_only)
return handler.handle(data, check, mutator)
def jsonContext(self, data, check, mutator):
"""See base.RequestHandler.jsonContext for specification."""
return universities.UNIVERSITIES
class CreateProfileFormHandler(form_handler.FormHandler):
"""Form handler implementation to handle incoming data that is supposed to
create new profiles.
"""
def __init__(self, view, form, persist_age_only=None):
"""Initializes new instance of form handler.
Args:
view: Callback to implementation of base.RequestHandler
that creates this object.
form: Instance of _UserProfileForm whose data is to be handled.
persist_age_only: If specified and equals True, only age of the student is
going to be persisted instead of the full birth date, even if it is
present the dictionary with properties.
"""
super(CreateProfileFormHandler, self).__init__(view)
self.form = form
self.persist_age_only = persist_age_only
def handle(self, data, check, mutator):
"""Creates and persists a new profile based on the data that was sent
in the current request and supplied to the form.
See form_handler.FormHandler.handle for specification.
"""
if not self.form.is_valid():
# TODO(nathaniel): problematic self-use.
return self._view.get(data, check, mutator)
else:
profile_properties = _getProfileEntityPropertiesFromForm(
self.form, data.models)
user = data.ndb_user
if not user:
# try to make sure that no user entity exists for the current account.
# it should be guaranteed by the condition above evaluating to None,
# but there is a slim chance that an entity has been created in
# the meantime.
user = user_logic.getByCurrentAccount()
username = self.form.getUserProperties()['user_id'] if not user else None
createProfileTxn(
data.program.key(), profile_properties, data.program_timeline,
username=username, user=user, persist_age_only=self.persist_age_only,
models=data.models)
url = links.LINKER.program(
data.program, self._view.url_names.PROFILE_SHOW)
return http.HttpResponseRedirect(url + '?validated=true')
class ProfileEditPage(base.RequestHandler):
"""View to edit user profiles."""
access_checker = access.HAS_PROFILE_ACCESS_CHECKER
def __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names, template_path, form_factory):
"""Initializes a new instance of the request handler for the specified
parameters.
Args:
initializer: Implementation of initialize.Initializer interface.
linker: Instance of links.Linker class.
renderer: Implementation of render.Renderer interface.
error_handler: Implementation of error.ErrorHandler interface.
url_pattern_constructor:
Implementation of url_patterns.UrlPatternConstructor.
url_names: Instance of url_names.UrlNames.
template_path: The path of the template to be used.
form_factory: Implementation of ProfileFormFactory interface.
"""
super(ProfileEditPage, self).__init__(
initializer, linker, renderer, error_handler)
self.url_pattern_constructor = url_pattern_constructor
self.url_names = url_names
self.template_path = template_path
self.form_factory = form_factory
def templatePath(self):
"""See base.RequestHandler.templatePath for specification."""
return self.template_path
def djangoURLPatterns(self):
"""See base.RequestHandler.djangoURLPatterns for specification."""
return [
self.url_pattern_constructor.construct(
r'profile/edit/%s$' % url_patterns.PROGRAM,
self, name=self.url_names.PROFILE_EDIT)]
def context(self, data, check, mutator):
"""See base.RequestHandler.context for specification."""
terms_of_service = program_logic.getTermsOfService(
data.program, data.ndb_profile)
form_data = _adaptProfilePropertiesForForm(
data.ndb_profile.to_dict(), terms_of_service)
form = self.form_factory.create(
data, terms_of_service,
include_student_fields=data.ndb_profile.is_student,
data=data.POST or form_data)
# TODO(daniel): Code In specific items should not be created here
# ** Temporarily disable delete_profile_url while we implement the new
# ** deletion strategy. This will prevent the "delete profile" button
# ** from showing up.
# delete_profile_url = (
# links.LINKER.program(data.program, self.url_names.PROFILE_DELETE)
# if data.program.prefix == 'gci_program' else None)
delete_profile_url = None
return {
'page_name': PROFILE_EDIT_PAGE_NAME,
'forms': [form],
'error': bool(form.errors),
'delete_profile_url': delete_profile_url,
'form_below_header_msg': profile_form_below_header.Create(
data, agreement=terms_of_service),
}
def post(self, data, check, mutator):
"""See base.RequestHandler.post for specification."""
terms_of_service = program_logic.getTermsOfService(
data.program, data.ndb_profile)
form = self.form_factory.create(
data, terms_of_service,
include_student_fields=data.ndb_profile.is_student,
data=data.POST)
if not form.is_valid():
# TODO(nathaniel): problematic self-use.
return self.get(data, check, mutator)
else:
profile_properties = _getProfileEntityPropertiesFromForm(
form, data.models, data.ndb_profile)
# if new Terms Of Service document was signed during this request,
# signature must be created
if (terms_of_service and
not data.ndb_profile.isTermsOfServiceSigned(terms_of_service.key())):
accepted_tos = ndb.Key.from_old_key(terms_of_service.key())
else:
accepted_tos = None
editProfileTxn(
data.ndb_profile.key, profile_properties, accepted_tos=accepted_tos)
url = links.LINKER.program(data.program, self.url_names.PROFILE_SHOW)
return http.HttpResponseRedirect(url + '?validated=true')
def jsonContext(self, data, check, mutator):
"""See base.RequestHandler.jsonContext for specification."""
return universities.UNIVERSITIES
def _getStudentFormsContext(data, profile, url_names):
"""Returns context that is applicable to student forms.
Args:
data: request_data.RequestData for the current request.
url_names: Instance of url_names.UrlNames.
Returns:
A dict that contains the context for student forms.
"""
context = {}
# TODO(daniel): these elements below do not really belong here.
# They are only needed to pass the context that is needed by JS module
# that supports StudentFormsTemplate. It should be possible to define
# these items in that sub-template
if data.program.prefix == 'gci_program':
button_type = (
toggle_button.ButtonType.on_off.value
if data.is_host
else toggle_button.ButtonType.disabled.value)
context['verify_consent_form_url'] = links.LINKER.profile(
profile, url_names.STUDENT_FORM_VERIFY,
action='verify', form_type=student_forms_view.CONSENT_FORM_TYPE)
context['unverify_consent_form_url'] = links.LINKER.profile(
profile, url_names.STUDENT_FORM_VERIFY,
action='unverify', form_type=student_forms_view.CONSENT_FORM_TYPE)
context['is_consent_form_verified'] = (
profile.student_data.is_consent_form_verified
if profile.is_student
else None)
context['consent_form_button_type'] = button_type
context['verify_enrollment_form_url'] = links.LINKER.profile(
profile, url_names.STUDENT_FORM_VERIFY,
action='verify', form_type=student_forms_view.ENROLLMENT_FORM_TYPE)
context['unverify_enrollment_form_url'] = links.LINKER.profile(
profile, url_names.STUDENT_FORM_VERIFY,
action='unverify', form_type=student_forms_view.ENROLLMENT_FORM_TYPE)
context['is_enrollment_form_verified'] = (
profile.student_data.is_enrollment_form_verified
if profile.is_student
else None)
context['enrollment_form_button_type'] = button_type
context['freeze_awaiting_forms_url'] = links.LINKER.profile(
profile, url_names.STUDENT_FORM_VERIFY,
action='verify', form_type=student_forms_view.AWAITING_FORMS_TYPE)
context['unfreeze_awaiting_forms_form_url'] = links.LINKER.profile(
profile, url_names.STUDENT_FORM_VERIFY,
action='unverify', form_type=student_forms_view.AWAITING_FORMS_TYPE)
context['is_awaiting_forms'] = (
profile.flags.awaiting_forms if profile.is_student else None)
context['awaiting_forms_button_type'] = button_type
return context
class ProfileReadonlyFactory(object):
"""Interface that defines a factory to create read-only template
for profiles.
"""
def create(self, data, profile):
"""Creates a new instance of readonly.Readonly template for the specified
profile.
Args:
data: request_data.RequestData for the current request.
profile: profile_model.Profile entity.
Returns:
readonly.Readonly for the specified profile.
"""
raise NotImplementedError
class ProfileShowPage(base.RequestHandler):
"""View to display the read-only profile page."""
access_checker = access.HAS_PROFILE_ACCESS_CHECKER
def __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names, template_path,
readonly_factory):
"""Initializes a new instance of the request handler for the specified
parameters.
Args:
initializer: Implementation of initialize.Initializer interface.
linker: Instance of links.Linker class.
renderer: Implementation of render.Renderer interface.
error_handler: Implementation of error.ErrorHandler interface.
url_pattern_constructor:
Implementation of url_patterns.UrlPatternConstructor.
url_names: Instance of url_names.UrlNames.
template_path: The path of the template to be used.
readonly_factory: Implementation of ProfileReadonlyFactory interface.
"""
super(ProfileShowPage, self).__init__(
initializer, linker, renderer, error_handler)
self.url_pattern_constructor = url_pattern_constructor
self.url_names = url_names
self.template_path = template_path
self.readonly_factory = readonly_factory
def djangoURLPatterns(self):
"""See base.RequestHandler.djangoURLPatterns for specification."""
return [
self.url_pattern_constructor.construct(
r'profile/show/%s$' % url_patterns.PROGRAM,
self, name=self.url_names.PROFILE_SHOW)]
def templatePath(self):
"""See base.RequestHandler.templatePath for specification."""
return self.template_path
def context(self, data, check, mutator):
"""See soc.views.base.RequestHandler.context for specification."""
profile_template = self.readonly_factory.create(data, data.ndb_profile)
profile_tabs = (
tabs.profileTabs(data, selected_tab_id=tabs.VIEW_PROFILE_TAB_ID)
if data.program.prefix == 'gsoc_program' else None)
context = {
'page_name': '%s Profile - %s' % (
data.program.short_name, data.ndb_profile.public_name),
'program_name': data.program.name,
'profile_template': profile_template,
'tabs': profile_tabs,
}
context.update(_getStudentFormsContext(
data, data.ndb_profile, self.url_names))
return context
class ProfileAdminPage(base.RequestHandler):
"""View to display the read-only profile page to program administrators."""
access_checker = access.PROGRAM_ADMINISTRATOR_ACCESS_CHECKER
def __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names, template_path,
readonly_factory):
"""Initializes a new instance of the request handler for the specified
parameters.
Args:
initializer: Implementation of initialize.Initializer interface.
linker: Instance of links.Linker class.
renderer: Implementation of render.Renderer interface.
error_handler: Implementation of error.ErrorHandler interface.
url_pattern_constructor:
Implementation of url_patterns.UrlPatternConstructor.
url_names: Instance of url_names.UrlNames.
template_path: The path of the template to be used.
readonly_factory: Implementation of ProfileReadonlyFactory interface.
"""
super(ProfileAdminPage, self).__init__(
initializer, linker, renderer, error_handler)
self.url_pattern_constructor = url_pattern_constructor
self.url_names = url_names
self.template_path = template_path
self.readonly_factory = readonly_factory
def djangoURLPatterns(self):
"""See base.RequestHandler.djangoURLPatterns for specification."""
return [
self.url_pattern_constructor.construct(
r'profile/admin/%s$' % url_patterns.PROFILE,
self, name=self.url_names.PROFILE_ADMIN)]
def templatePath(self):
"""See base.RequestHandler.templatePath for specification."""
return self.template_path
def context(self, data, check, mutator):
"""See soc.views.base.RequestHandler.context for specification."""
profile_template = self.readonly_factory.create(data, data.url_ndb_profile)
context = {
'page_name': '%s Profile - %s' % (
data.program.short_name, data.url_ndb_profile.public_name),
'program_name': data.program.name,
'profile_template': profile_template,
}
context.update(_getStudentFormsContext(
data, data.url_ndb_profile, self.url_names))
return context
class ProfileDeletePage(base.RequestHandler):
"""Page to request profile deletion."""
access_checker = access.HAS_PROFILE_ACCESS_CHECKER
def __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names, template_path):
"""Initializes a new instance of the request handler for the specified
parameters.
Args:
initializer: Implementation of initialize.Initializer interface.
linker: Instance of links.Linker class.
renderer: Implementation of render.Renderer interface.
error_handler: Implementation of error.ErrorHandler interface.
url_pattern_constructor:
Implementation of url_patterns.UrlPatternConstructor.
url_names: Instance of url_names.UrlNames.
template_path: The path of the template to be used.
"""
super(ProfileDeletePage, self).__init__(
initializer, linker, renderer, error_handler)
self.url_pattern_constructor = url_pattern_constructor
self.url_names = url_names
self.template_path = template_path
def djangoURLPatterns(self):
"""See base.RequestHandler.djangoURLPatterns for specification."""
return [
self.url_pattern_constructor.construct(
r'profile/delete/%s$' % url_patterns.PROGRAM,
self, name=self.url_names.PROFILE_DELETE)
]
def templatePath(self):
"""See base.RequestHandler.templatePath for specification."""
return self.template_path
def context(self, data, check, mutator):
"""See base.RequestHandler.context for specification."""
return {'page_name': PROFILE_DELETE_PAGE_NAME}
def post(self, data, check, mutator):
"""See base.RequestHandler.post for specification."""
profile_logic.requestProfileDeletion(data.ndb_profile)
return http.HttpResponseRedirect(
links.LINKER.program(data.program, self.url_names.PROFILE_DELETE)
+ '?validated=True')
class AgeNotSetAccessChecker(access.AccessChecker):
"""AccessChecker that ensures that the currently logged-in user has a profile
for which neither age nor birth_date is not set.
"""
def checkAccess(self, data, check):
"""See AccessChecker.checkAccess for specification."""
access.ensureLoggedIn(data)
if data.ndb_profile.age or data.ndb_profile.birth_date:
raise exception.Forbidden(message=AGE_ALREADY_SET)
AGE_NOT_SET_ACCESS_CHECKER = AgeNotSetAccessChecker()
PROFILE_AGE_CONFIRMATION_ACCESS_CHECKER = access.ConjunctionAccessChecker([
access.STUDENT_PROFILE_ACCESS_CHECKER,
AGE_NOT_SET_ACCESS_CHECKER])
class ProfileAgeConfirmationHandler(base.RequestHandler):
"""Handler to set age for students."""
access_checker = PROFILE_AGE_CONFIRMATION_ACCESS_CHECKER
def __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names):
"""Initializes a new instance of the request handler for the specified
parameters.
Args:
initializer: Implementation of initialize.Initializer interface.
linker: Instance of links.Linker class.
renderer: Implementation of render.Renderer interface.
error_handler: Implementation of error.ErrorHandler interface.
url_pattern_constructor:
Implementation of url_patterns.UrlPatternConstructor.
url_names: Instance of url_names.UrlNames.
"""
super(ProfileAgeConfirmationHandler, self).__init__(
initializer, linker, renderer, error_handler)
self.url_pattern_constructor = url_pattern_constructor
self.url_names = url_names
def djangoURLPatterns(self):
"""See base.RequestHandler.djangoURLPatterns for specification."""
return [
self.url_pattern_constructor.construct(
r'profile/age_confirmation/%s$' % url_patterns.PROGRAM,
self, name=self.url_names.PROFILE_AGE_CONFIRMATION),
]
def post(self, data, check, mutator):
"""See base.RequestHandler.post for specification."""
if 'age' not in data.POST:
raise exception.BadRequest(message='Age not present in POST data.')
age = data.POST['age']
if not age.isdigit():
raise exception.BadRequest(message='Age must be a number: %s' % age)
age = int(age)
if data.program.student_min_age and age < data.program.student_min_age:
raise exception.BadRequest(message='Age is too low: %s' % age)
elif data.program.student_max_age and age > data.program.student_max_age:
raise exception.BadRequest(message='Age is too large: %s' % age)
else:
editProfileTxn(data.ndb_profile.key, {'age': age})
return http.HttpResponse()
IDENTIFIER_LABEL = translation.ugettext('User identifier')
IDENTIFIER_HELP_TEXT = translation.ugettext(
'Username of the user to lookup for.')
PROFILE_NOT_FOUND = translation.ugettext('No profile is found for %s')
def cleanIdentifier(identifier, program_key):
"""Cleans identifier field.
Args:
identifier (string): Username of the profile to lookup for.
program_key (db.Key): Key of a program to which the profile is supposed
to belong.
Returns (melange.models.profile.Profile):
Cleaned value of identifier field. Specifically, a profile entity that
corresponds to the specified identifier and a program.
Raises:
django_forms.ValidationError if the submitted value is not valid, i.e.
the specified identifier is neither a valid username nor email, or if
no profile exists.
Additionally, searching by emails is not supported now. An error is raised
if an email is provided.
"""
# the identifier can be either an email address or a username
if '@' in identifier:
validators.validate_email(identifier)
raise django_forms.ValidationError(
'Searching by emails is not supported at this time. '
'Please provide a username.')
else:
username = cleaning.cleanLinkID(identifier)
profile = profile_logic.getProfileForUsername(username, program_key)
if not profile:
raise django_forms.ValidationError(PROFILE_NOT_FOUND % identifier)
return profile
class LookupProfileForm(soc_forms.ModelForm):
"""Form to lookup profiles."""
SUBMIT_BUTTON_TEXT = translation.ugettext('Search')
identifier = django_forms.CharField(
required=True, label=IDENTIFIER_LABEL, help_text=IDENTIFIER_HELP_TEXT)
def __init__(self, bound_field_class, request_data, **kwargs):
"""Initializes a new form.
Args:
bound_field_class (soc.views.forms.BoundField):
Class to use to bind fields for this form.
request_data (soc.views.helper.request_data.RequestData):
Request data for the current request.
"""
super(LookupProfileForm, self).__init__(bound_field_class, **kwargs)
self.request_data = request_data
def clean_identifier(self):
"""Cleans identifier field.
Returns (melange.models.profile.Profile):
Cleaned value of identifier field. Specifically, it is the profile entity
associated with the provided identifier.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
return cleanIdentifier(
self.cleaned_data['identifier'], self.request_data.program.key())
class LookupProfileFormFactory(object):
"""Interface that defines a factory to create a form to lookup a profile."""
def create(self, request_data, **kwargs):
"""Creates a new instance of LookupProfileForm to lookup a profile
for the specified parameters.
Args:
request_data: (soc.views.helper.request_data.RequestData):
Request data for the current request.
Returns (LookupProfileForm):
A new Instance of LookupProfileForm.
"""
raise NotImplementedError
# TODO(daniel): searching by email should be supported as well.
# It is not easy now, because email belong to Contact model which is
# LocalStructuredProperty on Profile model. Therefore, it cannot be indexed now.
class ProfileLookupPage(base.RequestHandler):
"""Page for program administrators to lookup profiles based on their
usernames.
"""
access_checker = access.PROGRAM_ADMINISTRATOR_ACCESS_CHECKER
def __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names, template_path, form_factory):
"""Initializes a new instance of the request handler for the specified
parameters.
Args:
initializer: Implementation of initialize.Initializer interface.
linker: Instance of links.Linker class.
renderer: Implementation of render.Renderer interface.
error_handler: Implementation of error.ErrorHandler interface.
url_pattern_constructor:
Implementation of url_patterns.UrlPatternConstructor.
url_names: Instance of url_names.UrlNames.
template_path: The path of the template to be used.
form_factory: Implementation of LookupProfileFormFactory interface.
"""
super(ProfileLookupPage, self).__init__(
initializer, linker, renderer, error_handler)
self.url_pattern_constructor = url_pattern_constructor
self.url_names = url_names
self.template_path = template_path
self.form_factory = form_factory
def djangoURLPatterns(self):
"""See base.RequestHandler.djangoURLPatterns for specification."""
return [
self.url_pattern_constructor.construct(
r'profile/lookup/%s$' % url_patterns.PROGRAM,
self, name=self.url_names.PROFILE_LOOKUP),
]
def templatePath(self):
"""See base.RequestHandler.templatePath for specification."""
return self.template_path
def context(self, data, check, mutator):
"""See base.RequestHandler.context for specification."""
form = self.form_factory.create(
data, data=data.POST)
return {
'error': bool(form.errors),
'forms': [form],
'form_submit_button_text': form.SUBMIT_BUTTON_TEXT,
'page_name': PROFILE_LOOKUP_PAGE_NAME
}
def post(self, data, check, mutator):
"""See base.RequestHandler.post for specification."""
form = self.form_factory.create(data, data=data.POST)
if not form.is_valid():
return self.get(data, check, mutator)
else:
profile = form.cleaned_data['identifier']
return http.HttpResponseRedirect(
links.LINKER.profile(profile, self.url_names.PROFILE_ADMIN))
@ndb.transactional
def createProfileTxn(
program_key, profile_properties, timeline, username=None, user=None,
persist_age_only=None, models=types.MELANGE_MODELS):
"""Creates a new user profile based on the specified properties.
Args:
program_key: Program key.
profile_properties: A dict mapping profile properties to their values.
timeline: Timeline for the program for which a new profile is created.
username: Username for a new User entity that will be created along with
the new Profile. May only be passed if user argument is omitted.
user: User entity for the profile. May only be passed if username argument
is omitted.
persist_age_only: If specified and equals True, only age of the student is
going to be persisted instead of the full birth date, even if it is
present the dictionary with properties.
models: instance of types.Models that represent appropriate models.
"""
if username and user:
raise ValueError('Username and user arguments cannot be set together.')
elif not (username or user):
raise ValueError('Exactly one of username or user argument must be set.')
if username:
result = user_logic.createUser(username)
if not result:
raise exception.BadRequest(message=result.extra)
else:
user = result.extra
result = profile_logic.createProfile(
user.key, program_key, profile_properties, timeline,
persist_age_only=persist_age_only, models=models)
if not result:
raise exception.BadRequest(message=result.extra)
else:
# create a signature for the accepted Terms Of Service document
if result.extra.accepted_tos:
signature_logic.createSignature(
result.extra.accepted_tos[0], result.extra)
return result.extra
@ndb.transactional
def editProfileTxn(profile_key, profile_properties, accepted_tos=None):
"""Edits an existing profile based on the specified properties.
Args:
profile_key: Profile key of an existing profile to edit.
profile_properties: A dict mapping profile properties to their values.
accepted_tos: Optional ndb.Key of the Terms Of Service document that was
accepted by the user and for which a signature must be created.
"""
result = profile_logic.editProfile(profile_key, profile_properties)
if not result:
raise exception.BadRequest(message=result.extra)
else:
if accepted_tos:
signature_logic.createSignature(accepted_tos, result.extra)
return result.extra