| # 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. |
| |
| """Module for templates with proposal lists.""" |
| |
| import cgi |
| import json |
| |
| from google.appengine.ext import ndb |
| |
| from django.utils import translation |
| |
| from melange.models import organization as org_model |
| from melange.models import profile as profile_model |
| from melange.request import exception |
| from melange.request import links |
| |
| from soc.views import template |
| from soc.views.helper import lists |
| |
| from summerofcode.logic import proposal as proposal_logic |
| from summerofcode.models import proposal as proposal_model |
| from summerofcode.views.helper import urls |
| |
| |
| _PROPOSAL_LIST_TITLE = translation.ugettext('Proposals') |
| |
| _PROPOSAL_LIST_FOR_ORG_MEMBER_DESCRIPTION = translation.ugettext( |
| 'List of proposals which have been submitted to your organizations') |
| |
| _PROPOSAL_LIST_FOR_STUDENT_DESCRIPTION = translation.ugettext( |
| 'List of proposals which you have submitted into the program.') |
| |
| _DUPLICATE_STATUS = translation.ugettext('Duplicate') |
| _NO_MENTOR_ASSIGNED_STATUS = translation.ugettext('No Mentor Assigned') |
| _ACCEPTED_BUT_WITHDRAWN_STATUS = translation.ugettext('Accepted but withdrawn') |
| _ACCEPTED_BUT_NO_MENTOR_ASSIGNED_STATUS = translation.ugettext( |
| 'Accepted but no mentor assigned.') |
| _ACCEPTED_BUT_IGNORED_STATUS = translation.ugettext('Accepted but ignored') |
| _PENDING_ACCEPTANCE_STATUS = translation.ugettext('Pending acceptance') |
| |
| _ACCEPTED_LABEL = translation.ugettext('Accepted') |
| _ALL_LABEL = translation.ugettext('All') |
| _DUPLICATE_LABEL = translation.ugettext('Duplicate') |
| _IGNORED_LABEL = translation.ugettext('Ignored') |
| _NEEDS_ATTENTION_LABEL = translation.ugettext('Needs Attention') |
| _NO_MENTOR_ASSIGNED_LABEL = translation.ugettext('No Mentor Assigned') |
| _REJECTED_LABEL = translation.ugettext('Rejected') |
| _REVIEWABLE_LABEL = translation.ugettext('Reviewable') |
| |
| _ACCEPTED_REGEX = '(%s)' % proposal_model.Status.ACCEPTED |
| _ALL_REGEX = '' |
| _DUPLICATE_REGEX = '(%s)' % _DUPLICATE_STATUS |
| _IGNORED_REGEX = '(%s)' % proposal_model.Status.IGNORED |
| _NEEDS_ATTENTION_REGEX = '(%s)' % ( |
| '|'.join([ |
| _DUPLICATE_STATUS, |
| _NO_MENTOR_ASSIGNED_STATUS, |
| _ACCEPTED_BUT_WITHDRAWN_STATUS, |
| _ACCEPTED_BUT_NO_MENTOR_ASSIGNED_STATUS, |
| _ACCEPTED_BUT_IGNORED_STATUS])) |
| _NO_MENTOR_ASSIGNED_REGEX = '(%s)' % _NO_MENTOR_ASSIGNED_STATUS |
| _REJECTED_REGEX = '(%s)' % proposal_model.Status.REJECTED |
| _REVIEWABLE_REGEX = '(%s)' % ( |
| '|'.join([ |
| str(proposal_model.Status.PENDING), |
| _DUPLICATE_STATUS, |
| _NO_MENTOR_ASSIGNED_STATUS, |
| _ACCEPTED_BUT_WITHDRAWN_STATUS, |
| _ACCEPTED_BUT_NO_MENTOR_ASSIGNED_STATUS, |
| _ACCEPTED_BUT_IGNORED_STATUS])) |
| |
| _STATUS_OPTIONS = [ |
| (_REVIEWABLE_REGEX, _REVIEWABLE_LABEL), |
| (_NEEDS_ATTENTION_REGEX, _NEEDS_ATTENTION_LABEL), |
| (_ACCEPTED_REGEX, _ACCEPTED_LABEL), |
| (_REJECTED_REGEX, _REJECTED_LABEL), |
| (_DUPLICATE_REGEX, _DUPLICATE_LABEL), |
| (_NO_MENTOR_ASSIGNED_REGEX, _NO_MENTOR_ASSIGNED_LABEL), |
| (_IGNORED_REGEX, _IGNORED_LABEL), |
| (_ALL_REGEX, _ALL_LABEL)] |
| |
| |
| SAVE_BUTTON_ID = 'save' |
| _SAVE_BUTTON_LABEL = 'Save' |
| |
| |
| def _roundAverageScore(entity, *args): |
| """Formatting function for the "Average" column. |
| |
| Ensures that the average score is rounded to two decimal places. |
| |
| Args: |
| entity: A Proposal entity. |
| *args: Ignored extra arguments. |
| |
| Returns: |
| The entity's average score rounded to two decimal places or None if the |
| entity has no average score. |
| """ |
| return ( |
| round(entity.average_score, ndigits=2) if entity.average_score else None) |
| |
| |
| class ProposalList(template.Template): |
| """Template for list of proposals.""" |
| |
| def __init__(self, template_path, data, list_config, query, |
| description, prefetcher=None): |
| """Initializes a new instance of the list for the specified parameters. |
| |
| Args: |
| url_names: Instance of url_names.UrlNames. |
| template_path: The path of the template to be used. |
| data: request_data.RequestData for the current request. |
| list_config: List configuration object. |
| query: ndb.Query to get list data. |
| description: A string containing description of the list. |
| prefetcher: Optional instance of lists.ListPrefetcher to prefetch data. |
| """ |
| super(ProposalList, self).__init__(data) |
| self.template_path = template_path |
| self._list_config = list_config |
| self._query = query |
| self._description = description |
| self._prefetcher = prefetcher |
| |
| def context(self): |
| """See template.Template.context for specification.""" |
| list_configuration_response = lists.ListConfigurationResponse( |
| self.data, self._list_config, 0, self._description) |
| |
| return { |
| 'list_title': _PROPOSAL_LIST_TITLE, |
| 'lists': [list_configuration_response] |
| } |
| |
| def getListData(self): |
| # TODO(daniel): add missing doc string |
| idx = lists.getListIndex(self.data.request) |
| if idx != 0: |
| return None |
| |
| prefetcher = self._prefetcher |
| |
| response_builder = lists.RawQueryContentResponseBuilder( |
| self.data.request, self._list_config, self._query, |
| lists.keyStarter, prefetcher=prefetcher) |
| |
| return response_builder.buildNDB() |
| |
| def postStudentProposalList(self): |
| """POST handler for the student proposals list actions. |
| |
| Returns: |
| True if the data is successfully modified; False otherwise. |
| """ |
| idx = lists.getListIndex(self.data.request) |
| if idx != 0: |
| return False |
| |
| post_data = self.data.POST.get('data') |
| |
| if not post_data: |
| raise exception.BadRequest(message="Missing data") |
| |
| # Sample parsed dictionary looks like, |
| # {u'ah9kZXZ.......2hpa2hlchbBiAgICAgICoCQw': {u'rank': u'3'}} |
| parsed = json.loads(post_data) |
| |
| # Fetching profile key of student from first proposal key in parsed data. |
| # Since this list of proposals belong to the same student, the parent |
| # key for all the proposals, which is the profile key corresponding |
| # to the student, will be the same. Hence it doesn't matter from which |
| # proposal we obtain the profile key from and we don't have to worry |
| # about the Python dictionary's ordering non-guarantees. We arbitrarily |
| # choose the first proposal in the dictionary keys() list to fetch the |
| # profile key from. |
| profile_key = ndb.Key(urlsafe=parsed.keys()[0]).parent() |
| |
| return proposal_logic.updateProposalRanks(profile_key, parsed) |
| |
| |
| def getProposalListForStudent(data, student_key): |
| """Returns an instance of ProposalList class that will list proposals |
| for the specified student. |
| |
| Args: |
| data: request_data.RequestData for the current request. |
| student_key: ndb.Key of the student. |
| |
| Returns: |
| The newly created ProposalList object. |
| """ |
| list_config = lists.ListConfiguration() |
| list_config.addSimpleColumn('title', 'Title') |
| list_config.addPlainTextColumn( |
| 'key', 'Key', |
| lambda entity, *args: entity.key.urlsafe()) |
| list_config.addPlainTextColumn( |
| 'rank', 'Rank', |
| lambda entity, *args: proposal_logic.getProposalRank(entity)) |
| list_config.setColumnEditable('rank', True) |
| list_config.addPlainTextColumn( |
| 'organization', 'Organization', |
| lambda entity, *args: entity.organization.get().name) |
| list_config.addDateColumn( |
| 'submitted_on', 'Submitted On', |
| lambda entity, *args: entity.created_on) |
| |
| list_config.setRowAction( |
| lambda entity, *args: links.LINKER.userId( |
| entity.key.parent(), entity.key.id(), |
| urls.UrlNames.PROPOSAL_REVIEW_AS_STUDENT)) |
| list_config.addPostEditButton('save', 'Save', '', [], refresh='none') |
| query = proposal_logic.queryProposalsForStudent(student_key) |
| |
| return ProposalList( |
| 'summerofcode/_list_component.html', data, list_config, |
| query, _PROPOSAL_LIST_FOR_STUDENT_DESCRIPTION) |
| |
| |
| def _red(text): |
| """Wraps the specified text in HTML so that it is rendered using red font.""" |
| return '<strong><font color="red">%s</font></strong>' % text |
| |
| |
| def _getStatusFunc(data): |
| """Returns a function to get value for 'status' column.""" |
| |
| def _getStatus(entity, cached_data, *args): |
| """Helper function to get value for 'status' column.""" |
| if entity.accept_as_project: |
| if ((data.is_developer or data.is_host or data.program.duplicates_visible) |
| and entity.key.parent().get().student_data.has_duplicates): |
| return _red(_DUPLICATE_STATUS) |
| elif entity.status == proposal_model.Status.WITHDRAWN: |
| return _red(_ACCEPTED_BUT_WITHDRAWN_STATUS) |
| elif not entity.mentors: |
| return _red(_ACCEPTED_BUT_NO_MENTOR_ASSIGNED_STATUS) |
| elif entity.is_ignored: |
| return _red(_ACCEPTED_BUT_IGNORED_STATUS) |
| elif (data.program.allocations_visible |
| and entity.key in cached_data.get(_PENDING_ACCEPTANCE_CACHE_KEY, [])): |
| return _red(_PENDING_ACCEPTANCE_STATUS) |
| else: |
| return str(entity.status) |
| elif entity.is_ignored: |
| return 'Ignored' |
| else: |
| return str(entity.status) |
| |
| return _getStatus |
| |
| |
| _PENDING_ACCEPTANCE_CACHE_KEY = 'pending_acceptance_key' |
| |
| class StatusColumnListPrefetcher(lists.ListPrefetcher): |
| """Prefetcher implementation that prefetches to-be-accepted proposals for |
| organizations. |
| """ |
| |
| def __init__(self, data): |
| """Initializes a new instance of the prefetcher. |
| |
| Args: |
| data: request_data.RequestData for the current request. |
| """ |
| self._data = data |
| |
| def prefetch(self, entities): |
| """See lists.ListPrefetcher.prefetch for specification.""" |
| if self._data.program.allocations_visible: |
| orgs = ndb.get_multi(set(entity.organization for entity in entities)) |
| |
| proposal_keys = set() |
| for org in orgs: |
| proposals = proposal_logic.getProposalsToBeAcceptedForOrg(org) |
| proposal_keys.update(proposal.key for proposal in proposals) |
| |
| return {_PENDING_ACCEPTANCE_CACHE_KEY: proposal_keys} |
| else: |
| return {} |
| |
| |
| def getProposalListForOrgMember(data, profile): |
| """Returns an instance of ProposalList class that will list proposals |
| for the specified organization member. |
| |
| Args: |
| data: request_data.RequestData for the current request. |
| profile: ndb.Key of the organization member. |
| |
| Returns: |
| The newly created ProposalList object. |
| """ |
| list_config = lists.ListConfiguration(add_key_column=False) |
| |
| list_config.addPlainTextColumn( |
| 'key', 'Key', |
| lambda entity, *args: entity.key.urlsafe(), |
| hidden=True) |
| list_config.addSimpleColumn('title', 'Title') |
| list_config.addPlainTextColumn( |
| 'student', 'Student', |
| lambda entity, *args: entity.key.parent().get().public_name) |
| list_config.addPlainTextColumn( |
| 'email', 'Student Email', |
| lambda entity, *args: entity.key.parent().get().contact.email, |
| hidden=True) |
| list_config.addHtmlColumn( |
| 'status', 'Status', _getStatusFunc(data), options=_STATUS_OPTIONS) |
| |
| # organization column is added only when the user is a mentor for |
| # more than one organization |
| if len(data.ndb_profile.mentor_for) > 1: |
| list_config.addPlainTextColumn( |
| 'organization', 'Organization', |
| lambda entity, *args: entity.organization.get().name) |
| |
| list_config.addNumericalColumn( |
| 'total_score', 'Total Score', lambda entity, *args: entity.total_score) |
| list_config.addNumericalColumn( |
| 'count_scores', 'Number of Scores', |
| lambda entity, *args: entity.count_scores) |
| |
| list_config.addNumericalColumn( |
| 'average_score', 'Average', _roundAverageScore) |
| |
| def getYourScore(entity, *args): |
| """Helper function to get value for 'your_score' column.""" |
| score = proposal_logic.getScoreForProfile(entity, data.ndb_profile.key) |
| return score.value if score else None |
| list_config.addNumericalColumn('your_score', 'Your Score', getYourScore) |
| |
| list_config.addDateColumn( |
| 'modified_on', 'Last modified', lambda entity, *args: entity.modified_on) |
| list_config.addDateColumn( |
| 'created_on', 'Created on', |
| lambda entity, *args: entity.created_on, hidden=True) |
| |
| def getMentors(entity, *args): |
| """Helper function to get value for 'mentors' column.""" |
| mentors = ndb.get_multi(entity.mentors) |
| return ', '.join(mentor.public_name for mentor in mentors) |
| list_config.addPlainTextColumn( |
| 'mentors', 'Assigned mentors', getMentors) |
| |
| def getPossibleMentors(entity, *args): |
| """Helper function to get value for 'possible_mentors' column.""" |
| possible_mentors = ndb.get_multi(entity.possible_mentors) |
| return ', '.join( |
| possible_mentor.public_name for possible_mentor in possible_mentors) |
| list_config.addPlainTextColumn( |
| 'possible_mentors', 'Possible mentors', |
| getPossibleMentors, hidden=True) |
| |
| list_config.setRowAction( |
| lambda entity, *args: links.LINKER.userId( |
| entity.key.parent(), entity.key.id(), |
| urls.UrlNames.PROPOSAL_REVIEW_AS_ORG_MEMBER)) |
| |
| def getExtraColumnFunc(column_id): |
| """Helper function to get a function to get value for an extra column.""" |
| def func(entity, *args): |
| return entity.extra_data.get(column_id, '') if entity.extra_data else '' |
| return func |
| |
| extra_columns = [] |
| orgs = ndb.get_multi(profile.mentor_for) |
| for org in orgs: |
| for column_id in org.extra_fields: |
| extra_columns.append(column_id) |
| |
| list_config.addPlainTextColumn( |
| column_id, cgi.escape(column_id), getExtraColumnFunc(column_id)) |
| list_config.setColumnEditable(column_id, True, 'text', {}) |
| |
| if extra_columns: |
| list_config.addPostEditButton( |
| SAVE_BUTTON_ID, _SAVE_BUTTON_LABEL, '', refresh='none') |
| |
| query = proposal_logic.queryProposalsForOrganizationMember(profile) |
| prefetcher = StatusColumnListPrefetcher(data) |
| |
| return ProposalList( |
| 'summerofcode/_list_component.html', data, list_config, |
| query, _PROPOSAL_LIST_FOR_ORG_MEMBER_DESCRIPTION, prefetcher=prefetcher) |
| |
| |
| def getProposalListForHost(data): |
| """Returns an instance of ProposalList class that will list proposals |
| for hosts for the program specified in the current request. |
| |
| Args: |
| data: request_data.RequestData for the current request. |
| |
| Returns: |
| The newly created ProposalList object. |
| """ |
| list_config = lists.ListConfiguration() |
| list_config.addSimpleColumn('title', 'Title') |
| list_config.addPlainTextColumn( |
| 'student', 'Student ID', |
| lambda entity, *args: profile_model.getUserId(entity.key.parent())) |
| list_config.addNumericalColumn( |
| 'total_score', 'Total Score', lambda entity, *args: entity.total_score) |
| list_config.addNumericalColumn( |
| 'count_scores', 'Number of Scores', |
| lambda entity, *args: entity.count_scores) |
| list_config.addNumericalColumn( |
| 'average_score', 'Average', _roundAverageScore) |
| list_config.addHtmlColumn( |
| 'status', 'Status', _getStatusFunc(data), options=_STATUS_OPTIONS) |
| |
| list_config.addDateColumn( |
| 'modified_on', 'Last modified', lambda entity, *args: entity.modified_on) |
| list_config.addDateColumn( |
| 'created_on', 'Created on', |
| lambda entity, *args: entity.created_on, hidden=True) |
| list_config.addSimpleColumn('accept_as_project', 'Accept As Project') |
| list_config.addPlainTextColumn( |
| 'org_id', 'Organization ID', |
| lambda entity, *args: org_model.getOrgId(entity.organization), |
| hidden=True) |
| |
| list_config.setRowAction( |
| lambda entity, *args: links.LINKER.userId( |
| entity.key.parent(), entity.key.id(), |
| urls.UrlNames.PROPOSAL_REVIEW_AS_ORG_MEMBER)) |
| |
| query = proposal_logic.queryProposalsForProgramHost(data.program.key()) |
| prefetcher = StatusColumnListPrefetcher(data) |
| |
| return ProposalList( |
| 'summerofcode/_list_component.html', data, list_config, |
| query, _PROPOSAL_LIST_FOR_ORG_MEMBER_DESCRIPTION, prefetcher=prefetcher) |