| # Copyright 2013 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 containing the views for Summer Of Code organization application.""" |
| |
| import json |
| |
| from google.appengine.ext import ndb |
| |
| from django import forms as django_forms |
| from django import http |
| from django.utils import html |
| from django.utils import translation |
| |
| from melange.logic import organization as org_logic |
| from melange.logic import profile as profile_logic |
| from melange.models import organization as org_model |
| from melange.request import access |
| from melange.request import exception |
| from melange.request import links |
| from melange.templates import readonly |
| # TODO(daniel): Was survey_response_list accidentally left out of a commit? |
| from melange.templates import survey_response_list # pylint: disable=no-name-in-module |
| from melange.utils import lists as melange_lists |
| from melange.views import organization as organization_view |
| from melange.views.helper import form_handler |
| |
| from soc.logic import cleaning |
| from soc.mapreduce.helper import control as mapreduce_control |
| from soc.models import licenses |
| from soc.models import program as program_model |
| |
| from soc.views import forms |
| from soc.views import survey as survey_view |
| from soc.views import template |
| from soc.views.helper import url as url_helper |
| from soc.views.helper import url_patterns |
| from soc.views.helper import lists |
| |
| from soc.modules.gsoc.views import base |
| from soc.modules.gsoc.views import forms as gsoc_forms |
| from soc.modules.gsoc.views.helper import url_patterns as soc_url_patterns |
| |
| from summerofcode.request import error |
| from summerofcode.request import render |
| from summerofcode.templates import tabs |
| from summerofcode.views.helper import urls |
| |
| |
| ORG_ID_HELP_TEXT = translation.ugettext( |
| 'Organization ID is used as part of various URL links throughout ' |
| ' the site. You may reuse the same id for different years of the program. ' |
| '<a href="http://en.wikipedia.org/wiki/ASCII">ASCII</a> alphanumeric ' |
| 'characters, digits, and underscores only.') |
| |
| ORG_NAME_HELP_TEXT = translation.ugettext( |
| 'Complete, formal name of the organization.') |
| |
| DESCRIPTION_HELP_TEXT = translation.ugettext( |
| 'Description of the organization to be displayed on a public profile page.') |
| |
| TAGS_HELP_TEXT = translation.ugettext( |
| 'Comma separated list of organization tags. Each tag must be shorter ' |
| 'than %s characters.') |
| |
| IDEAS_PAGE_HELP_TEXT = translation.ugettext( |
| 'The URL to a page with list of ideas for projects for this organization.') |
| |
| IDEAS_PAGE_HELP_TEXT_WITH_FAQ = translation.ugettext( |
| 'The URL to a page with list of ideas for projects for this organization.' |
| 'This is the most important part of your application. You can read about ' |
| 'ideas lists on the <a href="%s">FAQs</a>') |
| |
| LICENSE_HELP_TEXT = translation.ugettext( |
| 'The main license which is used by this organization.') |
| |
| LOGO_URL_HELP_TEXT = translation.ugettext( |
| 'URL to the logo of the organization. Please provide a 64px64px image or ' |
| 'ensure that the image will scale appropriately to this size.') |
| |
| MAILING_LIST_HELP_TEXT = translation.ugettext( |
| 'Mailing list email address, URL to sign-up page, etc.') |
| |
| WEB_PAGE_HELP_TEXT = translation.ugettext( |
| 'Main website of the organization.') |
| |
| IRC_CHANNEL_HELP_TEXT = translation.ugettext( |
| 'Public IRC channel which may be used to get in touch with developers.') |
| |
| FEED_URL_HELP_TEXT = translation.ugettext( |
| 'The URL should be a valid ATOM or RSS feed. Feed entries are shown on ' |
| 'the organization home page in Melange.') |
| |
| GOOGLE_PLUS_HELP_TEXT = translation.ugettext( |
| 'URL to the Google+ page of the organization.') |
| |
| TWITTER_HELP_TEXT = translation.ugettext( |
| 'URL of the Twitter page of the organization.') |
| |
| FACEBOOK_HELP_TEXT = translation.ugettext( |
| 'URL to the Facebook page of the organization.') |
| |
| BLOG_HELP_TEXT = translation.ugettext( |
| 'URL to the blog page of the organization.') |
| |
| IS_VETERAN_HELP_TEXT = translation.ugettext( |
| 'Check this field if the organization has participated in a previous ' |
| 'instance of the program.') |
| |
| BACKUP_ADMIN_HELP_TEXT = translation.ugettext( |
| 'Username of the user who will also serve as administrator for this ' |
| 'organization. Please note that the user must have created ' |
| 'a profile for the current program in order to be eligible. ' |
| 'The organization will be allowed to assign more administrators upon ' |
| 'acceptance into the program.') |
| |
| SLOTS_REQUEST_MIN_HELP_TEXT = translation.ugettext( |
| 'Number of amazing proposals submitted to this organization that ' |
| 'have a mentor assigned and the organization would <strong>really</strong> ' |
| 'like to have a slot for.') |
| |
| SLOTS_REQUEST_MAX_HELP_TEXT = translation.ugettext( |
| 'Number of slots that this organization would like to be assigned if ' |
| 'there was an unlimited amount of slots available.') |
| |
| MAX_SCORE_HELP_TEXT = translation.ugettext( |
| 'Mentors review proposals when student registration is open. This value ' |
| 'specifies the maximum number of points that can be given to a proposal by ' |
| 'one mentor. Please keep in mind changing this value does not have impact ' |
| 'on the existing scores. In particular, scores, which have been higher ' |
| 'than the updated maximum value, are still considered valid.') |
| |
| CONTRIB_TEMPLATE_HELP_TEXT = translation.ugettext( |
| 'This template can be used by contributors, such as students ' |
| 'and other non-member participants, when they apply to contribute ' |
| 'to the organization.') |
| |
| SCORING_ENABLED_HELP_TEXT = translation.ugettext( |
| 'When scoring is not enabled, mentors for the organization are not ' |
| 'allowed to score on proposals.') |
| |
| ACCEPTED_STUDENTS_MESSAGE_HELP_TEXT = translation.ugettext( |
| 'Custom message that is sent to all accepted students for ' |
| 'for this organization.') |
| |
| REJECTED_STUDENTS_MESSAGE_HELP_TEXT = translation.ugettext( |
| 'Custom message that is sent to all rejected students who submitted a ' |
| 'proposal to this organization.') |
| |
| EXTRA_FIELDS_HELP_TEXT = translation.ugettext( |
| 'Additional fields that will be added to proposals for this ' |
| 'organization. These fields are displayed as columns in the list of ' |
| 'proposals and may be edited by organization members. Field names should ' |
| 'appear on lines by themselves. Additionally, each name must be shorter ' |
| 'than %s characters') |
| |
| ORG_ID_LABEL = translation.ugettext('Organization ID') |
| |
| ORG_NAME_LABEL = translation.ugettext('Organization name') |
| |
| DESCRIPTION_LABEL = translation.ugettext('Description') |
| |
| TAGS_LABEL = translation.ugettext('Tags') |
| |
| IDEAS_PAGE_LABEL = translation.ugettext('Ideas list') |
| |
| LICENSE_LABEL = translation.ugettext('Main license') |
| |
| LOGO_URL_LABEL = translation.ugettext('Logo URL') |
| |
| MAILING_LIST_LABEL = translation.ugettext('Mailing list') |
| |
| WEB_PAGE_LABEL = translation.ugettext('Organization website') |
| |
| IRC_CHANNEL_LABEL = translation.ugettext('IRC Channel') |
| |
| FEED_URL_LABEL = translation.ugettext('Feed URL') |
| |
| GOOGLE_PLUS_LABEL = translation.ugettext('Google+ URL') |
| |
| TWITTER_LABEL = translation.ugettext('Twitter URL') |
| |
| BLOG_LABEL = translation.ugettext('Blog page') |
| |
| FACEBOOK_LABEL = translation.ugettext('Facebook URL') |
| |
| IS_VETERAN_LABEL = translation.ugettext('Veteran organization') |
| |
| BACKUP_ADMIN_LABEL = translation.ugettext('Backup administrator') |
| |
| SLOTS_REQUEST_MIN_LABEL = translation.ugettext('Min slots requested') |
| |
| SLOTS_REQUEST_MAX_LABEL = translation.ugettext('Max slots requested') |
| |
| MAX_SCORE_LABEL = translation.ugettext('Max score') |
| |
| CONTRIB_TEMPLATE_LABEL = translation.ugettext('Application template') |
| |
| SCORING_ENABLED_LABEL = translation.ugettext('Scoring enabled') |
| |
| ACCEPTED_STUDENTS_MESSAGE_LABEL = translation.ugettext( |
| 'Message to accepted students') |
| |
| REJECTED_STUDENTS_MESSAGE_LABEL = translation.ugettext( |
| 'Message to rejected students') |
| |
| EXTRA_FIELDS_LABEL = translation.ugettext( |
| 'Proposal extra fields') |
| |
| # TODO(daniel): list of countries should be program-specific |
| ELIGIBLE_COUNTRY_LABEL = translation.ugettext( |
| 'I hereby declare that the applying organization is not located in ' |
| 'any of the countries which are not eligible to participate in the ' |
| 'program: Iran, Syria, Cuba, Sudan, North Korea.') |
| |
| ORG_APPLICATION_SUBMIT_PAGE_NAME = translation.ugettext( |
| 'Submit application') |
| |
| ORG_APPLICATION_SHOW_PAGE_NAME = translation.ugettext( |
| 'Organization application - %s') |
| |
| ORG_SURVEY_RESPONSE_SHOW_PAGE_NAME = translation.ugettext( |
| 'Organization questionnaire - %s') |
| |
| ORG_PREFERENCES_EDIT_PAGE_NAME = translation.ugettext( |
| 'Edit organization preferences') |
| |
| ORG_PROFILE_CREATE_PAGE_NAME = translation.ugettext( |
| 'Create organization profile') |
| |
| ORG_PROFILE_EDIT_PAGE_NAME = translation.ugettext( |
| 'Edit organization profile') |
| |
| NO_ORG_APP = translation.ugettext( |
| 'The organization application for the program %s does not exist.') |
| |
| PROFILE_DOES_NOT_EXIST = translation.ugettext( |
| 'No profile exists for username %s.') |
| |
| OTHER_PROFILE_IS_THE_CURRENT_PROFILE = translation.ugettext( |
| 'The currently logged in profile cannot be specified as ' |
| 'the other organization administrator.') |
| |
| EXTRA_FIELD_TOO_LONG = translation.ugettext( |
| 'Extra field %s is too long: %s. Please make sure that all names are ' |
| 'shorter than %s characters.') |
| |
| TAG_TOO_LONG = translation.ugettext('Tag %s is too long: %s') |
| |
| GENERAL_INFO_GROUP_TITLE = translation.ugettext('General Info') |
| CONTACT_GROUP_TITLE = translation.ugettext('Contact') |
| |
| ORGANIZATION_LIST_DESCRIPTION = 'List of organizations' |
| |
| _CONTACT_PROPERTIES_FORM_KEYS = [ |
| 'blog', 'facebook', 'feed_url', 'google_plus', 'irc_channel', |
| 'mailing_list', 'twitter', 'web_page'] |
| |
| _ORG_PREFERENCES_PROPERTIES_FORM_KEYS = [ |
| 'max_score', 'slot_request_max', 'slot_request_min', 'contrib_template', |
| 'scoring_enabled', 'extra_fields'] |
| |
| _ORG_PROFILE_PROPERTIES_FORM_KEYS = [ |
| 'description', 'ideas_page', 'logo_url', 'name', 'org_id', 'tags', |
| 'license', 'is_veteran'] |
| |
| _ORG_MESSAGES_PROPERTIES_FORM_KEYS = [ |
| 'accepted_students_message', 'rejected_students_message'] |
| |
| EXTRA_FIELD_MAX_LENGTH = 25 |
| TAG_MAX_LENGTH = 30 |
| MAX_SCORE_MIN_VALUE = 1 |
| MAX_SCORE_MAX_VALUE = 12 |
| |
| _LICENSE_CHOICES = ((_license, _license) for _license in licenses.LICENSES) |
| |
| _SET_STATUS_BUTTON_ID = 'save' |
| |
| _APPLY_ORG_ADMISSION_DECISION_ID = 'apply_org_admission_decision_button_id' |
| |
| |
| def cleanOrgId(org_id): |
| """Cleans org_id field. |
| |
| Args: |
| org_id: The submitted organization ID. |
| |
| Returns: |
| Cleaned value for org_id field. |
| |
| Raises: |
| django_forms.ValidationError if the submitted value is not valid. |
| """ |
| if not org_id: |
| raise django_forms.ValidationError('This field is required.') |
| |
| cleaning.cleanLinkID(org_id) |
| |
| return org_id |
| |
| |
| def cleanBackupAdmin(username, request_data): |
| """Cleans backup_admin field. |
| |
| Args: |
| username: Username of the user to assign as the backup administrator. |
| request_data: request_data.RequestData for the current request. |
| |
| Raises: |
| django_forms.ValidationError if no profile exists for at least one |
| of the submitted usernames. |
| """ |
| username = username.strip() |
| profile = profile_logic.getProfileForUsername( |
| username, request_data.program.key(), models=request_data.models) |
| if not profile: |
| raise django_forms.ValidationError(PROFILE_DOES_NOT_EXIST % username) |
| elif profile.key == request_data.ndb_profile.key: |
| raise django_forms.ValidationError(OTHER_PROFILE_IS_THE_CURRENT_PROFILE) |
| else: |
| return profile |
| |
| |
| def cleanTags(tags): |
| """Cleans tags field. |
| |
| Args: |
| tags: The submitted value, which is a comma separated string with tags. |
| |
| Returns: |
| A list of submitted tags. |
| |
| Raises: |
| django_forms.ValidationError if at least one of the tags is not valid. |
| """ |
| tag_list = [] |
| for tag in [tag for tag in tags.split(',') if tag]: |
| if len(tag) > TAG_MAX_LENGTH: |
| raise django_forms.ValidationError(TAG_TOO_LONG % (tag, len(tag))) |
| tag_list.append(tag.strip()) |
| return tag_list |
| |
| |
| def cleanExtraFields(text): |
| """Cleans extra_fields field. |
| |
| Args: |
| text: The submitted value which is an arbitrary string. |
| |
| Returns: |
| A list of extra fields. |
| |
| Raises: |
| django_forms.ValidationError if at least one of the fields is not valid. |
| """ |
| extra_fields = [] |
| for field in [field for field in text.split('\n') if field]: |
| field = field.strip() |
| if len(field) > EXTRA_FIELD_MAX_LENGTH: |
| raise django_forms.ValidationError( |
| EXTRA_FIELD_TOO_LONG % (field, len(field), EXTRA_FIELD_MAX_LENGTH)) |
| else: |
| extra_fields.append(field) |
| return extra_fields |
| |
| |
| class _OrgProfileForm(gsoc_forms.GSoCModelForm): |
| """Form to set properties of organization profile by organization |
| administrators. |
| """ |
| |
| org_id = django_forms.CharField( |
| required=True, label=ORG_ID_LABEL, help_text=ORG_ID_HELP_TEXT) |
| |
| name = django_forms.CharField( |
| required=True, label=ORG_NAME_LABEL, help_text=ORG_NAME_HELP_TEXT) |
| |
| # TODO(daniel): make sure this field is escaped properly |
| description = django_forms.CharField( |
| widget=django_forms.Textarea, required=True, label=DESCRIPTION_LABEL, |
| help_text=DESCRIPTION_HELP_TEXT) |
| |
| tags = django_forms.CharField( |
| required=False, label=TAGS_LABEL, |
| help_text=TAGS_HELP_TEXT % TAG_MAX_LENGTH) |
| |
| license = django_forms.CharField( |
| required=True, label=LICENSE_LABEL, help_text=LICENSE_HELP_TEXT, |
| widget=django_forms.Select(choices=_LICENSE_CHOICES)) |
| |
| logo_url = django_forms.URLField( |
| required=False, label=LOGO_URL_LABEL, help_text=LOGO_URL_HELP_TEXT) |
| |
| ideas_page = django_forms.URLField( |
| required=True, label=IDEAS_PAGE_LABEL) |
| |
| mailing_list = django_forms.CharField( |
| required=False, label=MAILING_LIST_LABEL, |
| help_text=MAILING_LIST_HELP_TEXT) |
| |
| web_page = django_forms.URLField( |
| required=True, label=WEB_PAGE_LABEL, help_text=WEB_PAGE_HELP_TEXT) |
| |
| irc_channel = django_forms.CharField( |
| required=False, label=IRC_CHANNEL_LABEL, help_text=IRC_CHANNEL_HELP_TEXT) |
| |
| feed_url = django_forms.URLField( |
| required=False, label=FEED_URL_LABEL, help_text=FEED_URL_HELP_TEXT) |
| |
| google_plus = django_forms.URLField( |
| required=False, label=GOOGLE_PLUS_LABEL, help_text=GOOGLE_PLUS_HELP_TEXT) |
| |
| twitter = django_forms.URLField( |
| required=False, label=TWITTER_LABEL, help_text=TWITTER_HELP_TEXT) |
| |
| blog = django_forms.URLField( |
| required=False, label=BLOG_LABEL, help_text=BLOG_HELP_TEXT) |
| |
| facebook = django_forms.URLField( |
| required=False, label=FACEBOOK_LABEL, help_text=FACEBOOK_HELP_TEXT) |
| |
| is_veteran = django_forms.BooleanField( |
| required=False, label=IS_VETERAN_LABEL, help_text=IS_VETERAN_HELP_TEXT) |
| |
| backup_admin = django_forms.CharField( |
| required=True, label=BACKUP_ADMIN_LABEL, |
| help_text=BACKUP_ADMIN_HELP_TEXT) |
| |
| eligible_country = django_forms.BooleanField( |
| required=True, label=ELIGIBLE_COUNTRY_LABEL) |
| |
| Meta = object |
| |
| def __init__(self, request_data=None, **kwargs): |
| """Initializes a new form. |
| |
| Args: |
| request_data: request_data.RequestData for the current request. |
| """ |
| super(_OrgProfileForm, self).__init__(**kwargs) |
| self.request_data = request_data |
| |
| # set help text for ideas page. |
| help_page_key = program_model.Program.help_page.get_value_for_datastore( |
| self.request_data.program) |
| if help_page_key: |
| self.fields['ideas_page'].help_text = ( |
| IDEAS_PAGE_HELP_TEXT_WITH_FAQ % |
| self.request_data.redirect.document(help_page_key).url()) |
| else: |
| self.fields['ideas_page'].help_text = IDEAS_PAGE_HELP_TEXT |
| |
| def clean_org_id(self): |
| """Cleans org_id field. |
| |
| Returns: |
| Cleaned value for org_id field. |
| |
| Raises: |
| django_forms.ValidationError if the submitted value is not valid. |
| """ |
| return cleanOrgId(self.cleaned_data['org_id']) |
| |
| def clean_backup_admin(self): |
| """Cleans backup_admin field. |
| |
| Returns: |
| Profile entity corresponding to the backup administrator. |
| |
| Raises: |
| django_forms.ValidationError if the submitted value is not valid. |
| """ |
| return cleanBackupAdmin( |
| self.cleaned_data['backup_admin'], self.request_data) |
| |
| def clean_tags(self): |
| """Cleans tags field. |
| |
| Returns: |
| A list of submitted tags. |
| |
| Raises: |
| django_forms.ValidationError if at least one of the tags is not valid. |
| """ |
| return cleanTags(self.cleaned_data['tags']) |
| |
| def getContactProperties(self): |
| """Returns properties of the contact information that were submitted in |
| the form. |
| |
| Returns: |
| A dict mapping contact properties to the corresponding values. |
| """ |
| return self._getPropertiesForFields(_CONTACT_PROPERTIES_FORM_KEYS) |
| |
| def getOrgProperties(self): |
| """Returns properties of the organization that were submitted in this form. |
| |
| Returns: |
| A dict mapping organization properties to the corresponding values. |
| """ |
| return self._getPropertiesForFields(_ORG_PROFILE_PROPERTIES_FORM_KEYS) |
| |
| |
| def _formToCreateOrgProfile(**kwargs): |
| """Returns a Django form to create a new organization profile. |
| |
| Returns: |
| _OrgProfileForm adjusted to create a new organization profile. |
| """ |
| return _OrgProfileForm(**kwargs) |
| |
| |
| def _formToEditOrgProfile(**kwargs): |
| """Returns a Django form to edit an existing organization profile. |
| |
| Returns: |
| _OrgProfileForm adjusted to edit an existing organization profile. |
| """ |
| form = _OrgProfileForm(**kwargs) |
| |
| # organization ID property is not editable |
| del form.fields['org_id'] |
| |
| # other organization admins are set only when organization profile is created |
| del form.fields['backup_admin'] |
| |
| # the declaration is submitted only when organization profile is created |
| del form.fields['eligible_country'] |
| |
| return form |
| |
| |
| class _OrgPreferencesForm(gsoc_forms.GSoCModelForm): |
| """Form to set preferences of organization by organization administrators.""" |
| |
| slot_request_min = django_forms.IntegerField( |
| label=SLOTS_REQUEST_MIN_LABEL, help_text=SLOTS_REQUEST_MIN_HELP_TEXT, |
| required=True) |
| |
| slot_request_max = django_forms.IntegerField( |
| label=SLOTS_REQUEST_MAX_LABEL, help_text=SLOTS_REQUEST_MAX_HELP_TEXT, |
| required=True) |
| |
| max_score = django_forms.IntegerField( |
| label=MAX_SCORE_LABEL, help_text=MAX_SCORE_HELP_TEXT, |
| min_value=MAX_SCORE_MIN_VALUE, max_value=MAX_SCORE_MAX_VALUE) |
| |
| contrib_template = django_forms.CharField( |
| widget=django_forms.Textarea, label=CONTRIB_TEMPLATE_LABEL, |
| help_text=CONTRIB_TEMPLATE_HELP_TEXT, required=False) |
| |
| scoring_enabled = django_forms.BooleanField( |
| label=SCORING_ENABLED_LABEL, help_text=SCORING_ENABLED_HELP_TEXT, |
| required=False) |
| |
| accepted_students_message = django_forms.CharField( |
| widget=django_forms.Textarea, label=ACCEPTED_STUDENTS_MESSAGE_LABEL, |
| help_text=ACCEPTED_STUDENTS_MESSAGE_HELP_TEXT, required=False) |
| |
| rejected_students_message = django_forms.CharField( |
| widget=django_forms.Textarea, label=REJECTED_STUDENTS_MESSAGE_LABEL, |
| help_text=REJECTED_STUDENTS_MESSAGE_HELP_TEXT, required=False) |
| |
| extra_fields = django_forms.CharField( |
| widget=django_forms.Textarea, label=EXTRA_FIELDS_LABEL, |
| help_text=EXTRA_FIELDS_HELP_TEXT % EXTRA_FIELD_MAX_LENGTH, required=False) |
| |
| Meta = object |
| |
| def clean_extra_fields(self): |
| """Cleans extra_fields field. |
| |
| Returns: |
| A list of submitted extra fields. |
| |
| Raises: |
| django_forms.ValidationError if at least one of the fields is not valid. |
| """ |
| return cleanExtraFields(self.cleaned_data['extra_fields']) |
| |
| def getOrgProperties(self): |
| """Returns properties of the organization that were submitted in this form. |
| |
| Returns: |
| A dict mapping organization preferences properties to |
| the corresponding values. |
| """ |
| return self._getPropertiesForFields(_ORG_PREFERENCES_PROPERTIES_FORM_KEYS) |
| |
| def getOrgMessagesProperties(self): |
| """Returns properties of the organization messages that were submitted |
| in this form. |
| |
| Returns: |
| A dict mapping organization messages properties to |
| the corresponding values. |
| """ |
| return self._getPropertiesForFields(_ORG_MESSAGES_PROPERTIES_FORM_KEYS) |
| |
| |
| class SOCCreateOrgProfileFormFactory(organization_view.OrgProfileFormFactory): |
| """Implementation of organization_view.OrgProfileFormFactory to be used |
| to create forms to create organization profiles for Summer Of Code programs. |
| """ |
| |
| def create(self, request_data, **kwargs): |
| """See organization_view.OrgProfileFormFactory.create for specification.""" |
| return organization_view._OrgProfileForm( |
| gsoc_forms.GSoCBoundField, request_data, |
| template_path=gsoc_forms.TEMPLATE_PATH, |
| **kwargs) |
| |
| SOC_CREATE_ORG_PROFILE_FORM_FACTORY = SOCCreateOrgProfileFormFactory() |
| |
| |
| class SOCEditOrgProfileFormFactory(organization_view.OrgProfileFormFactory): |
| """Implementation of organization_view.OrgProfileFormFactory to be used |
| to create forms to edit organization profiles for Summer Of Code programs. |
| """ |
| |
| def create(self, request_data, **kwargs): |
| """See organization_view.OrgProfileFormFactory.create for specification.""" |
| form = organization_view._OrgProfileForm( |
| gsoc_forms.GSoCBoundField, request_data, |
| template_path=gsoc_forms.TEMPLATE_PATH, |
| **kwargs) |
| |
| for field_id in organization_view.ONLY_CREATE_ORG_PROFILE_FIELD_IDS: |
| del form.fields[field_id] |
| |
| return form |
| |
| SOC_EDIT_ORG_PROFILE_FORM_FACTORY = SOCEditOrgProfileFormFactory() |
| |
| |
| class SOCOrgApplicationFormFactory(survey_view.SurveyTakeFormFactory): |
| """Implementation of survey_view.SurveyTakeFormFactory to be used to create |
| forms for organization applications for Summer Of Code programs. |
| """ |
| |
| def create(self, survey, **kwargs): |
| """See survey_view.SurveyTakeFormFactory.create for specification.""" |
| return forms.SurveyTakeForm( |
| gsoc_forms.GSoCBoundField, survey=survey, |
| template_path=gsoc_forms.TEMPLATE_PATH, |
| **kwargs) |
| |
| SOC_ORG_APPLICATION_FORM_FACTORY = SOCOrgApplicationFormFactory() |
| |
| |
| class SOCOrgApplicationReadonlyFactory( |
| organization_view.OrgApplicationReadonlyFactory): |
| |
| def create(self, data, app_response): |
| """See organization_view.OrgApplicationReadonlyFactory.create for |
| specification. |
| """ |
| builder = readonly.ReadonlyBuilder() |
| builder.setTemplatePath('summerofcode/_readonly_template.html') |
| builder.setGroupTemplatePath('summerofcode/readonly/_group.html') |
| |
| builder.addGroupDescriptor(organization_view.GENERAL_INFO_GROUP_DESCRIPTOR) |
| builder.addGroupDescriptor(organization_view.CONTACT_GROUP_DESCRIPTOR) |
| |
| builder.setValues( |
| organization_view.getOrgProfilePropertiesForForm(data.url_ndb_org)) |
| |
| # descriptor for survey response |
| if app_response: |
| builder.addGroupDescriptor( |
| readonly.SurveyGroupDescriptor( |
| organization_view.APP_RESPONSE_GROUP_TITLE, data.org_app)) |
| builder.setValues( |
| survey_view.surveyResponseAsDict(data.org_app, app_response)) |
| |
| return builder.build(data) |
| |
| SOC_ORG_APPLICATION_READONLY_FACTORY = SOCOrgApplicationReadonlyFactory() |
| |
| |
| class SOCAppResponseReadonlyFactory( |
| organization_view.AppResponseReadonlyFactory): |
| """Implementation of organization_view.AppResponseReadonlyFactory to be used |
| for organization application responses for Summer Of Code programs. |
| """ |
| |
| def create(self, data, app_response): |
| """See organization_view.AppResponseReadonlyFactory.create |
| for specification. |
| """ |
| builder = readonly.ReadonlyBuilder() |
| builder.setTemplatePath('summerofcode/_readonly_template.html') |
| builder.setGroupTemplatePath('summerofcode/readonly/_group.html') |
| |
| builder.addGroupDescriptor( |
| readonly.SurveyGroupDescriptor( |
| organization_view.APP_RESPONSE_GROUP_TITLE, data.org_app)) |
| builder.setValues( |
| survey_view.surveyResponseAsDict(data.org_app, app_response)) |
| |
| return builder.build(data) |
| |
| SOC_APP_RESPONSE_READONLY_FACTORY = SOCAppResponseReadonlyFactory() |
| |
| |
| ORG_PROFILE_CREATE_PAGE = organization_view.OrgProfileCreatePage( |
| base._GSOC_INITIALIZER, links.LINKER, render.SOC_RENDERER, |
| error.SOC_ERROR_HANDLER, soc_url_patterns.SOC_URL_PATTERN_CONSTRUCTOR, |
| urls.UrlNames, 'summerofcode/organization/org_profile_edit.html', |
| SOC_CREATE_ORG_PROFILE_FORM_FACTORY) |
| |
| |
| ORG_PROFILE_EDIT_PAGE = organization_view.OrgProfileEditPage( |
| base._GSOC_INITIALIZER, links.LINKER, render.SOC_RENDERER, |
| error.SOC_ERROR_HANDLER, soc_url_patterns.SOC_URL_PATTERN_CONSTRUCTOR, |
| urls.UrlNames, 'summerofcode/organization/org_profile_edit.html', |
| SOC_EDIT_ORG_PROFILE_FORM_FACTORY) |
| |
| |
| ORG_APPLICATION_SUBMIT_PAGE = organization_view.OrgApplicationSubmitPage( |
| base._GSOC_INITIALIZER, links.LINKER, render.SOC_RENDERER, |
| error.SOC_ERROR_HANDLER, soc_url_patterns.SOC_URL_PATTERN_CONSTRUCTOR, |
| urls.UrlNames, 'modules/gsoc/org_app/take.html', |
| SOC_ORG_APPLICATION_FORM_FACTORY) |
| |
| |
| ORG_APPLICATION_SHOW_PAGE = organization_view.OrgApplicationShowPage( |
| base._GSOC_INITIALIZER, links.LINKER, render.SOC_RENDERER, |
| error.SOC_ERROR_HANDLER, soc_url_patterns.SOC_URL_PATTERN_CONSTRUCTOR, |
| urls.UrlNames, 'modules/gsoc/org_app/show.html', |
| SOC_ORG_APPLICATION_READONLY_FACTORY) |
| |
| |
| ORG_APPLICATION_RESPONSE_SHOW_PAGE = ( |
| organization_view.OrgApplicationResponseShowPage( |
| base._GSOC_INITIALIZER, links.LINKER, render.SOC_RENDERER, |
| error.SOC_ERROR_HANDLER, soc_url_patterns.SOC_URL_PATTERN_CONSTRUCTOR, |
| urls.UrlNames, 'modules/gsoc/org_app/show.html', |
| SOC_APP_RESPONSE_READONLY_FACTORY)) |
| |
| |
| ORG_APPLICATION_RESPONSE_SHOW_PAGE = ( |
| organization_view.OrgApplicationResponseShowPage( |
| base._GSOC_INITIALIZER, links.LINKER, render.SOC_RENDERER, |
| error.SOC_ERROR_HANDLER, soc_url_patterns.SOC_URL_PATTERN_CONSTRUCTOR, |
| urls.UrlNames, 'modules/gsoc/org_app/show.html', |
| SOC_APP_RESPONSE_READONLY_FACTORY)) |
| |
| |
| def _adaptOrgPreferencesPropertiesForForm(properties): |
| """Adapts properties of an organization and organization messages entities, |
| which are persisted in datastore, to representation which may be passed |
| to populate _OrgPreferencesForm. |
| |
| Args: |
| properties: A dict containing contact properties as persisted |
| in datastore. |
| |
| Returns: |
| A dict mapping properties of organization models to values which can be |
| populated to an organization preferences form. |
| """ |
| if org_model.Organization.extra_fields._name in properties: |
| properties[org_model.Organization.extra_fields._name] = '\n'.join( |
| field for field |
| in properties[org_model.Organization.extra_fields._name]) |
| return properties |
| |
| |
| ORG_PREFERENCES_EDIT_PAGE_ACCESS_CHECKER = access.ConjuctionAccessChecker([ |
| access.IS_USER_ORG_ADMIN_FOR_NDB_ORG, |
| access.UrlOrgStatusAccessChecker([org_model.Status.ACCEPTED])]) |
| |
| class OrgPreferencesEditPage(base.GSoCRequestHandler): |
| """View to edit organization preferences.""" |
| |
| access_checker = ORG_PREFERENCES_EDIT_PAGE_ACCESS_CHECKER |
| |
| def templatePath(self): |
| """See base.RequestHandler.templatePath for specification.""" |
| return 'modules/gsoc/org_profile/base.html' |
| |
| def djangoURLPatterns(self): |
| """See base.RequestHandler.djangoURLPatterns for specification.""" |
| return [ |
| soc_url_patterns.url( |
| r'org/preferences/edit/%s$' % url_patterns.ORG, |
| self, name=urls.UrlNames.ORG_PREFERENCES_EDIT)] |
| |
| def context(self, data, check, mutator): |
| """See base.RequestHandler.context for specification.""" |
| properties = data.url_ndb_org.to_dict() |
| properties.update( |
| org_logic.getOrganizationMessages(data.url_ndb_org.key).to_dict()) |
| form_data = _adaptOrgPreferencesPropertiesForForm(properties) |
| |
| form = _OrgPreferencesForm(data=data.POST or form_data) |
| return { |
| 'error': bool(form.errors), |
| 'forms': [form], |
| 'page_name': ORG_PREFERENCES_EDIT_PAGE_NAME, |
| 'tabs': tabs.orgTabs(data, selected_tab_id=tabs.ORG_PREFERENCES_TAB_ID) |
| } |
| |
| def post(self, data, check, mutator): |
| """See base.RequestHandler.post for specification.""" |
| form = _OrgPreferencesForm(data=data.POST) |
| if not form.is_valid(): |
| # TODO(nathaniel): problematic self-use. |
| return self.get(data, check, mutator) |
| else: |
| org_properties = form.getOrgProperties() |
| org_messages_properties = form.getOrgMessagesProperties() |
| updateOrganizationTxn( |
| data.url_ndb_org.key, org_properties, |
| org_messages_properties=org_messages_properties) |
| |
| url = links.LINKER.organization( |
| data.url_ndb_org.key, urls.UrlNames.ORG_PREFERENCES_EDIT) |
| return http.HttpResponseRedirect(url) |
| |
| |
| # TODO(daniel): replace this class with new style list |
| class PublicOrganizationList(template.Template): |
| """Public list of organizations participating in a specified program.""" |
| |
| def __init__(self, data): |
| """See template.Template.__init__ for specification.""" |
| super(PublicOrganizationList, self).__init__(data) |
| self._list_config = lists.ListConfiguration() |
| self._list_config.addPlainTextColumn( |
| 'name', 'Name', lambda e, *args: e.name.strip()) |
| self._list_config.addPlainTextColumn( |
| 'tags', 'Tags', lambda e, *args: ', '.join(e.tags)) |
| self._list_config.addPlainTextColumn('ideas', 'Ideas', |
| lambda e, *args: url_helper.urlize(e.ideas, name='[ideas page]'), |
| hidden=True) |
| self._list_config.setDefaultSort('name', 'asc') |
| |
| def templatePath(self): |
| """See template.Template.templatePath for specification.""" |
| return 'modules/gsoc/admin/_accepted_orgs_list.html' |
| |
| def context(self): |
| """See template.Template.context for specification.""" |
| description = ORGANIZATION_LIST_DESCRIPTION |
| |
| list_configuration_response = lists.ListConfigurationResponse( |
| self.data, self._list_config, 0, description) |
| |
| return { |
| 'lists': [list_configuration_response], |
| } |
| |
| _STATUS_APPLYING_ID = translation.ugettext('Needs review') |
| _STATUS_PRE_ACCEPTED_ID = translation.ugettext('Pre-accepted') |
| _STATUS_PRE_REJECTED_ID = translation.ugettext('Pre-rejected') |
| _STATUS_ACCEPTED_ID = translation.ugettext('Accepted') |
| _STATUS_REJECTED_ID = translation.ugettext('Rejected') |
| |
| _STATUS_ID_TO_ENUM_LINK = ( |
| (_STATUS_APPLYING_ID, org_model.Status.APPLYING), |
| (_STATUS_PRE_ACCEPTED_ID, org_model.Status.PRE_ACCEPTED), |
| (_STATUS_PRE_REJECTED_ID, org_model.Status.PRE_REJECTED), |
| (_STATUS_ACCEPTED_ID, org_model.Status.ACCEPTED), |
| (_STATUS_REJECTED_ID, org_model.Status.REJECTED), |
| ) |
| _STATUS_ID_TO_ENUM_MAP = dict(_STATUS_ID_TO_ENUM_LINK) |
| _STATUS_ENUM_TO_ID_MAP = dict( |
| (v, k) for (k, v) in _STATUS_ID_TO_ENUM_LINK) |
| |
| class OrgApplicationList(template.Template): |
| """List of organization applications that have been submitted for the program. |
| """ |
| |
| def __init__(self, data, survey, idx=0, description=None): |
| """Creates a new OrgApplicationList template. |
| |
| Args: |
| data: request_data.RequestData object for the current request. |
| survey: Survey entity to show the responses for |
| idx: The index of the list to use. |
| description: The (optional) description of the list. |
| """ |
| super(OrgApplicationList, self).__init__(data) |
| |
| self.idx = idx |
| self.description = description or '' |
| self.list_config = lists.ListConfiguration() |
| |
| self.list_config.addPlainTextColumn( |
| 'key', 'Key', lambda entity, *args: entity.key.parent().id(), |
| hidden=True) |
| self.list_config.addSimpleColumn( |
| 'created_on', 'Created On', column_type=lists.DATE) |
| self.list_config.addSimpleColumn( |
| 'modified_on', 'Last Modified On', column_type=lists.DATE) |
| self.list_config.addPlainTextColumn( |
| 'name', 'Name', lambda entity, *args: entity.key.parent().get().name) |
| self.list_config.addPlainTextColumn( |
| 'org_id', 'Organization ID', |
| lambda entity, *args: entity.key.parent().get().org_id) |
| self.list_config.addPlainTextColumn( |
| 'new_or_veteran', 'New/Veteran', |
| lambda entity, *args: |
| 'Veteran' if entity.key.parent().get().is_veteran else 'New') |
| self.list_config.addPlainTextColumn( |
| 'description', 'Description', |
| lambda entity, *args: entity.key.parent().get().description) |
| self.list_config.addPlainTextColumn( |
| 'license', 'License', |
| lambda entity, *args: entity.key.parent().get().license) |
| self.list_config.addPlainTextColumn( |
| 'ideas_page', 'Ideas Page', |
| lambda entity, *args: entity.key.parent().get().ideas_page) |
| |
| survey_response_list.addColumnsForSurvey(self.list_config, survey) |
| |
| # TODO(ljvderijk): Poke Mario during all-hands to see if we can separate |
| # "search options" and in-line selection options. |
| options = [ |
| ('', 'All'), |
| ('(%s)' % _STATUS_APPLYING_ID, _STATUS_APPLYING_ID), |
| ('(%s)' % _STATUS_PRE_ACCEPTED_ID, _STATUS_PRE_ACCEPTED_ID), |
| ('(%s)' % _STATUS_PRE_REJECTED_ID, _STATUS_PRE_REJECTED_ID), |
| # TODO(daniel): figure out how ignored state is used. |
| # ('(ignored)', 'ignored'), |
| ] |
| |
| self.list_config.addPlainTextColumn( |
| 'status', 'Status', |
| lambda entity, *args: |
| _STATUS_ENUM_TO_ID_MAP[entity.key.parent().get().status], |
| options=options) |
| self.list_config.setColumnEditable('status', True, 'select') |
| self.list_config.addPostEditButton(_SET_STATUS_BUTTON_ID, 'Save') |
| |
| self.list_config.setRowAction( |
| lambda entity, *args: links.LINKER.organization( |
| entity.key.parent(), urls.UrlNames.ORG_APPLICATION_SHOW)) |
| |
| def templatePath(self): |
| """See template.Template.templatePath for specification.""" |
| return 'summerofcode/organization/_org_application_list.html' |
| |
| def context(self): |
| """See template.Template.context for specification.""" |
| description = ORGANIZATION_LIST_DESCRIPTION |
| |
| list_configuration_response = lists.ListConfigurationResponse( |
| self.data, self.list_config, 0, description) |
| |
| return {'lists': [list_configuration_response]} |
| |
| def getListData(self): |
| """Returns data for the list.""" |
| query = org_logic.getApplicationResponsesQuery(self.data.org_app.key()) |
| |
| response_builder = lists.RawQueryContentResponseBuilder( |
| self.data.request, self.list_config, query, lists.keyStarter, |
| prefetcher=None) |
| return response_builder.buildNDB() |
| |
| |
| class PublicOrganizationListRowRedirect(melange_lists.RedirectCustomRow): |
| """Class which provides redirects for rows of public organization list.""" |
| |
| def __init__(self, data): |
| """Initializes a new instance of the row redirect. |
| |
| See lists.RedirectCustomRow.__init__ for specification. |
| |
| Args: |
| data: request_data.RequestData for the current request. |
| """ |
| super(PublicOrganizationListRowRedirect, self).__init__() |
| self.data = data |
| |
| def getLink(self, item): |
| """See lists.RedirectCustomRow.getLink for specification.""" |
| org_key = ndb.Key( |
| self.data.models.ndb_org_model._get_kind(), item['columns']['key']) |
| return links.LINKER.organization(org_key, urls.UrlNames.ORG_HOME) |
| |
| |
| class PublicOrganizationListPage(base.GSoCRequestHandler): |
| """View to list all participating organizations in the program.""" |
| |
| # TODO(daniel): the page should be accessible after orgs are announced |
| access_checker = access.ALL_ALLOWED_ACCESS_CHECKER |
| |
| def templatePath(self): |
| """See base.GSoCRequestHandler.templatePath for specification.""" |
| return 'modules/gsoc/accepted_orgs/base.html' |
| |
| def djangoURLPatterns(self): |
| """See base.GSoCRequestHandler.djangoURLPatterns for specification.""" |
| return [ |
| # TODO(daniel): remove "new", when the old view is not needed anymore |
| soc_url_patterns.url( |
| r'org/list/public/%s$' % url_patterns.PROGRAM, self, |
| name=urls.UrlNames.ORG_PUBLIC_LIST)] |
| |
| |
| def jsonContext(self, data, check, mutator): |
| """See base.GSoCRequestHandler.jsonContext for specification.""" |
| query = data.models.ndb_org_model.query( |
| org_model.Organization.program == |
| ndb.Key.from_old_key(data.program.key()), |
| org_model.Organization.status == org_model.Status.ACCEPTED) |
| |
| response = melange_lists.JqgridResponse( |
| melange_lists.ORGANIZATION_LIST_ID, |
| row=PublicOrganizationListRowRedirect(data)) |
| |
| start = html.escape(data.GET.get('start', '')) |
| |
| return response.getData(query, start=start) |
| |
| def context(self, data, check, mutator): |
| """See base.GSoCRequestHandler.context for specification.""" |
| return { |
| 'page_name': "Accepted organizations for %s" % data.program.name, |
| 'accepted_orgs_list': PublicOrganizationList(data), |
| } |
| |
| |
| class OrgApplicationListPage(base.GSoCRequestHandler): |
| """View to list all applications that have been submitted in the program.""" |
| |
| # TODO(daniel): This list should be active only when org application is set. |
| access_checker = access.PROGRAM_ADMINISTRATOR_ACCESS_CHECKER |
| |
| def templatePath(self): |
| """See base.GSoCRequestHandler.templatePath for specification.""" |
| return 'soc/org_app/records.html' |
| |
| def djangoURLPatterns(self): |
| """See base.GSoCRequestHandler.djangoURLPatterns for specification.""" |
| return [ |
| soc_url_patterns.url( |
| r'org/application/list/%s$' % url_patterns.PROGRAM, self, |
| name=urls.UrlNames.ORG_APPLICATION_LIST)] |
| |
| def jsonContext(self, data, check, mutator): |
| """See base.GSoCRequestHandler.jsonContext for specification.""" |
| idx = lists.getListIndex(data.request) |
| if idx == 0: |
| list_data = OrgApplicationList(data, data.org_app).getListData() |
| return list_data.content() |
| else: |
| raise exception.BadRequest(message='Invalid ID has been specified.') |
| |
| def context(self, data, check, mutator): |
| """See base.GSoCRequestHandler.context for specification.""" |
| record_list = OrgApplicationList(data, data.org_app) |
| |
| page_name = translation.ugettext('Records - %s' % (data.org_app.title)) |
| context = { |
| 'page_name': page_name, |
| 'record_list': record_list, |
| 'apply_org_admission_decision_id':_APPLY_ORG_ADMISSION_DECISION_ID, |
| } |
| return context |
| |
| def post(self, data, check, mutator): |
| """See base.GSoCRequestHandler.post for specification.""" |
| button_id = data.POST.get('button_id') |
| if button_id is not None: |
| if button_id == _SET_STATUS_BUTTON_ID: |
| handler = SetOrganizationStatusHandler(self) |
| return handler.handle(data, check, mutator) |
| elif button_id == _APPLY_ORG_ADMISSION_DECISION_ID: |
| handler = ApplyOrgAdmissionDecisionHandler(self) |
| return handler.handle(data, check, mutator) |
| else: |
| raise exception.BadRequest( |
| message='Button id %s not supported.' % button_id) |
| else: |
| raise exception.BadRequest(message='Invalid POST data.') |
| |
| |
| class SetOrganizationStatusHandler(form_handler.FormHandler): |
| """Form handler implementation to set status of organizations based on |
| data which is sent in a request. |
| """ |
| |
| def handle(self, data, check, mutator): |
| """See form_handler.FormHandler.handle for specification.""" |
| post_data = data.POST.get('data') |
| |
| if not post_data: |
| raise exception.BadRequest(message='Missing data.') |
| |
| parsed_data = json.loads(post_data) |
| for org_key_id, properties in parsed_data.iteritems(): |
| org_key = ndb.Key( |
| data.models.ndb_org_model._get_kind(), org_key_id) |
| new_status = _STATUS_ID_TO_ENUM_MAP.get(properties.get('status')) |
| if not new_status: |
| raise exception.BadRequest( |
| message='Missing or invalid new status in POST data.') |
| else: |
| organization = org_key.get() |
| org_logic.setStatus( |
| organization, data.program, data.site, |
| data.program.getProgramMessages(), new_status) |
| return http.HttpResponse() |
| |
| |
| class ApplyOrgAdmissionDecisionHandler(form_handler.FormHandler): |
| """Form handler implementation to start map reduce job that will apply |
| current decisions regarding admission of organizations into the program. |
| """ |
| |
| def handle(self, data, check, mutator): |
| """See form_handler.FormHandler.handle for specification.""" |
| params = { |
| 'program_key': str(data.program.key()), |
| # TODO(daniel): it must be obtained programatically somehow. |
| 'entity_kind': 'summerofcode.models.organization.SOCOrganization', |
| } |
| mapreduce_control.start_map('ApplyOrgAdmissionDecisions', params) |
| |
| url = links.LINKER.program(data.program, urls.UrlNames.ORG_APPLICATION_LIST) |
| return http.HttpResponseRedirect(url + '?validated=True') |
| |
| |
| @ndb.transactional |
| def updateOrganizationTxn( |
| org_key, org_properties, org_messages_properties=None): |
| """Updates the specified organization based on the specified properties. |
| |
| This function simply calls organization logic's function to do actual job |
| but ensures that the entire operation is executed within a transaction. |
| |
| Args: |
| org: Organization entity. |
| org_properties: A dict containing properties to be updated. |
| org_messages_properties: A optional dict containing properties of the |
| corresponding organization messages to be updated. |
| """ |
| org = org_key.get() |
| org_logic.updateOrganization( |
| org, org_properties=org_properties, |
| org_messages_properties=org_messages_properties) |