blob: 254b85f9cf489863f4d7fb53b7a1dc99ab002d80 [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.
"""Module for templates with proposal lists."""
import cgi
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 links
from soc.views import template
from soc.views.helper import lists
from summerofcode.logic import proposal as proposal_logic
from summerofcode.mapreduce import proposal_acceptance
from summerofcode.models import proposal as proposal_model
from summerofcode.views.helper import urls
_PROPOSAL_LIST_TITLE = translation.ugettext('Proposals')
_PROPOSAL_LIST_FOR_HOST_DESCRIPTION = translation.ugettext(
'List of proposals which have been submitted to the program')
_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')
_PENDING_ACCEPTANCE_LABEL = translation.ugettext('Pending Acceptance')
_ACCEPTED_REGEX = '%s' % proposal_model.Status.ACCEPTED
_ALL_REGEX = ''
_DUPLICATE_REGEX = '%s' % _DUPLICATE_STATUS
_IGNORED_REGEX = '%s' % proposal_model.Status.IGNORED
_PENDING_ACCEPTANCE_REGEX = '%s' % _PENDING_ACCEPTANCE_STATUS
_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),
str(proposal_model.Status.ACCEPTED),
str(proposal_model.Status.REJECTED),
_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),
(_PENDING_ACCEPTANCE_REGEX, _PENDING_ACCEPTANCE_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, last_update=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.
last_update: Optional datetime.datetime object that specifies when the
data in the list was updated most recently.
"""
super(ProposalList, self).__init__(data)
self.template_path = template_path
self._list_config = list_config
self._query = query
self._description = description
self._prefetcher = prefetcher
self._last_update = last_update
def context(self):
"""See template.Template.context for specification."""
list_configuration_response = lists.ListConfigurationResponse(
self.data, self._list_config, 0, self._description)
return {
'title': _PROPOSAL_LIST_TITLE,
'lists': [list_configuration_response],
'last_update': self._last_update,
}
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 _red(text):
"""Wraps the specified text in HTML so that it is rendered in red font."""
return '<strong><font color="red">%s</font></strong>' % text
def _green(text):
"""Wraps the specified text in HTML so that it is rendered in green font."""
return '<strong><font color="green">%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.status == proposal_model.Status.ACCEPTED:
return _green(str(proposal_model.Status.ACCEPTED))
elif entity.status == proposal_model.Status.REJECTED:
return _red(str(proposal_model.Status.REJECTED))
elif entity.accept_as_project:
student = entity.key.parent().get()
if 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)
# the proposal was marked as "to be accepted" by the job
elif entity.key in student.student_data.to_be_accepted_proposals:
# the student has another "to be accepted" proposal, so a duplicate
# is reported
if student.student_data.has_duplicates:
if (data.is_developer or data.is_host or
data.program.duplicates_visible):
return _red(_DUPLICATE_STATUS)
# no duplicates! this proposal is in "pending acceptance" status
else:
if (data.is_developer or data.is_host or
data.program.allocations_visible):
return _green(_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.addPlainTextColumn(
'accept', 'Accept', lambda entity, *args: entity.accept_as_project)
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)
job_status = proposal_acceptance.getSetToBeAcceptedProposalsJobStatus(
data.program.key())
return ProposalList(
'summerofcode/_list_component.html', data, list_config,
query, _PROPOSAL_LIST_FOR_ORG_MEMBER_DESCRIPTION, prefetcher=prefetcher,
last_update=job_status.started_on if job_status else None)
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.addNumericalColumn(
'median_score', 'Median Score',
lambda entity, *args: entity.median_score, hidden=True)
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_HOST_DESCRIPTION, prefetcher=prefetcher)