blob: a8e0ccbb7144787cb47084357d24983a52e09a9f [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.
"""The views and generalized organization related views are forms.
The classes defined here are supposed to be instantiated with dependencies that
are specific to actual programs.
"""
import datetime
import json
from google.appengine.ext import ndb
from django import forms as django_forms
from django import http
from django.template import loader
from django.utils import html
from django.utils import translation
from melange.logic import contact as contact_logic
from melange.logic import organization as org_logic
from melange.logic import profile as profile_logic
from melange.logic import signature as signature_logic
from melange.models import connection as connection_model
from melange.models import contact as contact_model
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
from melange.templates import profile_form_below_header
from melange.utils import lists as melange_lists
from melange.utils import time as time_utils
from melange.templates import survey_response_list
from melange.views import connection as connection_view
from melange.views.helper import form_handler
from soc.logic import cleaning
from soc.logic import conversation_updater
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 base
from soc.views import forms
from soc.views import template
from soc.views.helper import lists
from soc.views.helper import url_patterns
# TODO(daniel): tabs make sense only for Summer Of Code
from summerofcode.templates import tabs
# TODO(daniel): top-message makes sense only for Summer Of Code
from summerofcode.templates import top_message
ORG_ID_LABEL = translation.ugettext('Organization ID')
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. '
' This identifier must start with a letter (a-z), and the rest of the '
' characters can be letters (a-z), numbers (0-9), or an underscore (_).'
' No other characters are valid.')
ORG_NAME_LABEL = translation.ugettext('Organization name')
ORG_NAME_HELP_TEXT = translation.ugettext(
'Complete, formal name of the organization.')
DESCRIPTION_LABEL = translation.ugettext('Description')
DESCRIPTION_HELP_TEXT = translation.ugettext(
'Description of the organization to be displayed on a public profile page.')
TAGS_LABEL = translation.ugettext('Tags')
TAGS_HELP_TEXT = translation.ugettext(
'Comma separated list of organization tags. Each tag must be shorter '
'than %s characters.')
TAG_MAX_LENGTH = 30
LICENSE_LABEL = translation.ugettext('Main license')
LICENSE_HELP_TEXT = translation.ugettext(
'The main license which is used by this organization.')
_LICENSE_CHOICES = ((_license, _license) for _license in licenses.LICENSES)
LOGO_IMAGE_FIELD_ID = 'logo_image'
LOGO_IMAGE_LABEL = translation.ugettext('Logo Image')
LOGO_IMAGE_HELP_TEXT = translation.ugettext(
'Select an image file with logo of the organization. '
'Please provide a 256px256px image or ensure that the image will scale '
'cleanly to that size. '
'Supported formats are JPG, PNG, WEBP.')
LOGO_URL_LABEL = translation.ugettext('Logo URL')
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.')
LOGO_SUBMIT_BUTTON_TEXT = "Upload Logo"
IDEAS_PAGE_LABEL = translation.ugettext('Ideas list')
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>')
MAILING_LIST_LABEL = translation.ugettext('Mailing list')
MAILING_LIST_HELP_TEXT = translation.ugettext(
'Mailing list email address, URL to the sign-up page, etc.')
NOTIFICATIONS_EMAIL_LABEL = translation.ugettext('Notifications email')
NOTIFICATIONS_EMAIL_HELP_TEXT = translation.ugettext(
'If entered, all notifications for this organization will be sent '
'to this email address.')
WEB_PAGE_LABEL = translation.ugettext('Organization website')
WEB_PAGE_HELP_TEXT = translation.ugettext(
'Main website of the organization.')
IRC_CHANNEL_LABEL = translation.ugettext('IRC Channel')
IRC_CHANNEL_HELP_TEXT = translation.ugettext(
'Link to a web page describing your organization\'s public IRC channel. '
'Page should include server name, channel, and any specific policies '
'or instructions. Do not use an irc:// URL.')
FEED_URL_LABEL = translation.ugettext('Feed URL')
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_LABEL = translation.ugettext('Google+ URL')
GOOGLE_PLUS_HELP_TEXT = translation.ugettext(
'URL to the Google+ page of the organization.')
TWITTER_LABEL = translation.ugettext('Twitter URL')
TWITTER_HELP_TEXT = translation.ugettext(
'URL of the Twitter page of the organization.')
BLOG_LABEL = translation.ugettext('Blog page')
BLOG_HELP_TEXT = translation.ugettext(
'URL to the blog page of the organization.')
FACEBOOK_LABEL = translation.ugettext('Facebook URL')
FACEBOOK_HELP_TEXT = translation.ugettext(
'URL to the Facebook page of the organization.')
IS_VETERAN_LABEL = translation.ugettext('Veteran organization')
IS_VETERAN_HELP_TEXT = translation.ugettext(
'Check this field if the organization has participated in a previous '
'instance of the program.')
# 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, Crimea.')
TERMS_OF_SERVICE_LABEL = translation.ugettext(
'Read the terms and conditions and acknowledge by selecting the checkbox '
'at the end.')
TERMS_OF_SERVICE_AGREE_TEXT = translation.ugettext(
'I have read and agree to the terms and conditions.')
ADMINS_FIELD_ID = 'admins'
ADMINS_LABEL = translation.ugettext('Administrators')
_CONTACT_PROPERTIES_FORM_KEYS = [
'blog', 'facebook', 'feed_url', 'google_plus', 'irc_channel',
'mailing_list', 'twitter', 'web_page', 'notifications_email']
_ORG_PROFILE_PROPERTIES_FORM_KEYS = [
'description', 'ideas_page', 'logo_url', 'name', 'org_id', 'tags',
'license', 'is_veteran']
# keys of the form fields which are relevant to profile creation only
ONLY_CREATE_ORG_PROFILE_FIELD_IDS = set([
'org_id', 'eligible_country', 'terms_of_service'])
_TAG_TOO_LONG = translation.ugettext('Tag %s is too long: %s')
_ORGANIZATION_LIST_DESCRIPTION = translation.ugettext('List of organizations')
_ORG_PROFILE_CREATE_PAGE_NAME = translation.ugettext(
'Create organization profile')
_ORG_PROFILE_EDIT_PAGE_NAME = translation.ugettext(
'Edit organization profile')
_ORG_LOGO_EDIT_PAGE_NAME = translation.ugettext(
'Organization logo')
ORG_PREFERENCES_EDIT_PAGE_NAME = translation.ugettext(
'Edit organization preferences')
_ORG_APPLICATION_SUBMIT_PAGE_NAME = translation.ugettext(
'Submit application')
_ORG_APPLICATION_SHOW_PAGE_NAME = translation.ugettext(
'Organization application - %s')
_ORG_APPLICATION_RESPONSE_SHOW_PAGE_NAME = translation.ugettext(
'Organization questionnaire - %s')
APP_RESPONSE_GROUP_TITLE = translation.ugettext(
'Application response')
LOGO_IMAGE_HEIGHT = 256
LOGO_IMAGE_WIDTH = 256
GENERAL_INFO_GROUP_DESCRIPTOR = readonly.GroupDescriptor(
translation.ugettext('General Info'), [
readonly.FieldDescriptor(
org_model.Organization.org_id._name, ORG_ID_LABEL),
readonly.FieldDescriptor(
org_model.Organization.name._name, ORG_NAME_LABEL),
readonly.FieldDescriptor(
org_model.Organization.is_veteran._name, IS_VETERAN_LABEL),
readonly.FieldDescriptor(
org_model.Organization.description._name, DESCRIPTION_LABEL,
is_safe=True),
readonly.FieldDescriptor(
org_model.Organization.tags._name, TAGS_LABEL),
readonly.FieldDescriptor(
org_model.Organization.license._name, LICENSE_LABEL),
readonly.FieldDescriptor(
org_model.Organization.logo_url._name, LOGO_URL_LABEL),
readonly.FieldDescriptor(
LOGO_IMAGE_FIELD_ID, LOGO_IMAGE_LABEL),
readonly.FieldDescriptor(
org_model.Organization.ideas_page._name, IDEAS_PAGE_LABEL),
readonly.FieldDescriptor(
ADMINS_FIELD_ID, ADMINS_LABEL)
])
CONTACT_GROUP_DESCRIPTOR = readonly.GroupDescriptor(
translation.ugettext('Contact'), [
readonly.FieldDescriptor(
contact_model.Contact.mailing_list._name, MAILING_LIST_LABEL),
readonly.FieldDescriptor(
contact_model.Contact.email._name, NOTIFICATIONS_EMAIL_LABEL),
readonly.FieldDescriptor(
contact_model.Contact.web_page._name, WEB_PAGE_LABEL),
readonly.FieldDescriptor(
contact_model.Contact.irc_channel._name, IRC_CHANNEL_LABEL),
readonly.FieldDescriptor(
contact_model.Contact.feed_url._name, FEED_URL_LABEL),
readonly.FieldDescriptor(
contact_model.Contact.google_plus._name, GOOGLE_PLUS_LABEL),
readonly.FieldDescriptor(
contact_model.Contact.twitter._name, TWITTER_LABEL),
readonly.FieldDescriptor(
contact_model.Contact.blog._name, BLOG_LABEL),
readonly.FieldDescriptor(
contact_model.Contact.facebook._name, FACEBOOK_LABEL),
])
def getAdminsValue(organization):
"""Returns a value to render for 'admins' field for the specified
organization.
Args:
organization: org_model.Organization entity.
Returns:
A string to render on the page.
"""
context = {'admins': profile_logic.getOrgAdmins(organization)}
return loader.render_to_string(
'melange/organization/_admins.html', dictionary=context)
_SET_STATUS_BUTTON_ID = 'save'
_APPLY_ORG_ADMISSION_DECISION_ID = 'apply_org_admission_decision_button_id'
_APPLICATION_PROCCESS_UNFINISHED_REMINDERS_ID = (
'application_process_unfinished_reminders_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 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
class _OrgProfileForm(forms.ModelForm):
"""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)
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)
notifications_email = django_forms.CharField(
required=False, label=NOTIFICATIONS_EMAIL_LABEL,
help_text=NOTIFICATIONS_EMAIL_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)
terms_of_service = django_forms.BooleanField(
required=True, label=TERMS_OF_SERVICE_LABEL)
eligible_country = django_forms.BooleanField(
required=True, label=ELIGIBLE_COUNTRY_LABEL)
def __init__(self, bound_field_class, request_data,
include_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.RequestData for the current request.
include_fields: Optional list of identifiers of the fields to include
in the form.
"""
super(_OrgProfileForm, self).__init__(bound_field_class, **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
# terms of service field should be included only if defined for the program
if not request_data.program.org_admin_agreement:
self.fields.pop('terms_of_service', None)
# only keep fields explicitly included
if include_fields:
for field_id in self.fields.keys():
if field_id not in include_fields:
self.fields.pop(field_id, None)
# add the content of Terms Of Service document if still needed
if 'terms_of_service' in self.fields:
self.fields['terms_of_service'].widget = forms.TOSWidget(
tos_text=request_data.program.org_admin_agreement.content,
tos_agree_text=TERMS_OF_SERVICE_AGREE_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_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'])
clean_description = cleaning.clean_html_content('description')
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 adaptOrgProfilePropertiesForForm(organization):
"""Adapts properties of the specified organization so that they can be
used in the form.
Args:
organization: org_model.Organization entity.
Returns:
A dict containing organization properties for the form.
"""
properties = organization.to_dict()
# initialize list of tags as comma separated list of values
properties[org_model.Organization.tags._name] = (
', '.join(organization.tags))
if organization.contact:
properties.update(organization.contact.to_dict())
return properties
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 organization 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
class OrgApplicationReminder(object):
"""Reminder to be included in context if organization application has
yet to be submitted.
"""
def __init__(self, url, deadline):
"""Initializes a new instance of this class.
Args:
url: URL to Submit Organization Application page.
deadline: a datetime by which the application has to be submitted.
"""
self.url = url
self.deadline = deadline
class OrgProfileFormFactory(object):
"""Interface that defines a factory to create a form for
an organization profile.
"""
def create(self, request_data, **kwargs):
"""Creates a new instance of _OrgProfileForm for organization profile
for the specified parameters.
Args:
request_data: request_data.RequestData for the current request.
Returns:
_OrgProfileForm adjusted depending on the needs.
"""
raise NotImplementedError
class OrgProfileCreatePage(base.RequestHandler):
"""View to create organization profile."""
# TODO(daniel): access checker should be customized
access_checker = access.ConjuctionAccessChecker([
access.NON_STUDENT_PROFILE_ACCESS_CHECKER,
access.ORG_SIGNUP_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 OrgProfileFormFactory interface.
"""
super(OrgProfileCreatePage, 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'org/profile/create/%s$' % url_patterns.PROGRAM,
self, name=self.url_names.ORG_PROFILE_CREATE)]
def context(self, data, check, mutator):
"""See base.RequestHandler.context for specification."""
form = self.form_factory.create(data, data=data.POST)
return {
'page_name': _ORG_PROFILE_CREATE_PAGE_NAME,
'description': data.org_app.content,
'forms': [form],
'error': bool(form.errors),
'form_below_header_msg': profile_form_below_header.Create(
data, agreement=data.program.org_admin_agreement),
}
def post(self, data, check, mutator):
"""See base.RequestHandler.post for specification."""
form = self.form_factory.create(
data, data=data.POST, files=data.request.file_uploads)
if not form.is_valid():
# we are not storing this form, remove the uploaded blobs from the cloud
for blob_info in data.request.file_uploads.itervalues():
blob_info.delete()
# TODO(nathaniel): problematic self-use.
return self.get(data, check, mutator)
else:
contact_properties = form.getContactProperties()
result = contact_logic.createContact(**contact_properties)
if not result:
raise exception.BadRequest(message=result.extra)
else:
org_properties = form.getOrgProperties()
org_properties['contact'] = result.extra
# org_id is a special property
org_id = org_properties['org_id']
del org_properties['org_id']
result = createOrganizationTxn(
data, org_id, data.program.key(), org_properties, data.ndb_profile,
[data.ndb_profile.key], data.models,
tos_key=data.program.org_admin_agreement.key())
if not result:
raise exception.BadRequest(message=result.extra)
else:
url = links.LINKER.organization(
result.extra.key, self.url_names.ORG_APPLICATION_SUBMIT)
return http.HttpResponseRedirect(url)
class OrgProfileEditPage(base.RequestHandler):
"""View to edit organization profile."""
access_checker = access.ConjuctionAccessChecker([
access.IS_USER_ORG_ADMIN_FOR_NDB_ORG,
access.UrlOrgStatusAccessChecker(
[org_model.Status.ACCEPTED, org_model.Status.PRE_ACCEPTED,
org_model.Status.PRE_REJECTED, org_model.Status.APPLYING])])
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 OrgProfileFormFactory interface.
"""
super(OrgProfileEditPage, 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'org/profile/edit/%s$' % url_patterns.ORG,
self, name=self.url_names.ORG_PROFILE_EDIT)]
def context(self, data, check, mutator):
"""See base.RequestHandler.context for specification."""
form_data = adaptOrgProfilePropertiesForForm(data.url_ndb_org)
form = self.form_factory.create(data, data=data.POST or form_data)
# add a reminder if no application has been submitted and it is still
# before the deadline
if (not org_logic.getApplicationResponse(data.url_ndb_org.key) and
time_utils.isBefore(data.org_app.survey_end)):
url = links.LINKER.organization(
data.url_ndb_org.key, self.url_names.ORG_APPLICATION_SUBMIT)
deadline = data.org_app.survey_end
org_application_reminder = OrgApplicationReminder(url, deadline)
else:
org_application_reminder = None
# TODO(daniel): ugly, ugly ugly
org_tabs = (
tabs.orgTabs(data, selected_tab_id=tabs.ORG_PROFILE_TAB_ID)
if data.program.prefix == 'gsoc_program' else None)
return {
'page_name': _ORG_PROFILE_EDIT_PAGE_NAME,
'forms': [form],
'error': bool(form.errors),
'org_application_reminder': org_application_reminder,
'tabs': org_tabs,
'form_below_header_msg': profile_form_below_header.Create(
data, agreement=data.program.org_admin_agreement),
'form_top_msg': top_message.applicationProcessCompleteTopMessage(data),
}
def post(self, data, check, mutator):
"""See base.RequestHandler.post for specification."""
form = self.form_factory.create(
data, data=data.POST, files=data.request.file_uploads)
if not form.is_valid():
# we are not storing this form, remove the uploaded blobs from the cloud
for blob_info in data.request.file_uploads.itervalues():
blob_info.delete()
# TODO(nathaniel): problematic self-use.
return self.get(data, check, mutator)
else:
contact_properties = form.getContactProperties()
result = contact_logic.createContact(**contact_properties)
if not result:
raise exception.BadRequest(message=result.extra)
else:
org_properties = form.getOrgProperties()
org_properties['contact'] = result.extra
updateOrganizationTxn(data.url_ndb_org.key, org_properties)
url = links.LINKER.organization(
data.url_ndb_org.key, self.url_names.ORG_PROFILE_EDIT)
return http.HttpResponseRedirect(url)
class OrgApplicationSubmitPage(base.RequestHandler):
"""View to submit application to a program by organization representatives."""
access_checker = access.ConjuctionAccessChecker([
access.IS_USER_ORG_ADMIN_FOR_NDB_ORG,
access.UrlOrgStatusAccessChecker(
[org_model.Status.APPLYING, org_model.Status.PRE_ACCEPTED,
org_model.Status.PRE_REJECTED])])
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 survey_view.SurveyTakeFormFactory interface.
"""
super(OrgApplicationSubmitPage, 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'org/application/submit/%s$' % url_patterns.ORG,
self, name=self.url_names.ORG_APPLICATION_SUBMIT)]
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."""
application = org_logic.getApplicationResponse(data.url_ndb_org.key)
form_data = application.to_dict() if application else None
form = self.form_factory.create(data.org_app, data=data.POST or form_data)
# TODO(daniel): ugly, ugly, ugly
org_tabs = (
tabs.orgTabs(data, selected_tab_id=tabs.ORG_APP_RESPONSE_TAB_ID)
if data.program.prefix == 'gsoc_program' else None)
return {
'page_name': _ORG_APPLICATION_SUBMIT_PAGE_NAME,
'forms': [form],
'error': bool(form.errors),
'tabs': org_tabs,
'form_top_msg': top_message.surveyResponseTopMessage(
data, bool(application))
}
def post(self, data, check, mutator):
"""See base.RequestHandler.post for specification."""
form = self.form_factory.create(data.org_app, data=data.POST)
if not form.is_valid():
# TODO(nathaniel): problematic self-use.
return self.get(data, check, mutator)
else:
properties = form.getSurveyResponseProperties()
org_logic.setApplicationResponse(
data.url_ndb_org.key, data.org_app.key(), properties)
url = links.LINKER.organization(
data.url_ndb_org.key, self.url_names.ORG_APPLICATION_SUBMIT)
return http.HttpResponseRedirect(url + '?validated=True')
class OrgApplicationReadonlyFactory(object):
"""Interface that defines a factory to create read-only template for
the whole organization application.
"""
def create(self, data, app_response):
"""Creates a new instance of readonly.Readonly template for the organization
defined in the URL and the specified response.
Args:
data: request_data.RequestData for the current request.
app_response: survey_model.SurveyResponse entity.
Returns:
readonly.Readonly for organization application.
"""
raise NotImplementedError
class OrgApplicationShowPage(base.RequestHandler):
"""Page to display an organization application for program administrators."""
access_checker = access.DisjunctionAccessChecker([
access.PROGRAM_ADMINISTRATOR_ACCESS_CHECKER,
access.IS_USER_ORG_ADMIN_FOR_NDB_ORG])
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 OrgApplicationReadonlyFactory interface.
"""
super(OrgApplicationShowPage, 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."""
# TODO(daniel): remove 2 when the old view is removed.
return [
self.url_pattern_constructor.construct(
r'org/application/show2/%s$' % url_patterns.ORG,
self, name=self.url_names.ORG_APPLICATION_SHOW)]
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."""
app_response = org_logic.getApplicationResponse(data.url_ndb_org.key)
return {
'page_name': _ORG_APPLICATION_SHOW_PAGE_NAME % data.url_ndb_org.name,
'record': self.readonly_factory.create(data, app_response)
}
class AppResponseReadonlyFactory(object):
"""Interface that defines a factory to create read-only template for
the survey response of the organization.
"""
def create(self, data, app_response):
"""Creates a new instance of readonly.Readonly template for the organization
defined in the URL and the specified response.
Args:
data: request_data.RequestData for the current request.
app_response: survey_model.SurveyResponse entity.
Returns:
readonly.Readonly for organization application.
"""
raise NotImplementedError
class OrgApplicationResponseShowPage(base.RequestHandler):
"""Page to display survey response."""
access_checker = access.IS_USER_ORG_ADMIN_FOR_NDB_ORG
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 AppResponseReadonlyFactory interface.
"""
super(OrgApplicationResponseShowPage, 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'org/survey_response/show/%s$' % url_patterns.ORG,
self, name=self.url_names.ORG_APPLICATION_RESPONSE_SHOW)]
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."""
app_response = org_logic.getApplicationResponse(data.url_ndb_org.key)
record = (
self.readonly_factory.create(data, app_response)
if app_response else None)
# TODO(daniel): ugly, ugly, ugly
org_tabs = (
tabs.orgTabs(data, selected_tab_id=tabs.ORG_APP_RESPONSE_TAB_ID)
if data.program.prefix == 'gsoc_program' else None)
return {
'page_name': _ORG_APPLICATION_RESPONSE_SHOW_PAGE_NAME % (
data.url_ndb_org.name),
'record': record,
'tabs': org_tabs,
}
class OrgLogoForm(forms.ModelForm):
"""Form to set logo image for the organization."""
logo_image = forms.ImageField(
required=True, label=LOGO_IMAGE_LABEL, help_text=LOGO_IMAGE_HELP_TEXT,
required_width=LOGO_IMAGE_WIDTH, required_height=LOGO_IMAGE_HEIGHT,
widget=forms.ImageWidget(
width=LOGO_IMAGE_WIDTH, height=LOGO_IMAGE_HEIGHT))
# See files_utils.DONT_USE_BLOBOSTORE_MIDDLEWARE for details on this field
dont_use_blobstore_middleware = forms.CharField(
required=False, widget=forms.HiddenInput())
class OrgLogoFormFactory(object):
"""Interface that defines a factory to create instances of OrgLogoForm
class.
"""
def create(self, **kwargs):
"""Creates a new instance of OrgLogoForm for the specified parameters.
Returns:
The newly created OrgLogoForm instance.
"""
raise NotImplementedError
class OrgLogoEditPage(base.RequestHandler):
"""View to set logo image for the organization"""
access_checker = access.ConjuctionAccessChecker([
access.IS_USER_ORG_ADMIN_FOR_NDB_ORG,
access.UrlOrgStatusAccessChecker(
[org_model.Status.ACCEPTED, org_model.Status.PRE_ACCEPTED,
org_model.Status.PRE_REJECTED, org_model.Status.APPLYING])])
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 OrgLogoFormFactory interface.
"""
super(OrgLogoEditPage, 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'org/logo/edit/%s$' % url_patterns.ORG,
self, name=self.url_names.ORG_LOGO_EDIT)]
def context(self, data, check, mutator):
"""See base.RequestHandler.context for specification."""
initial = {'logo_image': data.url_ndb_org.logo_image_url}
form = self.form_factory.create(
data=data.POST, initial=initial, files=data.request.FILES)
# this is hacky, but it is necessary because of the way how Django deals
# with file uploads: if logo image is in request.file_uploads and does not
# validate, the existing (pre-upload) image will not be rendered (because
# files takes precedence over initial).
# here, a new form instance is created and errors are explicitly set
# so that the error message is written out and the image is still rendered
if 'logo_image' in form.errors and data.url_ndb_org.logo_image_url:
error_dict = form.errors
form = self.form_factory.create(data=data.POST, initial=initial)
form._errors = error_dict
# TODO(daniel): ugly, ugly ugly
# This should not depend on 'gsoc_program' literal.
org_tabs = (
tabs.orgTabs(data, selected_tab_id=tabs.ORG_LOGO_TAB_ID)
if data.program.prefix == 'gsoc_program' else None)
return {
'forms': [form],
'error': bool(form.errors),
'is_multipart': True,
'page_name': _ORG_LOGO_EDIT_PAGE_NAME,
'button_value': LOGO_SUBMIT_BUTTON_TEXT,
'tabs': org_tabs,
'form_top_msg': top_message.applicationProcessCompleteTopMessage(data),
}
def post(self, data, check, mutator):
"""See base.RequestHandler.post for specification."""
form = self.form_factory.create(
data=data.POST, files=data.request.FILES)
if not form.is_valid():
# we are not storing this form, remove the uploaded blobs from the cloud
for blob_info in data.request.file_uploads.itervalues():
blob_info.delete()
# TODO(nathaniel): problematic self-use.
return self.get(data, check, mutator)
else:
try:
org_logic.setOrgLogo(
data.url_ndb_org.key, form.cleaned_data.get('logo_image'))
except Exception:
# NOTE: This error message is vague. It results from ambiguous
# characteristics of exceptions which are raised by Cloud Storage API
raise exception.BadRequest(
message='An error occurred. Please try again later or try '
'to upload another image.')
url = links.LINKER.organization(
data.url_ndb_org.key, self.url_names.ORG_LOGO_EDIT)
return http.HttpResponseRedirect(url)
class OrgPreferencesForm(forms.ModelForm):
"""Form to set properties of organization preferences by organization
administrators.
"""
def __init__(self, bound_field_class, org_properties_field_keys=None,
org_messages_properties_field_keys=None, **kwargs):
"""Initializes a new form.
Args:
bound_field_class: Subclass of BoundField class to be used for the from.
org_properties_field_keys: Field keys that correspond to properties
in organization model.
org_messages_properties_field_keys: Field keys that correspond to
properties in organization messages model.
"""
super(OrgPreferencesForm, self).__init__(bound_field_class, **kwargs)
self.org_properties_field_keys = org_properties_field_keys or []
self.org_messages_properties_field_keys = (
org_messages_properties_field_keys or [])
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(self.org_properties_field_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(self.org_messages_properties_field_keys)
class OrgPreferencesFormFactory(object):
"""Interface that defines a factory to create a form for
organization preferences.
"""
def create(self, request_data, **kwargs):
"""Creates a new instance of a from for organization preferences
for the specified parameters.
Args:
request_data: request_data.RequestData for the current request.
Returns:
Implementation of OrgPreferencesForm adjusted depending on the needs.
"""
raise NotImplementedError
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.RequestHandler):
"""View to edit organization preferences."""
access_checker = ORG_PREFERENCES_EDIT_PAGE_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 AppResponseReadonlyFactory interface.
"""
super(OrgPreferencesEditPage, 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'org/preferences/edit/%s$' % url_patterns.ORG,
self, name=self.url_names.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 = self.form_factory.create(data=data.POST or form_data)
# TODO(daniel): ugly, ugly ugly
org_tabs = (
tabs.orgTabs(data, selected_tab_id=tabs.ORG_PREFERENCES_TAB_ID)
if data.program.prefix == 'gsoc_program' else None)
return {
'error': bool(form.errors),
'forms': [form],
'page_name': ORG_PREFERENCES_EDIT_PAGE_NAME,
'tabs': org_tabs,
}
def post(self, data, check, mutator):
"""See base.RequestHandler.post for specification."""
form = self.form_factory.create(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, self.url_names.ORG_PREFERENCES_EDIT)
return http.HttpResponseRedirect(url)
_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, url_names, template_path,
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
url_names: Instance of url_names.UrlNames.
template_path: The path of the template to be used.
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.url_names = url_names
self.template_path = template_path
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(
'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)
def getHasBackupAdmin(entity, *args):
"""Helper function to get value for has_backup_admin column."""
admin_keys = profile_logic.getOrgAdmins(
entity.key.parent().get(), keys_only=True)
return 'Yes' if len(admin_keys) >= 2 else 'No'
self.list_config.addPlainTextColumn(
'has_backup_admin', 'Has Backup Admin', getHasBackupAdmin, hidden=True)
self.list_config.addPlainTextColumn(
'has_logo', 'Has Logo',
lambda entity, *args:
'Yes' if entity.key.parent().get().logo_path else 'No', hidden=True)
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'),
]
# we strip of the first element of options, which is "All". See TODO above.
editoptions_values = ';'.join('%s:%s' % option for option in options[1:])
editoptions = dict(value=editoptions_values)
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', editoptions)
self.list_config.addPostEditButton(_SET_STATUS_BUTTON_ID, 'Save')
self.list_config.setRowAction(
lambda entity, *args: links.LINKER.organization(
entity.key.parent(), self.url_names.ORG_APPLICATION_SHOW))
def templatePath(self):
"""See template.Template.templatePath for specification."""
return self.template_path
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 OrgApplicationListPage(base.RequestHandler):
"""View to list all applications that have been submitted in the program."""
access_checker = access.ConjuctionAccessChecker([
access.PROGRAM_ADMINISTRATOR_ACCESS_CHECKER,
access.APPLICATION_SURVEY_EXISTS_ACCESS_CHECKER])
def __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names, template_path,
list_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.
list_template_path: The path of the template to be used to render
the list of applications.
"""
super(OrgApplicationListPage, 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.list_template_path = list_template_path
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'org/application/list/%s$' % url_patterns.PROGRAM, self,
name=self.url_names.ORG_APPLICATION_LIST)]
def jsonContext(self, data, check, mutator):
"""See base.RequestHandler.jsonContext for specification."""
idx = lists.getListIndex(data.request)
if idx == 0:
list_data = OrgApplicationList(
data, data.org_app, self.url_names,
self.list_template_path).getListData()
return list_data.content()
else:
raise exception.BadRequest(message='Invalid ID has been specified.')
def context(self, data, check, mutator):
"""See base.RequestHandler.context for specification."""
record_list = OrgApplicationList(
data, data.org_app, self.url_names, self.list_template_path)
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,
'application_process_unfinished_reminders_id':
_APPLICATION_PROCCESS_UNFINISHED_REMINDERS_ID,
'application_process_unfinished_reminders_sent_on':
data.program.application_process_unfinished_reminders_sent_on,
}
return context
def post(self, data, check, mutator):
"""See base.RequestHandler.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:
url = links.LINKER.program(
data.program, self.url_names.ORG_APPLICATION_LIST)
handler = ApplyOrgAdmissionDecisionHandler(self, url=url)
return handler.handle(data, check, mutator)
elif button_id == _APPLICATION_PROCCESS_UNFINISHED_REMINDERS_ID:
url = links.LINKER.program(
data.program, self.url_names.ORG_APPLICATION_LIST)
handler = ApplicationProcessUnfinishedReminders(self, url=url)
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 OrgApplicationBulkListPage(base.RequestHandler):
"""Bulk view to change status of org applications for a program.
Provides a simple view of all Organization Applications, and
provides a Bulk edit mechanism for changing the state of unaccepted
applications.
"""
access_checker = access.PROGRAM_ADMINISTRATOR_ACCESS_CHECKER
# used to prefix Org Keys in the post data
ORG_KEY_PREFIX = "orgstatus_"
ORG_KEY_PREFIX_LEN = len(ORG_KEY_PREFIX)
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.
list_template_path: The path of the template to be used to render
the list of applications.
"""
super(OrgApplicationBulkListPage, 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.changes = []
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'org/application/bulklist/%s$' % url_patterns.PROGRAM, self,
name=self.url_names.ORG_APPLICATION_BULK_LIST)]
def context(self, data, check, mutator):
"""See base.RequestHandler.context for specification."""
# TODO(robert): Consider adding pagination support to this
# handler.
query = org_logic.getApplicationResponsesQuery(data.org_app.key())
applications = query.fetch(1000)
org_keys = [application.key.parent() for application in applications]
orgs = ndb.get_multi(org_keys)
options = (
org_model.Status.APPLYING,
org_model.Status.PRE_ACCEPTED,
org_model.Status.PRE_REJECTED,
)
content = []
for org in orgs:
# these states are unchangable, so mark then as final
finalized_status = org.status in (org_model.Status.ACCEPTED,
org_model.Status.REJECTED)
url = links.LINKER.organization(
org.key, self.url_names.ORG_APPLICATION_SHOW)
content.append(
{'name': org.name,
'org_id': org.org_id,
'org_key': org.key,
'status': org.status,
'final': finalized_status,
'url': url
})
page_name = translation.ugettext(
'Org Bulk Status Edit - %s' % (data.org_app.title))
context = {
'page_name': page_name,
'old_application_url': links.LINKER.program(
data.program, self.url_names.ORG_APPLICATION_LIST),
'content': content,
'options': options,
'changes': self.changes,
'org_key_prefix': self.ORG_KEY_PREFIX,
}
return context
def post(self, data, check, mutator):
"""See base.RequestHandler.post for specification."""
self.changes = []
# figure out which orgs are selected.
affected = [key for key in data.POST.keys()
if key.startswith(self.ORG_KEY_PREFIX)]
# are we setting to Pre-Accept or Pre-Reject?
status_value = data.POST.get('status')
new_status = org_model.Status(status_value)
org_keys = []
for key in affected:
# skip unchecked orgs
if data.POST.get(key) != "on":
continue
org_id = key[self.ORG_KEY_PREFIX_LEN:] # strip off 'ORG_KEY_PREFIX'
org_key = ndb.Key(
data.models.ndb_org_model._get_kind(), org_id)
org_keys.append(org_key)
if org_keys:
organizations = ndb.get_multi(org_keys)
changed_organizations = []
for organization in organizations:
if organization.status == new_status:
self.changes.append(
"%s already is %s" % (organization.org_id, new_status))
else:
self.changes.append(
"%s set to %s" % (organization.org_id, new_status))
organization.status = new_status
changed_organizations.append(organization)
# Not using org_logic.setStatus here so we can use
# ndb.put_multi. (This is ok right now, because setStatus
# doesn't have any logic for the pre- states.)
ndb.put_multi(changed_organizations)
return self.get(data, check, mutator)
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()),
'entity_kind': 'melange.models.organization.Organization',
}
mapreduce_control.start_map('ApplyOrgAdmissionDecisions', params)
return http.HttpResponseRedirect(self._url)
class ApplicationProcessUnfinishedReminders(form_handler.FormHandler):
"""Form handler implementation to start map reduce job that will send
reminders to administrators of applying organization who have not finished.
"""
def handle(self, data, check, mutator):
"""See form_handler.FormHandler.handle for specification."""
params = {
'program_key': str(data.program.key()),
'entity_kind': 'melange.models.organization.Organization',
'dry_run': 'False',
}
mapreduce_control.start_map('ApplicationProcessUnfinishedReminders', params)
# update the timestamp in program model.
data.program.application_process_unfinished_reminders_sent_on = (
datetime.datetime.utcnow())
data.program.put()
return http.HttpResponseRedirect(self._url)
class PublicOrganizationListFactory(object):
"""Interface that defines a factory to create lists of organizations."""
def create(self, data):
"""Creates a new list of organizations.
Returns:
A template with a list of organizations.
"""
raise NotImplementedError
class PublicOrganizationListRowRedirect(melange_lists.RedirectCustomRow):
"""Class which provides redirects for rows of public organization list."""
def __init__(self, data, url_names):
"""Initializes a new instance of the row redirect.
See lists.RedirectCustomRow.__init__ for specification.
Args:
data: request_data.RequestData for the current request.
url_names: Instance of url_names.UrlNames.
"""
super(PublicOrganizationListRowRedirect, self).__init__()
self.data = data
self.url_names = url_names
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, self.url_names.ORG_HOME)
class PublicOrganizationListPage(base.RequestHandler):
"""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 __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names, template_path, list_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.
list_factory: Implementation of PublicOrganizationListFactory interface.
"""
super(PublicOrganizationListPage, 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.list_factory = list_factory
def templatePath(self):
"""See base.GSoCRequestHandler.templatePath for specification."""
return self.template_path
def djangoURLPatterns(self):
"""See base.GSoCRequestHandler.djangoURLPatterns for specification."""
return [
self.url_pattern_constructor.construct(
r'org/list/public/%s$' % url_patterns.PROGRAM, self,
name=self.url_names.ORG_PUBLIC_LIST)]
def jsonContext(self, data, check, mutator):
"""See base.GSoCRequestHandler.jsonContext for specification."""
tag = data.GET.get('tag')
if tag:
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,
org_model.Organization.tags.IN([tag]))
else:
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, self.url_names))
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': self.list_factory.create(data),
}
# TODO(nathaniel): remove suppression when
# https://bitbucket.org/logilab/pylint.org/issue/6/false-positive-no
# is fixed.
@ndb.transactional(xg=True) # pylint: disable=no-value-for-parameter
def createOrganizationTxn(
data, org_id, program_key, org_properties, profile, admin_keys, models,
tos_key=None):
"""Creates a new organization profile 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:
data: request_data.RequestData for the current request.
org_id: Identifier of the new organization. Must be unique on
'per program' basis.
program_key: Program key.
org_properties: A dict mapping organization properties to their values.
profile: profile_model.Profile entity of the administrator who creates the
organization profile.
admin_keys: List of profile keys of organization administrators for
this organization.
models: instance of types.Models that represent appropriate models.
Returns:
RichBool whose value is set to True if organization has been successfully
created. In that case, extra part points to the newly created organization
entity. Otherwise, RichBool whose value is set to False and extra part is
a string that represents the reason why the action could not be completed.
"""
result = org_logic.createOrganization(
org_id, program_key, org_properties, models)
if not result:
raise exception.BadRequest(message=result.extra)
for admin_key in admin_keys:
connection_view.createConnectionTxn(
data, admin_key, data.program, result.extra,
conversation_updater.ConversationUpdater(),
org_role=connection_model.ORG_ADMIN_ROLE,
user_role=connection_model.ROLE, seen_by_org=True, seen_by_user=True)
if tos_key:
signature_logic.createSignature(ndb.Key.from_old_key(tos_key), profile)
return result
@ndb.transactional(xg=True)
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)