blob: 06b87b640c80643c4248c6fef8a2ab18f4f53de2 [file] [log] [blame]
# Copyright 2011 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 containing the view for GCI invitation page."""
import logging
from django.utils.translation import ugettext
from google.appengine.api import users
from google.appengine.ext import db
from soc.logic import accounts
from soc.logic import cleaning
from soc.logic import invite as invite_logic
from soc.logic.exceptions import AccessViolation
from soc.logic.exceptions import BadRequest
from soc.logic.exceptions import NotFound
from soc.logic.helper import notifications
from soc.models.user import User
from soc.views.helper import lists
from soc.views.helper import url_patterns
from soc.views.helper.access_checker import isSet
from soc.views.template import Template
from soc.tasks import mailer
from soc.modules.gci.models.profile import GCIProfile
from soc.modules.gci.models.request import GCIRequest
from soc.modules.gci.views import forms as gci_forms
from soc.modules.gci.views.base import GCIRequestHandler
from soc.modules.gci.views.helper import url_names
from soc.modules.gci.views.helper.url_patterns import url
USER_DOES_NOT_EXIST = ugettext('User %s not found.')
USER_ALREADY_INVITED = ugettext(
'User %s has already been invited to become %s.')
USER_HAS_NO_PROFILE = ugettext(
'User %s must create a profile for this program.')
USER_IS_STUDENT = ugettext('User %s is a student for this program.')
USER_ALREADY_HAS_ROLE = ugettext(
'User %s already has %s role for this program.')
class InviteForm(gci_forms.GCIModelForm):
"""Django form for the invite page.
"""
identifiers = gci_forms.CharField(label='Username/Email')
class Meta:
model = GCIRequest
css_prefix = 'gci_intivation'
fields = ['message']
def __init__(self, request_data, *args, **kwargs):
super(InviteForm, self).__init__(*args, **kwargs)
# store request object to cache results of queries
self.request_data = request_data
# reorder the fields so that link_id is the first one
field = self.fields.pop('identifiers')
self.fields.insert(0, 'identifiers', field)
field.help_text = ugettext(
"Comma separated usernames or emails of the people to invite.")
def clean_identifiers(self):
"""Accepts link_ids or email addresses of users which may be invited.
"""
assert isSet(self.request_data.organization)
users_to_invite = []
identifiers = self.cleaned_data.get('identifiers', '').split(',')
for identifier in identifiers:
self.cleaned_data['identifier'] = identifier.strip()
user = self._clean_identifier(identifier)
users_to_invite.append(user)
self.request_data.users_to_invite = users_to_invite
def _clean_identifier(self, identifier):
user_to_invite = None
# first check if the field represents a valid link_id
try:
existing_user_cleaner = cleaning.clean_existing_user('identifier')
user_to_invite = existing_user_cleaner(self)
except gci_forms.ValidationError, e:
if e.code != 'invalid':
raise
# otherwise check if the field represents a valid email address
email_cleaner = cleaning.clean_email('identifier')
try:
email = email_cleaner(self)
except gci_forms.ValidationError, e:
if e.code != 'invalid':
raise
msg = ugettext(u'Enter a valid link_id or email address.')
raise gci_forms.ValidationError(msg, code='invalid')
account = users.User(email)
user_account = accounts.normalizeAccount(account)
user_to_invite = User.all().filter('account', user_account).get()
# check if the user entity has been found
if not user_to_invite:
raise gci_forms.ValidationError(USER_DOES_NOT_EXIST % (identifier))
# check if the organization has already sent an invitation to the user
query = self._getQueryForExistingRequests(user_to_invite)
if query.get():
role = self.request_data.kwargs['role']
raise gci_forms.ValidationError(
USER_ALREADY_INVITED % (identifier, role))
# check if the user that is invited does not have the role
# TODO(dhans): in the ideal world, we want to invite Users with no profiles
profile = self.request_data.invite_profile \
= self._getProfile(user_to_invite)
if not profile:
raise gci_forms.ValidationError(USER_HAS_NO_PROFILE % identifier)
if profile.student_info:
raise gci_forms.ValidationError(USER_IS_STUDENT % identifier)
if self.request_data.kwargs['role'] == 'org_admin':
role_for = profile.org_admin_for
else:
role_for = set(profile.org_admin_for + profile.mentor_for)
if self.request_data.organization.key() in role_for:
role = self.request_data.kwargs['role']
raise gci_forms.ValidationError(
USER_ALREADY_HAS_ROLE % (identifier, role))
return user_to_invite
def _getQueryForExistingRequests(self, user_to_invite):
query = db.Query(GCIRequest, keys_only=True)
query.filter('type', 'Invitation')
query.filter('user', user_to_invite)
query.filter('role', self.request_data.kwargs['role'])
query.filter('org', self.request_data.organization)
return query
def _getProfile(self, user_to_invite):
key_name = '/'.join([
self.request_data.program.key().name(), user_to_invite.link_id])
return GCIProfile.get_by_key_name(key_name, parent=user_to_invite)
class ManageInviteForm(gci_forms.GCIModelForm):
"""Django form for the manage invitation page.
"""
class Meta:
model = GCIRequest
css_prefix = 'gci_intivation'
fields = ['message']
class InvitePage(GCIRequestHandler):
"""Encapsulate all the methods required to generate Invite page."""
def templatePath(self):
return 'v2/modules/gci/invite/base.html'
def djangoURLPatterns(self):
return [
url(r'invite/%s$' % url_patterns.INVITE,
self, name=url_names.GCI_SEND_INVITE)
]
def checkAccess(self, data, check, mutator):
"""Access checks for GCI Invite page."""
check.isProgramVisible()
check.isOrgAdmin()
def context(self, data, check, mutator):
"""Handler to for GCI Invitation Page HTTP get request."""
role = 'Org Admin' if data.kwargs['role'] == 'org_admin' else 'Mentor'
invite_form = InviteForm(data, data.POST or None)
return {
'logout_link': data.redirect.logout(),
'page_name': 'Invite a new %s' % role,
'program': data.program,
'forms': [invite_form]
}
def validate(self, data):
"""Creates new invitation based on the data inserted in the form.
Args:
data: A RequestHandler describing the current request.
Returns:
True if the new invitations have been successfully saved; False otherwise
"""
assert isSet(data.organization)
invite_form = InviteForm(data, data.POST)
if not invite_form.is_valid():
return False
assert isSet(data.users_to_invite)
assert len(data.users_to_invite)
# create a new invitation entity
invite_form.cleaned_data['org'] = data.organization
invite_form.cleaned_data['role'] = data.kwargs['role']
invite_form.cleaned_data['type'] = 'Invitation'
def create_invite_txn(user):
invite = invite_form.create(commit=True, parent=user)
context = notifications.inviteContext(data, invite)
sub_txn = mailer.getSpawnMailTaskTxn(context, parent=invite)
sub_txn()
return invite
for user in data.users_to_invite:
invite_form.instance = None
invite_form.cleaned_data['user'] = user
db.run_in_transaction(create_invite_txn, user)
return True
def post(self, data, check, mutator):
"""Handler to for GCI Invitation Page HTTP post request."""
if self.validate(data):
return data.redirect.dashboard().to()
else:
# TODO(nathaniel): problematic self-call.
return self.get(data, check, mutator)
class ManageInvite(GCIRequestHandler):
"""View to manage the invitation by organization admins."""
def templatePath(self):
return 'v2/modules/gci/invite/base.html'
def djangoURLPatterns(self):
return [
url(r'invite/manage/%s$' % url_patterns.USER_ID, self,
name=url_names.GCI_MANAGE_INVITE)
]
def checkAccess(self, data, check, mutator):
check.isProfileActive()
key_name = data.kwargs['user']
user_key = db.Key.from_path('User', key_name)
invite_id = int(data.kwargs['id'])
data.invite = GCIRequest.get_by_id(invite_id, parent=user_key)
check.isInvitePresent(invite_id)
# get invited user and check if it is not deleted
data.invited_user = data.invite.user
if not data.invited_user:
logging.warning(
'User entity does not exist for request with id %s', invite_id)
raise NotFound('Invited user does not exist')
# get the organization and check if the current user can manage the invite
data.organization = data.invite.org
check.isOrgAdmin()
if data.POST:
if 'withdraw' in data.POST:
check.canInviteBeWithdrawn()
elif 'resubmit' in data.POST:
check.canInviteBeResubmitted()
else:
raise BadRequest('No action specified in manage_gci_invite request.')
def context(self, data, check, mutator):
page_name = self._constructPageName(data)
form = ManageInviteForm(data.POST or None, instance=data.invite)
button_name = self._constructButtonName(data)
button_value = self._constructButtonValue(data)
return {
'page_name': page_name,
'forms': [form],
'button_name': button_name,
'button_value': button_value
}
def post(self, data, check, mutator):
# it is needed to handle notifications
data.invited_profile = self._getInvitedProfile(data)
if 'withdraw' in data.POST:
invite_logic.withdrawInvite(data)
elif 'resubmit' in data.POST:
invite_logic.resubmitInvite(data)
return data.redirect.userId().to(url_names.GCI_MANAGE_INVITE)
def _constructPageName(self, data):
invite = data.invite
return "%s Invite For %s" % (invite.role, data.invited_user.name)
def _constructButtonName(self, data):
invite = data.invite
if invite.status == 'pending':
return 'withdraw'
if invite.status in ['withdrawn', 'rejected']:
return 'resubmit'
def _constructButtonValue(self, data):
invite = data.invite
if invite.status == 'pending':
return 'Withdraw'
if invite.status in ['withdrawn', 'rejected']:
return 'Resubmit'
def _getInvitedProfile(self, data):
key_name = '/'.join([
data.program.key().name(),
data.invited_user.link_id])
return GCIProfile.get_by_key_name(key_name, parent=data.invited_user)
class RespondInvite(GCIRequestHandler):
"""View to respond to the invitation by the user."""
def templatePath(self):
return 'v2/modules/gci/invite/show.html'
def djangoURLPatterns(self):
return [
url(r'invite/respond/%s$' % url_patterns.ID, self,
name=url_names.GCI_RESPOND_INVITE)
]
def checkAccess(self, data, check, mutator):
check.isUser()
invite_id = int(data.kwargs['id'])
data.invite = GCIRequest.get_by_id(invite_id, parent=data.user)
check.isInvitePresent(invite_id)
check.canRespondInvite()
data.is_respondable = data.invite.status == 'pending'
# actual response may be sent only to pending requests
if data.POST:
if 'accept' not in data.POST and 'reject' not in data.POST:
raise BadRequest('Valid action is not specified in the request.')
check.isInviteRespondable()
def context(self, data, check, mutator):
page_name = self._constructPageName(data)
return {
'is_respondable': data.is_respondable,
'page_name': page_name,
'request': data.invite
}
def post(self, data, check, mutator):
if 'accept' in data.POST:
if not data.profile:
# TODO(nathaniel): is this dead code? How is this not overwritten
# by the data.redirect.id().to(url_names.GCI_RESPOND_INVITE) at the
# bottom of this method?
data.redirect.program()
data.redirect.to('edit_gci_profile')
invite_logic.acceptInvite(data)
else: # reject
invite_logic.rejectInvite(data)
return data.redirect.id().to(url_names.GCI_RESPOND_INVITE)
def _constructPageName(self, data):
invite = data.invite
return "%s Invite" % (invite.role.capitalize())
class UserInvitesList(Template):
"""Template for list of invites that have been sent to the current user."""
def __init__(self, data):
self.data = data
r = data.redirect
list_config = lists.ListConfiguration()
list_config.addPlainTextColumn('org', 'From',
lambda entity, *args: entity.org.name)
list_config.addSimpleColumn('status', 'Status')
list_config.setRowAction(
lambda e, *args: r.id(e.key().id())
.urlOf(url_names.GCI_RESPOND_INVITE))
self._list_config = list_config
def getListData(self):
q = GCIRequest.all()
q.filter('type', 'Invitation')
q.filter('user', self.data.user)
response_builder = lists.RawQueryContentResponseBuilder(
self.data.request, self._list_config, q, lists.keyStarter)
return response_builder.build()
def context(self):
invite_list = lists.ListConfigurationResponse(
self.data, self._list_config, 0)
return {
'lists': [invite_list],
}
def templatePath(self):
return 'v2/modules/gci/invite/_invite_list.html'
class ListUserInvitesPage(GCIRequestHandler):
"""View for the page that lists all the invites which have been sent to
the current user.
"""
def templatePath(self):
return 'v2/modules/gci/invite/invite_list.html'
def djangoURLPatterns(self):
return [
url(r'invite/list_user/%s$' % url_patterns.PROGRAM, self,
name=url_names.GCI_LIST_INVITES),
]
def checkAccess(self, data, check, mutator):
check.isProfileActive()
def jsonContext(self, data, check, mutator):
list_content = UserInvitesList(data).getListData()
if list_content:
return list_content.content()
else:
raise AccessViolation('You do not have access to this data')
def context(self, data, check, mutator):
return {
'page_name': 'Invitations to you',
'invite_list': UserInvitesList(data),
}