blob: 7d917977819999c0d0e133c942d1c593fa03bfdf [file] [log] [blame]
# 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)