blob: f18a5c513d573c5e6343e3d5ac478a94c2cff9c4 [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.
"""Logic for surveys."""
from google.appengine.ext import ndb
from melange.utils import time
from summerofcode.models import survey as survey_model
PRE_PERIOD_STATE = 'pre_period'
IN_PERIOD_STATE = 'in_period'
POST_PERIOD_STATE = 'post_period'
PERIOD_STATES = [PRE_PERIOD_STATE, IN_PERIOD_STATE, POST_PERIOD_STATE]
# TODO(daniel): move Period to its own utility classes
class Period(object):
"""Class to represent relationship between the current moment and
the specified period.
A period is defined by two dates that stand for its beginning and end,
respectively. If at least one of these dates is absent, the period
is considered unbounded.
"""
def __init__(self, start=None, end=None):
"""Initializes new instance of this class with the specified start and
end dates.
Args:
start: start date of the period. May be None if unspecified.
end: end date of the period. May be None if unspecified.
"""
self.start = start
self.end = end
@property
def state(self):
"""Returns state of the period with respect to the current moment in time.
For the period bounded at both start and end,there are
three possibilities. The current moment may be:
- before the period
- in the period
- after the period
For a period with no start date defined, the current moment may be:
- in the period
- after the period
For period with no end date defined, the current moment may be:
- before the period
- in the period
Returns:
A constant representing the current state of the period. Can be one of
PRE_PERIOD_STATE, IN_PERIOD_STATE or POST_PERIOD_STATE.
"""
# unbounded period
if not self.start and not self.end:
return IN_PERIOD_STATE
# period right-unbounded
elif self.start and not self.end:
if time.isBefore(self.start):
return PRE_PERIOD_STATE
else:
return IN_PERIOD_STATE
# period left-unbounded
elif not self.start and self.end:
if time.isAfter(self.end):
return POST_PERIOD_STATE
else:
return IN_PERIOD_STATE
# period bounded
elif time.isBefore(self.start):
return PRE_PERIOD_STATE
elif time.isAfter(self.end):
return POST_PERIOD_STATE
else:
return IN_PERIOD_STATE
def getPersonalExtension(profile_key, survey_key):
"""Returns personal extension for the specified survey and profile.
Args:
profile_key: profile key.
survey_key: survey key.
Returns:
survey_model.PersonalExtension if an entity for the specified parameters
exists, None otherwise.
"""
# TODO(daniel): NDB migration
ndb_survey_key = ndb.Key.from_old_key(survey_key)
query = survey_model.PersonalExtension.query(
survey_model.PersonalExtension.survey == ndb_survey_key,
ancestor=profile_key)
return query.get()
def getSurveyActivePeriod(survey, extension=None):
"""Returns period during which the specified survey is active.
If no extension is specified, the period is simply defined by start and
end dates of the specified survey.
Otherwise, it is checked if the extension actually extends this period
in any direction. Specifically, if its start date comes before the survey
start date, the period of activeness starts when the extension starts.
Similarly, if the extension ends after the survey normally ends, the period
of activeness is extended to that point.
Args:
survey: survey entity
extension: optional extension for the survey
Returns:
Period object describing when the specified survey is active.
"""
if not extension:
period_start = survey.survey_start
period_end = survey.survey_end
else:
if not extension.start_date:
period_start = survey.survey_start
else:
period_start = min(survey.survey_start, extension.start_date)
if not extension.end_date:
period_end = survey.survey_end
else:
period_end = max(survey.survey_end, extension.end_date)
return Period(start=period_start, end=period_end)
def _isSurveyInPeriodStates(survey, profile_key, period_states):
"""Tells whether the specified survey is currently in one of the specified
period states for the specified profile.
Args:
survey: survey entity.
profile_key: profile key for which the survey state is checked.
period_states: list of allowed PERIOD_STATES.
Returns:
True, if the survey is currently in one of the specified period states.
False, otherwise.
"""
active_period = getSurveyActivePeriod(survey)
if active_period.state in period_states:
return True
else:
# try finding a personal extension for the student
extension = getPersonalExtension(profile_key, survey.key())
active_period = getSurveyActivePeriod(survey, extension=extension)
return active_period.state in period_states
def isSurveyActive(survey, profile_key):
"""Tells whether the specified survey is currently active for the specified
profile or not.
Args:
survey: survey entity.
profile_key: profile key for which the survey state is checked.
Returns:
True, if the survey is currently active. False, otherwise.
"""
return _isSurveyInPeriodStates(survey, profile_key, [IN_PERIOD_STATE])
def hasSurveyStarted(survey, profile_key):
"""Tells whether the specified survey has already started for the specified
profile or not.
Please not that the function returns True even if the survey is not
active anymore.
Args:
survey: survey entity.
profile_key: profile key for which the survey state is checked.
Returns:
True, if the survey has already started. False, otherwise.
"""
return _isSurveyInPeriodStates(
survey, profile_key, [IN_PERIOD_STATE, POST_PERIOD_STATE])
def createOrUpdatePersonalExtension(profile_key, survey_key, **kwargs):
"""Creates personal extension for the specified survey and profile.
In order to make sure that there is at most one personal extension between
a particular profile and a survey, this function should be run within
a transaction.
The result of this function is saved in the datastore.
Args:
profile_key: profile_key.
survey_key: survey key.
Returns:
newly created or updated personal extension entity.
"""
extension = getPersonalExtension(profile_key, survey_key)
if extension:
extension.populate(**kwargs)
else:
# TODO(daniel): NDB migration; key does not need to be translated
# when Profile and Survey models migrate to NDB
ndb_survey_key = ndb.Key.from_old_key(survey_key)
extension = survey_model.PersonalExtension(
parent=profile_key, survey=ndb_survey_key, **kwargs)
extension.put()
return extension