| # 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) |