blob: 0c883400ff0b3d39ba4eb2e8eee522e8a25538e5 [file] [log] [blame]
# 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 containing the RequestData object that will be created for each
request.
"""
from google.appengine.api import users
from google.appengine.ext import db
from django import http
from django.core import urlresolvers
from melange import types
from melange.appengine import system
from melange.logic import settings as settings_logic
from melange.models import connection as connection_model
from melange.request import exception
from melange.request import links
from melange.utils import time
from melange.views.helper import urls
from soc.logic import program as program_logic
from soc.logic import site as site_logic
from soc.logic import user as user_logic
from soc.models import document as document_model
from soc.models import site as site_model
from soc.models import sponsor as sponsor_model
from soc.models import user as user_model
from soc.views.helper import access_checker
VIEW_AS_USER_DOES_NOT_EXIST = ('The requested user does not exist. Please go '
'to <a href="%s">User Settings</a> page and change the value.')
class TimelineHelper(object):
"""Helper class for the determination of the currently active period.
Methods ending with "On", "Start", or "End" return a date.
Methods ending with "Between" return a tuple with two dates.
Methods ending with neither return a Boolean.
"""
def __init__(self, timeline, org_app):
self.timeline = timeline
self.org_app = org_app
def currentPeriod(self):
"""Return where we are currently on the timeline."""
pass
def nextDeadline(self):
"""Determines the next deadline on the timeline."""
pass
def orgsAnnouncedOn(self):
return self.timeline.accepted_organization_announced_deadline
def beforeProgramStart(self):
"""Returns a bool indicating whether the program start date has passed
or not.
Returns:
True if he current data is before program start date; False otherwise
"""
return time.isBefore(self.timeline.program_start)
def afterProgramStart(self):
"""Returns a bool indicating whether the program start date has passed
or not.
Returns:
True if the current date is after program start date; False otherwise
"""
return time.isAfter(self.timeline.program_start)
def programActiveBetween(self):
return (self.timeline.program_start, self.timeline.program_end)
def orgSignupStart(self):
return self.org_app.survey_start if self.org_app else None
def orgSignupEnd(self):
return self.org_app.survey_end if self.org_app else None
def orgSignupBetween(self):
return (self.org_app.survey_start, self.org_app.survey_end) if \
self.org_app else (None, None)
def studentSignupStart(self):
return self.timeline.student_signup_start
def beforeStudentSignup(self):
"""Returns a bool indicating whether the student signup start date
has already passed or not.
Returns:
True if he current data is before student signup date; False otherwise
"""
return time.isBefore(self.studentSignupStart())
def studentSignupEnd(self):
return self.timeline.student_signup_end
def stopAllWorkDeadline(self):
return self.timeline.stop_all_work_deadline
def studentsSignupBetween(self):
return (self.timeline.student_signup_start,
self.timeline.student_signup_end)
def programActive(self):
start, end = self.programActiveBetween()
return time.isBetween(start, end)
def beforeOrgSignupStart(self):
return not self.org_app or time.isBefore(self.orgSignupStart())
def afterOrgSignupStart(self):
return self.org_app and time.isAfter(self.orgSignupStart())
def orgSignup(self):
if not self.org_app:
return False
start, end = self.orgSignupBetween()
return time.isBetween(start, end)
def orgsAnnounced(self):
return time.isAfter(self.orgsAnnouncedOn())
def beforeStudentSignupStart(self):
return time.isBefore(self.studentSignupStart())
def afterStudentSignupStart(self):
return time.isAfter(self.studentSignupStart())
def studentSignup(self):
start, end = self.studentsSignupBetween()
return time.isBetween(start, end)
def afterStudentSignupEnd(self):
return time.isAfter(self.studentSignupEnd())
def afterStopAllWorkDeadline(self):
return time.isAfter(self.stopAllWorkDeadline())
def surveyPeriod(self, survey):
start = survey.survey_start
end = survey.survey_end
return time.isAfter(start) and time.isBefore(end)
def afterSurveyStart(self, survey):
return time.isAfter(survey.survey_start)
def afterSurveyEnd(self, survey):
return time.isAfter(survey.survey_end)
class RequestData(object):
"""Object containing data we query for each request.
Fields:
site: the singleton site.Site entity
user: the user entity (if logged in)
profile: the profile entity
program: the program entity
request: the request object (as provided by django)
args: the request args (as provided by djang)
kwargs: the request kwargs (as provided by django)
path: the url of the current query, encoded as utf-8 string
full_path: same as path, but including any GET args
GET: the GET dictionary (from the request object)
POST: the POST dictionary (from the request object)
is_developer: is the current user a developer
is_host: is the current user a host of the program
gae_user: the Google Appengine user object
timeline: the timeline helper
models: types.Models implementation defining model classes
Optional fields (may not be specified for all requests):
url_connection: connection entity for the data in kwargs.
url_org: organization entity for the data in kwargs.
url_profile: profile entity for the data in kwargs.
url_user: user entity for the data in kwargs.
"""
# class attribute which is assigned to all fields which have not been set
_unset = object()
def __init__(self, request, args, kwargs):
"""Constructs a new RequestData object.
Args:
request: Django HTTPRequest object.
args: The args that Django sends along with the request.
kwargs: The kwargs that Django sends along with the request.
"""
self.request = request
self.args = args
self.kwargs = kwargs
self.models = types.MELANGE_MODELS
self._redirect = self._unset
self._site = self._unset
self._sponsor = self._unset
self._user = self._unset
self._profile = self._unset
self._program = self._unset
self._GET = self._unset
self._POST = self._unset
# TODO(daniel): check if this field is really used
self._path = self._unset
self._is_developer = self._unset
self._is_host = self._unset
self._gae_user = self._unset
self._css_path = self._unset
self._ds_write_disabled = self._unset
self._timeline = self._unset
self._url_connection = self._unset
self._url_org = self._unset
self._url_ndb_org = self._unset
self._url_profile = self._unset
self._url_student_info = self._unset
self._url_user = self._unset
self._document = self._unset
# explicitly copy POST and GET dictionaries so they can be modified
# the default QueryDict objects used by Django are immutable, but their
# copies may be modified
# TODO(daniel): these dictionaries should not be modified in the first
# place, so these copying must be eventually eliminated
if self.request:
self.request.POST = self.request.POST.copy()
self.request.GET = self.request.GET.copy()
def _isSet(self, value):
"""Checks whether the specified field has been set or not.
Args:
value: the specified value of one of the fields supported by this class.
Returns:
True if the value is set or False otherwise.
"""
return value is not self._unset
@property
def css_path(self):
"""Returns the css_path property."""
if not self._isSet(self._css_path):
# TODO(daniel): this should not return gsoc in module
# I believe css_path is needed in the main module because of a few sites
# that are not specific to any module, like site or host
self._css_path = 'gsoc'
return self._css_path
@property
def gae_user(self):
"""Returns the gae_user property."""
if not self._isSet(self._gae_user):
self._gae_user = users.get_current_user()
return self._gae_user
@property
def is_developer(self):
"""Returns the is_developer field."""
if not self._isSet(self._is_developer):
if users.is_current_user_admin():
self._is_developer = True
elif self.user and self.user.is_developer:
self._is_developer = True
else:
self._is_developer = False
return self._is_developer
@property
def is_host(self):
"""Returns the is_host field."""
if not self._isSet(self._is_host):
if not self.user:
self._is_host = False
elif 'sponsor' in self.kwargs:
key = db.Key.from_path('Sponsor', self.kwargs.get('sponsor'))
self._is_host = key in self.user.host_for
else:
key = program_logic.getSponsorKey(self.program)
self._is_host = key in self.user.host_for
return self._is_host
@property
def path(self):
"""Returns the path field."""
if not self._isSet(self._path):
self._path = self.request.path.encode('utf-8')
return self._path
@property
def redirect(self):
"""Returns the redirect helper."""
if not self._isSet(self._redirect):
self._redirect = RedirectHelper(self)
return self._redirect
@property
def site(self):
"""Returns the site field."""
if not self._isSet(self._site):
# XSRF middleware might have already retrieved it for us
if not hasattr(self.request, 'site'):
# populate site.Site singleton to request field
self.request.site = site_logic.singleton()
self._site = self.request.site
return self._site
@property
def sponsor(self):
"""Returns the sponsor field."""
if not self._isSet(self._sponsor):
if self.kwargs.get('sponsor'):
sponsor_key = db.Key.from_path('Sponsor', self.kwargs['sponsor'])
else:
# In this case sponsor was not in the URL. Anyway, it may be still
# possible to retrieve a reasonable sponsor, if a default program
# is set for the site.
# It is not the most efficient way to access the sponsor itself,
# because it requires a program to be fetched first. It seems to
# be acceptable, because there is a great chance that program has
# to also be provided at some point of request's life cycle.
sponsor_key = program_logic.getSponsorKey(self.program)
self._sponsor = sponsor_model.Sponsor.get(sponsor_key)
return self._sponsor
@property
def user(self):
"""Returns the user field."""
if not self._isSet(self._user):
self._user = user_logic.current()
# developer may view the page as another user
if self._user and user_logic.isDeveloper(user=self._user):
settings = settings_logic.getUserSettings(self._user.key())
if settings.view_as is not None:
user = user_model.User.get(settings.view_as.to_old_key())
if user:
self._user = user
else:
# TODO(daniel): use main LINKER object when merged
linker = links.Linker()
user_settings_url = linker.user(
self._user, urls.UrlNames.USER_SETTINGS)
raise exception.BadRequest(
message=VIEW_AS_USER_DOES_NOT_EXIST % user_settings_url)
return self._user
@property
def profile(self):
"""Returns the profile property."""
if not self._isSet(self._profile):
if not self.user or not self.program:
self._profile = None
else:
key_name = '%s/%s' % (self.program.key().name(), self.user.link_id)
self._profile = self.models.profile_model.get_by_key_name(
key_name, parent=self.user)
return self._profile
@property
def program(self):
"""Returns the program field."""
if not self._isSet(self._program):
self._getProgramWideFields()
return self._program
@property
def GET(self):
"""Returns the GET dictionary associated with the processed request."""
if not self._isSet(self._GET):
self._GET = self.request.GET
return self._GET
@property
def POST(self):
"""Returns the POST dictionary associated with the processed request."""
if not self._isSet(self._POST):
self._POST = self.request.POST
return self._POST
@property
def ds_write_disabled(self):
"""Memoizes and returns whether datastore writes are disabled."""
if not self._isSet(self._ds_write_disabled):
if self.request.method == 'GET':
value = self.request.GET.get('dsw_disabled', '')
if value.isdigit() and int(value) == 1:
self._ds_write_disabled = True
if not self._isSet(self._ds_write_disabled):
self._ds_write_disabled = not db.WRITE_CAPABILITY.is_enabled()
return self._ds_write_disabled
@property
def timeline(self):
"""Returns the timeline field."""
if not self._isSet(self._timeline):
self._timeline = TimelineHelper(self.program_timeline, None)
return self._timeline
@property
def url_connection(self):
"""Returns url_connection property.
This property represents connection entity corresponding to profile whose
identifier is a part of the URL of the processed request. Numerical
identifier of the connection is also a part of the URL.
Returns:
Retrieved connection entity.
Raises:
exception.BadRequest: if some data is missing in the current request.
exception.NotFound: if no entity is found.
"""
if not self._isSet(self._url_connection):
try:
connection_key = db.Key.from_path('Connection', int(self.kwargs['id']),
parent=self._getUrlProfileKey())
except KeyError:
raise exception.BadRequest(
message='The request does not contain connection id.')
self._url_connection = connection_model.Connection.get(connection_key)
if not self._url_connection:
raise exception.NotFound(
message='Requested connection does not exist.')
return self._url_connection
@property
def url_profile(self):
"""Returns url_profile property.
This property represents profile entity for a person whose identifier
is a part of the URL of the processed request for the program whose
identifier is also a part of the URL.
Returns:
Retrieved profile entity.
Raises:
exception.UserError: if no profile entity is found.
"""
if not self._isSet(self._url_profile):
self._url_profile = self.models.profile_model.get(
self._getUrlProfileKey())
if not self._url_profile:
raise exception.NotFound(message='Requested profile does not exist.')
return self._url_profile
@property
def url_student_info(self):
if not self._isSet(self._url_student_info):
self._url_student_info = self.url_profile.student_info
if not self._url_student_info:
raise exception.NotFound(message='Requested profile is not a student.')
return self._url_student_info
@property
def url_org(self):
"""Returns url_org property.
This property represents organization entity whose identifier is a part
of the URL of the processed request.
Returns:
Retrieved organization entity.
Raises:
exception.BadRequest: if the current request does not contain any
organization data.
exception.NotFound: if the organization is not found.
"""
if not self._isSet(self._url_org):
try:
fields = ['sponsor', 'program', 'organization']
key_name = '/'.join(self.kwargs[i] for i in fields)
except KeyError:
raise exception.BadRequest(
message='The request does not contain full organization data.')
self._url_org = self.models.org_model.get_by_key_name(key_name)
if not self._url_org:
raise exception.NotFound(
message='Requested organization does not exist.')
return self._url_org
# TODO(daniel): rename this to url_org when Organization is converted to NDB
@property
def url_ndb_org(self):
"""Returns url_org property.
This property represents organization entity whose identifier is a part
of the URL of the processed request.
Returns:
Retrieved organization entity.
Raises:
exception.BadRequest: if the current request does not contain any
organization data.
exception.NotFound: if the organization is not found.
"""
if not self._isSet(self._url_ndb_org):
try:
fields = ['sponsor', 'program', 'organization']
entity_id = '/'.join(self.kwargs[i] for i in fields)
except KeyError:
raise exception.BadRequest(
message='The request does not contain full organization data.')
self._url_ndb_org = self.models.ndb_org_model.get_by_id(entity_id)
if not self._url_ndb_org:
raise exception.NotFound(
message='Requested organization does not exist.')
return self._url_ndb_org
@property
def url_user(self):
"""Returns url_user property.
This property represents user entity for a person whose identifier
is a part of the URL of the processed request.
Returns:
Retrieved user entity.
Raises:
exception.BadRequest: if the current request does not contain
any user data.
exception.NotFound: if the user is not found.
"""
if not self._isSet(self._url_user):
key_name = self.kwargs.get('user')
if not key_name:
raise exception.BadRequest(
message='The request does not contain user data.')
self._url_user = user_model.User.get_by_key_name(key_name)
if not self._url_user:
raise exception.NotFound(message='Requested user does not exist.')
return self._url_user
# TODO(daniel): rename it to url_document
@property
def document(self):
"""Returns document property."""
if not self._isSet(self._document):
fields = []
kwargs = self.kwargs.copy()
prefix = kwargs.pop('prefix', None)
fields.append(prefix)
if prefix in ['gsoc_program', 'gsoc_org', 'gci_program', 'gci_org']:
fields.append(kwargs.pop('sponsor', None))
fields.append(kwargs.pop('program', None))
if prefix in ['gsoc_org', 'gci_org']:
fields.append(kwargs.pop('organization', None))
fields.append(kwargs.pop('document', None))
if any(kwargs.values()):
raise exception.BadRequest(message="Unexpected value for document url")
if not all(fields):
raise exception.BadRequest(message="Missing value for document url")
# TODO(daniel): remove key_name from it.
self.key_name = '/'.join(fields)
self._document = document_model.Document.get_by_key_name(self.key_name)
return self._document
def _getUrlProfileKey(self):
"""Returns db.Key that represents profile for the data specified in
the URL of the current request.
Returns:
db.Key of the profile for data specified in the URL of the
current request.
Raises:
exception.BadRequest: if some data is missing in the current request.
"""
try:
fields = ['sponsor', 'program', 'user']
profile_key_name = '/'.join(self.kwargs[i] for i in fields)
return db.Key.from_path(
'User', self.kwargs['user'], self.models.profile_model.kind(),
profile_key_name)
except KeyError:
raise exception.BadRequest(
message='The request does not contain full profile data.')
def _getProgramWideFields(self):
"""Fetches program wide fields in a single database round-trip."""
keys = []
# add program's key
if self.kwargs.get('sponsor') and self.kwargs.get('program'):
program_key_name = "%s/%s" % (
self.kwargs['sponsor'], self.kwargs['program'])
program_key = db.Key.from_path(
self.models.program_model.kind(), program_key_name)
else:
program_key = site_model.Site.active_program.get_value_for_datastore(
self.site)
program_key_name = program_key.name()
keys.append(program_key)
# add timeline's key
keys.append(db.Key.from_path(
self.models.timeline_model.kind(), program_key_name))
# add org_app's key
org_app_key_name = '%s/%s/orgapp' % (
self.models.program_model.prefix, program_key_name)
keys.append(db.Key.from_path('OrgAppSurvey', org_app_key_name))
self._program, self._program_timeline, self._org_app = db.get(keys)
# raise an exception if no program is found
if not self._program:
raise exception.NotFound(
message="There is no program for url '%s'" % program_key_name)
# TODO(nathaniel): This should be immutable.
class RedirectHelper(object):
"""Helper for constructing redirects."""
def __init__(self, data):
"""Initializes the redirect helper."""
self._data = data
self._response = http.HttpResponse()
self._clear()
def _clear(self):
"""Clears the internal state."""
self._no_url = False
self._url_name = None
self._url = None
self.args = []
self.kwargs = {}
def program(self, program=None):
"""Sets kwargs for an url_patterns.PROGRAM redirect."""
if not program:
assert access_checker.isSet(self._data.program)
program = self._data.program
self._clear()
self.kwargs['sponsor'] = program_logic.getSponsorKey(program).name()
self.kwargs['program'] = program.link_id
def organization(self, organization=None):
"""Sets the kwargs for an url_patterns.ORG redirect."""
if not organization:
assert access_checker.isSet(self._data.organization)
organization = self._data.organization
self.program()
self.kwargs['organization'] = organization.link_id
# TODO(daniel): id built-in function should not be shadowed
def id(self, id=None):
"""Sets the kwargs for an url_patterns.ID redirect."""
if not id:
assert 'id' in self._data.kwargs
id = self._data.kwargs['id']
self.program()
self.kwargs['id'] = id
return self
def key(self, key=None):
"""Sets the kwargs for an url_patterns.KEY redirect."""
if not key:
assert 'key' in self._data.kwargs
key = self._data.kwargs['key']
self.program()
self.kwargs['key'] = key
return self
def createProfile(self, role):
"""Sets args for an url_patterns.CREATE_PROFILE redirect."""
self.program()
self.kwargs['role'] = role
return self
def profile(self, user=None):
"""Sets args for an url_patterns.PROFILE redirect."""
if not user:
assert 'user' in self._data.kwargs
user = self._data.kwargs['user']
self.program()
self.kwargs['user'] = user
return self
def document(self, document):
"""Sets args for an url_patterns.DOCUMENT redirect.
If document is not set, a call to url() will return None.
"""
self._clear()
if not document:
self._no_url = True
return self
if isinstance(document, db.Model):
key = document.key()
else:
key = document
self.args = key.name().split('/')
return self
def userOrg(self, user=None, organization=None):
"""Sets args for an url_patterns.USER_ORG redirect."""
if not user:
assert 'user' in self._data.kwargs
user = self._data.kwargs['user']
if not organization:
assert access_checker.isSet(self._data.organization)
organization = self._data.organization
self.program()
self.kwargs['user'] = user
self.kwargs['organization'] = organization.link_id
return self
# TODO(daniel): id built-in function should not be shadowed
def userId(self, user=None, id=None):
"""Sets args for url_patterns.USER_ID redirect."""
if not user:
assert 'user' in self._data.kwargs
user = self._data.kwargs['user']
if not id:
assert 'id' in self._data.kwargs
id = self._data.kwargs['id']
self.program()
self.kwargs['user'] = user
self.kwargs['id'] = id
return self
def urlOf(self, name, full=False, secure=False, extra=None):
"""Returns the resolved url for name.
Uses internal state for args and kwargs.
"""
# TODO(nathaniel): Why isn't this just "url = reverse(name, args=self.args,
# kwargs=self.kwargs)"? Current suspicion: it's because there's a
# there's a difference in behavior between passing None and passing empty
# dicts. It's also curious that there isn't an "if self.args and
# self.kwargs" case at the top.
if self.args:
url = urlresolvers.reverse(name, args=self.args)
elif self.kwargs:
url = urlresolvers.reverse(name, kwargs=self.kwargs)
else:
url = urlresolvers.reverse(name)
url = self._appendGetArgs(url, extra_get_args=extra if extra else [])
return self._fullUrl(url, full, secure)
def url(self, full=False, secure=False):
"""Returns the url of the current state."""
if self._no_url:
return None
assert self._url or self._url_name
if self._url:
return self._fullUrl(self._url, full, secure)
return self.urlOf(self._url_name, full=full, secure=secure)
def _fullUrl(self, url, full, secure):
"""Returns the full version of the url iff full.
The full version starts with http:// and includes getHostname().
"""
if (not full) and (system.isLocal() or not secure):
return url
# TODO(nathaniel): consider using scheme-relative urls here?
if secure:
protocol = 'https'
hostname = system.getSecureHostname()
else:
protocol = 'http'
hostname = site_logic.getHostname(self._data)
return '%s://%s%s' % (protocol, hostname, url)
# TODO(nathaniel): Django's got to have a utility function for most of this.
def _appendGetArgs(
self, url, validated=False, extra_get_args=None):
"""Appends GET arguments to the specified URL."""
get_args = extra_get_args or []
if validated:
get_args.append('validated')
if get_args:
# check if the url has already a question mark
if url.find('?') == -1:
url = url + '?'
else:
url = url + '&'
# append all the GET arguments at the end of the URL
if get_args:
url = url + '&'.join(get_args)
return url
def to(self, name=None, validated=False, full=False, secure=False,
extra=None, anchor=None):
"""Redirects to the resolved url for name.
Uses internal state for args and kwargs.
Args:
name: Name of the URL pattern
validated: If set to True will add &validated to GET arguments
full: Whether the URL should include the protocol
secure: Whether the protocol of the URL should be set to HTTPS
extra: List of additional arguments that will be added as GET arguments
Returns:
An http.HttpResponse object redirecting to the appropriate url.
"""
if self._url:
url = self._url
else:
assert name or self._url_name
url = self.urlOf(name or self._url_name)
if anchor:
url = '%s#%s' % (url, anchor)
url = self._appendGetArgs(
url, validated=validated, extra_get_args=extra if extra else [])
return self.toUrl(url, full=full, secure=secure)
def toUrl(self, url, full=False, secure=False):
"""Redirects to the specified url.
Args:
url: A URL.
full: Whether or not to prefer use of a full URL including
protocol. This parameter is not necessarily binding.
secure: Whether or not to prefer use of a secure URL.
Returns:
An http.HttpResponse object redirecting to the given url.
"""
url = self._fullUrl(url, full, secure)
return http.HttpResponseRedirect(url)
def acceptedOrgs(self):
"""Sets the _url_name to the list of all accepted orgs."""
self.program()
return self
def homepage(self, program=None):
"""Sets the _url_name for the homepage of the current program.
Args:
program: the link_id of the program for which we need to get the homepage
"""
self.program(program)
return self
def searchpage(self):
"""Sets the _url_name for the searchpage of the current program."""
self.program()
return self
def orgHomepage(self, link_id):
"""Sets the _url_name for the specified org homepage."""
self.program()
self.kwargs['organization'] = link_id
return self
def dashboard(self):
"""Sets the _url_name for dashboard page of the current program."""
self.program()
return self
def events(self):
"""Sets the _url_name for the events page, if it is set."""
self.program()
return self
def staticContent(self, content_id):
"""Sets the args for url_patterns.STATIC_CONTENT redirect."""
self.program()
self.kwargs['content_id'] = content_id
return self