blob: ef0a349691c73873d004ad63612599cc65bc16b0 [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.
"""Views to manage Summer Of Code projects."""
from google.appengine.ext import db
from google.appengine.ext import ndb
from django import forms
from django import http
from django.utils import translation
from melange.request import access
from melange.request import exception
from melange.views.helper import form_handler
from soc.views import base
from soc.views.helper import url_patterns
from soc.modules.gsoc.models import project_survey as project_survey_model
from soc.modules.gsoc.views import base as soc_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.logic import project as project_logic
from summerofcode.logic import project_survey as project_survey_logic
from summerofcode.logic import survey as survey_logic
from summerofcode.models import project as project_model
from summerofcode.request import error
from summerofcode.request import links
from summerofcode.request import render
from summerofcode.views.helper import urls
MANAGE_PROJECT_ADMIN_PAGE_NAME = translation.ugettext(
'Manage project as Program Administrator')
PERSONAL_EXTENSION_FORM_START_DATE_LABEL = translation.ugettext('Start date')
PERSONAL_EXTENSION_FORM_END_DATE_LABEL = translation.ugettext('End date')
PERSONAL_EXTENSION_FORM_BUTTON_VALUE = translation.ugettext('Set Extension')
PROPOSAL_STATUS_FORM_STATUS_LABEL = translation.ugettext('Project status')
MIDTERM_EXTENSION_FORM_NAME = 'midterm_extension_form'
FINAL_EXTENSION_FORM_NAME = 'final_extension_form'
PROJECT_STATUS_FORM_NAME = 'project_status_form'
_FORM_NAMES = [MIDTERM_EXTENSION_FORM_NAME, FINAL_EXTENSION_FORM_NAME]
_STATUS_ACCEPTED_ID = 'accepted'
_STATUS_WITHDRAWN_ID = 'withdrawn'
_STATUS_ID_TO_ENUM_LINK = (
(_STATUS_ACCEPTED_ID, project_model.Status.ACCEPTED),
(_STATUS_WITHDRAWN_ID, project_model.Status.WITHDRAWN)
)
_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)
_STATUS_CHOICES = (
(_STATUS_ACCEPTED_ID, translation.ugettext('Accepted')),
(_STATUS_WITHDRAWN_ID, translation.ugettext('Withdrawn'))
)
_MESSAGE_CURRENT_USER_NOT_ADMIN = translation.ugettext(
'The currently logged in user is not an administrator for the organization '
'to which the project has been assigned.')
_MESSAGE_NO_MENTORS = translation.ugettext(
'No mentors have been assigned to the project. Each project must have at '
'least one mentor.')
_MESSAGE_USERS_NOT_MENTORS = translation.ugettext(
'%s are not mentors for the organization to which '
'the project has been submitted.')
def _getPersonalExtensionFormName(survey_type):
"""Returns name to be used by personal extension form for the specified
survey type.
Args:
survey_type: type of the survey. May be one of MIDTERM_EVAL or FINAL_EVAL.
Returns:
a string containing name for the form.
Raises:
ValueError: if survey type is not recognized.
"""
if survey_type == project_survey_model.MIDTERM_EVAL:
return MIDTERM_EXTENSION_FORM_NAME
elif survey_type == project_survey_model.FINAL_EVAL:
return FINAL_EXTENSION_FORM_NAME
else:
raise ValueError('Wrong survey type: %s' % survey_type)
def _getSurveyType(post_data):
"""Returns survey type for the form name of personal extension that is
submitted in POST data.
Args:
post_data: dict containing POST data.
Returns:
type of the survey. May be one of MIDTERM_EVAL or FINAL_EVAL.
Raises:
exception.BadRequest: if form name is not recoginized.
"""
if MIDTERM_EXTENSION_FORM_NAME in post_data:
return project_survey_model.MIDTERM_EVAL
elif FINAL_EXTENSION_FORM_NAME in post_data:
return project_survey_model.FINAL_EVAL
else:
raise exception.BadRequest(message='Form type not supported.')
def _getInitialValues(extension):
"""Returns initial values that should be populated to personal
extension form based on the specified extension entity.
Args:
extension: personal extension entity.
Returns:
a dict mapping form fields with their initial values. If extension is
not set, an empty dict is returned.
"""
return {
'start_date': extension.start_date,
'end_date': extension.end_date
} if extension else {}
def _setPersonalExtension(profile_key, survey_key, form):
"""Sets personal extension evaluation for the specified profile and
the specified survey based on the data sent in the specified form.
The extension is not set if
Args:
profile_key: profile key.
survey_key: survey key.
form: forms.Form instance that contains data sent by the user.
Returns:
True, if an extension has been successfully set; False otherwise.
"""
@ndb.transactional
def setPersonalExtensionTxn():
"""Transaction to set personal extension."""
start_date = form.cleaned_data['start_date']
end_date = form.cleaned_data['end_date']
survey_logic.createOrUpdatePersonalExtension(
profile_key, survey_key, start_date=start_date, end_date=end_date)
if form.is_valid():
setPersonalExtensionTxn()
return True
else:
return False
class ProjectStatusForm(gsoc_forms.GSoCModelForm):
"""Form to set status of a project."""
name = PROJECT_STATUS_FORM_NAME
meta = object
status = forms.CharField(
required=True,
widget=forms.Select(choices=_STATUS_CHOICES),
label=PROPOSAL_STATUS_FORM_STATUS_LABEL)
class PersonalExtensionForm(forms.Form):
"""Form type used to set personal extensions."""
start_date = forms.DateTimeField(required=False,
label=PERSONAL_EXTENSION_FORM_START_DATE_LABEL)
end_date = forms.DateTimeField(required=False,
label=PERSONAL_EXTENSION_FORM_END_DATE_LABEL)
def __init__(self, name=None, title=None, **kwargs):
"""Initializes the form with the specified values.
Args:
name: name of the form that is used as an identifier.
title: title of the form.
"""
super(PersonalExtensionForm, self).__init__(**kwargs)
self.name = name
self.title = title
self.button_value = PERSONAL_EXTENSION_FORM_BUTTON_VALUE
class ManageProjectProgramAdminView(soc_base.GSoCRequestHandler):
"""View for Program Administrators to manage projects."""
access_checker = access.PROGRAM_ADMINISTRATOR_ACCESS_CHECKER
def templatePath(self):
"""See base.templatePath for specification."""
return 'project_manage/admin_manage.html'
def djangoURLPatterns(self):
"""See base.djangoURLPatterns for specification."""
return [
soc_url_patterns.url(
r'project/manage/admin/%s$' % url_patterns.USER_ID,
self, name=urls.UrlNames.PROJECT_MANAGE_ADMIN)
]
def context(self, data, check, mutator):
"""See base.context for specification."""
evaluations = project_survey_logic.getStudentEvaluations(
data.program.key())
extension_forms = []
for evaluation in evaluations:
# try getting existing extension for this evaluation
extension = survey_logic.getPersonalExtension(
data.url_ndb_project.key.parent(), evaluation.key())
initial = _getInitialValues(extension)
name = _getPersonalExtensionFormName(evaluation.survey_type)
extension_forms.append(PersonalExtensionForm(data=data.POST or None,
name=name, title=evaluation.title, initial=initial))
if data.url_ndb_project.status != project_model.Status.FAILED:
form_data = {
'status': _STATUS_ENUM_TO_ID_MAP[data.url_ndb_project.status]
}
status_form = ProjectStatusForm(data=data.POST or form_data)
else:
status_form = None
context = {
'page_name': MANAGE_PROJECT_ADMIN_PAGE_NAME,
'extension_forms': extension_forms,
'status_form': status_form
}
return context
def post(self, data, check, mutator):
"""See base.post for specification."""
profile_key = data.url_ndb_project.key.parent()
if PROJECT_STATUS_FORM_NAME in data.POST:
handler = ProjectStatusHandler(self)
return handler.handle(data, check, mutator)
else:
# get type of survey based on submitted form name
survey_type = _getSurveyType(data.POST)
survey_key = project_survey_logic.constructEvaluationKey(
data.program.key(), survey_type)
# check if the survey exists
if not db.get(survey_key):
raise exception.BadRequest(message='Survey of type %s not found.' %
survey_type)
# try setting a personal extension
form = PersonalExtensionForm(data=data.POST)
result = _setPersonalExtension(profile_key, survey_key, form)
if result:
# redirect to somewhere
url = links.SOC_LINKER.userId(
data.url_ndb_profile.key, data.url_ndb_project.key.id(),
urls.UrlNames.PROJECT_MANAGE_ADMIN)
# TODO(daniel): append GET parameter in a better way
url = url + '?validated'
return http.HttpResponseRedirect(url)
else:
# TODO(nathaniel): problematic self-use.
return self.get(data, check, mutator)
class ProjectStatusHandler(form_handler.FormHandler):
"""Handler to set status of the project."""
def handle(self, data, check, mutator):
"""See form_handler.FormHandler.handle for specification."""
form = ProjectStatusForm(data=data.POST)
if not form.is_valid():
# TODO(nathaniel): problematic self-use.
return self._view.get(data, check, mutator)
else:
status = _STATUS_ID_TO_ENUM_MAP[form.cleaned_data['status']]
if status == project_model.Status.ACCEPTED:
result = project_logic.acceptProject(data.url_ndb_project.key)
elif status == project_model.Status.WITHDRAWN:
result = project_logic.withdrawProject(data.url_ndb_project.key)
else:
raise ValueError('Unknown status %s' % status)
if not result:
return exception.BadRequest(message=result.extra)
else:
return http.HttpResponseRedirect('')
class IsAdminForUrlProjectOrganizationAccessChecker(access.AccessChecker):
"""AccessChecker that ensures that the currently logged in user is
an administrator for the organization corresponding the project which
is defined in the URL.
"""
def checkAccess(self, data, check):
"""See AccessChecker.checkAccess for specification."""
if data.url_ndb_project.organization not in data.ndb_profile.admin_for:
raise exception.Forbidden(message=_MESSAGE_CURRENT_USER_NOT_ADMIN)
# Universal access checker used by all handlers which are accessible only
# by organization administrators and program administrators.
MANAGE_PROJECT_ACCESS_CHECKER = access.DisjunctionAccessChecker([
access.ConjuctionAccessChecker([
access.HAS_PROFILE_ACCESS_CHECKER,
IsAdminForUrlProjectOrganizationAccessChecker()]),
access.PROGRAM_ADMINISTRATOR_ACCESS_CHECKER])
class ProjectAssignMentors(base.RequestHandler):
"""Handler to assign mentors for the specified project."""
access_checker = MANAGE_PROJECT_ACCESS_CHECKER
def __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names):
"""Initializes a new instance of the request handler for the specified
parameters.
Args:
initializer: Implementation of initialize.Initializer interface.
linker: Instance of links.Linker class.
renderer: Implementation of render.Renderer interface.
error_handler: Implementation of error.ErrorHandler interface.
url_pattern_constructor:
Implementation of url_patterns.UrlPatternConstructor.
url_names: Instance of url_names.UrlNames.
"""
super(ProjectAssignMentors, self).__init__(
initializer, linker, renderer, error_handler)
self.url_pattern_constructor = url_pattern_constructor
self.url_names = url_names
def djangoURLPatterns(self):
"""See base.RequestHandler.djangoURLPatterns for specification."""
return [
self.url_pattern_constructor.construct(
r'project/assign_mentors/%s$' % url_patterns.USER_ID,
self, name=self.url_names.PROJECT_ASSIGN_MENTORS),
]
def post(self, data, check, mutator):
"""See base.RequestHandler.post for specification."""
mentor_keys = set(
ndb.Key(urlsafe=urlsafe)
for urlsafe in data.POST.getlist('assign_mentor') if urlsafe)
mentors = ndb.get_multi(mentor_keys)
result = project_logic.assignMentors(
data.url_ndb_project.key, mentors)
if not result:
if not result.extra:
# no mentors have been assigned
raise exception.BadRequest(message=_MESSAGE_NO_MENTORS)
else:
raise exception.BadRequest(
message=_MESSAGE_USERS_NOT_MENTORS %
', '.join(mentor.public_name for mentor in result.extra))
else:
url = links.SOC_LINKER.userId(
data.url_ndb_profile.key, data.url_ndb_project.key.id(),
self.url_names.PROJECT_DETAILS) + '?verified=True'
return http.HttpResponseRedirect(url)
PROJECT_ASSIGN_MENTORS = ProjectAssignMentors(
soc_base._GSOC_INITIALIZER, links.SOC_LINKER, render.SOC_RENDERER,
error.SOC_ERROR_HANDLER, soc_url_patterns.SOC_URL_PATTERN_CONSTRUCTOR,
urls.UrlNames)