blob: 14dc9698a2083e005472346ddcb69899636e1199 [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.
"""Module with connection related views."""
import collections
import datetime
from django import forms as django_forms
from django import http
from django.utils import html
from django.utils import translation
from google.appengine.ext import db
from google.appengine.ext import ndb
from melange.logic import connection as connection_logic
from melange.logic import profile as profile_logic
from melange.models import connection as connection_model
from melange.models import organization as org_model
from melange.models import user as user_model
from melange.request import access
from melange.request import exception
from melange.request import links
from melange.templates import connection_list
from melange.utils import lists as melange_lists
from melange.utils import rich_bool
from melange.views.helper import form_handler
from soc.logic import cleaning
from soc.logic.helper import notifications
from soc.tasks import mailer
from soc.views import base
from soc.views import forms as soc_forms
from soc.views import template
from soc.views.helper import lists
from soc.views.helper import url_patterns
from summerofcode.templates import top_message
ACTIONS_FORM_NAME = 'actions_form'
MESSAGE_FORM_NAME = 'message_form'
LIST_CONNECTIONS_FOR_USER_PAGE_NAME = translation.ugettext(
'List of connections for %s')
LIST_CONNECTIONS_FOR_ORG_ADMIN_PAGE_NAME = translation.ugettext(
'List connections for organization admin')
MANAGE_CONNECTION_PAGE_NAME = translation.ugettext(
'Manage connection')
PICK_ORGANIZATION_TO_CONNECT_PAGE_NAME = translation.ugettext(
'Pick organization to connect with')
PICK_ORGANIZATION_TO_CONNECT_LIST_DESCRIPTION = translation.ugettext(
'Pick organization to connect with')
START_CONNECTION_AS_ORG_PAGE_NAME = translation.ugettext(
'Start connections with users')
START_CONNECTION_AS_USER_PAGE_NAME = translation.ugettext(
'Start connection with organization')
START_CONNECTION_MESSAGE_LABEL = translation.ugettext(
'Message')
CONNECTION_FORM_USERS_HELP_TEXT = translation.ugettext(
'Comma separated list of usernames')
CONNECTION_FORM_USERS_LABEL = translation.ugettext(
'Users')
CONNECTION_AS_USER_FORM_MESSAGE_HELP_TEXT = translation.ugettext(
'Optional message to the organization')
CONNECTION_AS_ORG_FORM_MESSAGE_HELP_TEXT = translation.ugettext(
'Optional message to users')
MANAGE_CONNECTION_FORM_ORG_ROLE_HELP_TEXT = translation.ugettext(
'Type of role you designate to the user')
START_CONNECTION_FORM_ORG_ROLE_HELP_TEXT = translation.ugettext(
'Type of role you designate to the users')
CONNECTION_FORM_USER_ROLE_HELP_TEXT = translation.ugettext(
'Whether you request role from organization or not')
CONNECTION_FORM_IGNORED_HELP_TEXT = translation.ugettext(
'Ignored connections are not listed on the main list of all connections '
'for your organizations.')
CONNECTION_FORM_ORG_ROLE_LABEL = translation.ugettext(
'Role To Assign')
CONNECTION_FORM_USER_ROLE_LABEL = translation.ugettext(
'Role For Organization')
CONNECTION_FORM_IGNORED_LABEL = translation.ugettext('Ignored')
MESSAGE_FORM_CONTENT_LABEL = translation.ugettext(
'Send New Message')
MESSAGE_CONNECTION_CANNOT_BE_ACCESSED = translation.ugettext(
'Requested connection cannot by accessed by this user.')
ORGANIZATION_ITEM_LABEL = translation.ugettext('Organization')
USER_ID_ITEM_LABEL = translation.ugettext('Username')
PUBLIC_NAME_ITEM_LABEL = translation.ugettext('Public Name')
USER_ROLE_ITEM_LABEL = translation.ugettext('User Requests Role')
ORG_ROLE_ITEM_LABEL = translation.ugettext('Role Granted by Organization')
ACTUAL_ROLE_ITEM_LABEL = translation.ugettext('Actual Role')
INITIALIZED_ON_LABEL = translation.ugettext('Initialized On')
USER_ROLE_CHOICES = (
(connection_model.NO_ROLE, 'No'),
(connection_model.ROLE, 'Yes'))
ACTUAL_ORG_ROLE_CHOICES = [
(connection_model.MENTOR_ROLE, 'Mentor'),
(connection_model.ORG_ADMIN_ROLE, 'Organization Admin'),
]
ALL_ORG_ROLE_CHOICES = [
(connection_model.NO_ROLE, 'No Role'),
(connection_model.MENTOR_ROLE, 'Mentor'),
(connection_model.ORG_ADMIN_ROLE, 'Organization Admin'),
]
def cleanUsers(tokens, program_key):
"""Cleans users field.
Args:
tokens: A string containing user tokens.
Returns:
Cleaned value for users field. It is a tuple with three elements:
list of found profile entities, list of found user entities and list
of email addresses.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
identifiers = set(token.strip() for token in tokens.split(','))
emails = []
users = []
profiles = []
error_list = []
for identifier in identifiers:
try:
if '@' in identifier:
cleaning.cleanEmail(identifier)
emails.append(identifier)
else:
cleaning.cleanLinkID(identifier)
profile = profile_logic.getProfileForUsername(identifier, program_key)
if profile:
profiles.append(profile)
else:
user = user_model.User.get_by_id(identifier)
if user:
users.append(user)
else:
raise django_forms.ValidationError(
cleaning.USER_DOES_NOT_EXIST_ERROR_MSG % identifier)
except django_forms.ValidationError as e:
error_list.append(' '.join(e.messages))
# form is not valid if at least one error occurred
if error_list:
raise django_forms.ValidationError(
' '.join(error_list))
# TODO(daniel): anonymous connections should be supported
if users or emails:
raise django_forms.ValidationError(
'Anonymous connections are not supported at this time. '
'Please provide usernames of users with profiles only.')
return profiles, users, emails
def _getValueForUserRoleItem(data):
"""Returns value to be displayed for User Role item of connection summary.
Args:
data: request_data.RequestData for the current request.
Returns:
a string containing a value for User Role item.
"""
if data.url_connection.user_role == connection_model.ROLE:
return 'Yes'
else:
return 'No'
def _getValueForOrgRoleItem(data):
"""Returns value to be displayed for Organization Role item of connection
summary.
Args:
data: request_data.RequestData for the current request.
Returns:
a string containing a value for Organization Role item.
"""
if data.url_connection.org_role == connection_model.NO_ROLE:
return translation.ugettext('No role')
elif data.url_connection.org_role == connection_model.MENTOR_ROLE:
return translation.ugettext('Mentor')
else:
return translation.ugettext('Organization Administrator')
def _getValueForActualRoleItem(data):
"""Returns value to be displayed for Actual Role item of connection summary.
Args:
data: request_data.RequestData for the current request.
Returns:
A string containing a value for Organization Role item.
"""
return connection_model.VERBOSE_ROLE_NAMES[data.url_connection.getRole()]
class ConnectionForm(soc_forms.ModelForm):
"""Django form to show specific fields for an organization.
Upon creation the form can be customized using instance methods so as
to accommodate actual use cases.
"""
users = django_forms.CharField(
required=True, label=CONNECTION_FORM_USERS_LABEL,
help_text=CONNECTION_FORM_USERS_HELP_TEXT)
message = django_forms.CharField(
widget=django_forms.Textarea(), required=False)
role = django_forms.CharField()
ignored = django_forms.BooleanField(
required=False, label=CONNECTION_FORM_IGNORED_LABEL,
help_text=CONNECTION_FORM_IGNORED_HELP_TEXT)
Meta = object
def __init__(self, bound_field_class, request_data=None, **kwargs):
"""Initializes a new instance of connection form."""
super(ConnectionForm, self).__init__(bound_field_class, **kwargs)
self.request_data = request_data
self.fields['message'].label = START_CONNECTION_MESSAGE_LABEL
def clean_users(self):
"""Cleans users field.
Returns:
Cleaned value for users field. It is a tuple with three elements:
list of found profile entities, list of found user entities and list
of email addresses.
Raises:
django_forms.ValidationError if the submitted value is not valid.
"""
return cleanUsers(
self.cleaned_data['users'], self.request_data.program.key())
def setHelpTextForMessage(self, help_text):
"""Sets help text for 'message' field.
Args:
help_text: a string containing help text to set.
"""
self.fields['message'].help_text = help_text
def setLabelForRole(self, label):
"""Sets label for 'role' field.
Args:
label: a string containing the label to set.
"""
self.fields['user_role'].label = label
def setHelpTextForRole(self, help_text):
"""Sets help text for 'role' field.
Args:
help_text: a string containing help text to set.
"""
self.fields['user_role'].help_text = help_text
def removeField(self, key):
"""Removes field with the specified key.
Args:
key: a string with a key of a field to remove.
"""
del self.fields[key]
class MessageForm(soc_forms.ModelForm):
"""Django form to submit connection messages."""
content = django_forms.CharField(
widget=django_forms.Textarea(), required=True)
def __init__(self, bound_field_class, **kwargs):
"""Initializes a new instance of connection message form."""
super(MessageForm, self).__init__(bound_field_class, **kwargs)
self.fields['content'].label = MESSAGE_FORM_CONTENT_LABEL
def clean_content(self):
field_name = 'content'
wrapped_clean_html_content = cleaning.clean_html_content(field_name)
content = wrapped_clean_html_content(self)
if content:
return content
else:
raise django_forms.ValidationError(
translation.ugettext('Message content cannot be empty.'),
code='invalid')
class ConnectionFormFactory(object):
"""Factory to create forms to manage and start connections."""
def __init__(self, bound_field_class, template_path):
"""Initializes a new instance of the factory.
Args:
bound_field_class: Bound field class.
template_path: The path of the template to be used to render the form.
"""
self._bound_field_class = bound_field_class
self._template_path = template_path
def createFormToStartConnectionAsUser(self, **kwargs):
"""Returns a Django form to start connection as a user.
Returns:
ConnectionForm adjusted to start connection as a user.
"""
form = ConnectionForm(
self._bound_field_class, template_path=self._template_path, **kwargs)
form.removeField('role')
form.removeField('users')
form.removeField('ignored')
form.setHelpTextForMessage(CONNECTION_AS_USER_FORM_MESSAGE_HELP_TEXT)
return form
def createFormToStartConnectionAsOrg(self, **kwargs):
"""Returns a Django form to start connection as an organization
administrator.
Returns:
ConnectionForm adjusted to start connection as organization administrator.
"""
form = ConnectionForm(
self._bound_field_class, template_path=self._template_path, **kwargs)
form.fields['role'].label = CONNECTION_FORM_ORG_ROLE_LABEL
form.fields['role'].help_text = START_CONNECTION_FORM_ORG_ROLE_HELP_TEXT
form.fields['role'].widget = django_forms.Select(
choices=ACTUAL_ORG_ROLE_CHOICES)
form.removeField('ignored')
form.setHelpTextForMessage(CONNECTION_AS_ORG_FORM_MESSAGE_HELP_TEXT)
return form
def createFormToManageConnectionAsUser(self, **kwargs):
"""Returns a Django form to manage connection as a user.
Returns:
ConnectionForm adjusted to manage connection as a user.
"""
form = ConnectionForm(
self._bound_field_class, template_path=self._template_path, **kwargs)
form.removeField('message')
form.removeField('users')
form.removeField('ignored')
form.fields['role'].widget = django_forms.Select(
choices=USER_ROLE_CHOICES)
form.fields['role'].label = CONNECTION_FORM_USER_ROLE_LABEL
form.fields['role'].help_text = CONNECTION_FORM_USER_ROLE_HELP_TEXT
return form
def createFormToManageConnectionAsOrg(self, **kwargs):
"""Returns a Django form to manage connection as an organization admin.
Returns:
ConnectionForm adjusted to manage connection as an organization admin.
"""
form = ConnectionForm(
self._bound_field_class, template_path=self._template_path, **kwargs)
form.removeField('message')
form.removeField('users')
form.fields['role'].widget = django_forms.Select(
choices=ALL_ORG_ROLE_CHOICES)
form.fields['role'].label = CONNECTION_FORM_ORG_ROLE_LABEL
form.fields['role'].help_text = MANAGE_CONNECTION_FORM_ORG_ROLE_HELP_TEXT
return form
def createMessageForm(self, **kwargs):
"""Returns a Django form to submit a connection message.
Returns:
MessageForm to submit a connection message.
"""
return MessageForm(
self._bound_field_class, template_path=self._template_path, **kwargs)
START_CONNECTION_AS_ORG_ACCESS_CHECKER = access.ConjuctionAccessChecker([
access.PROGRAM_ACTIVE_ACCESS_CHECKER,
access.IS_USER_ORG_ADMIN_FOR_NDB_ORG,
access.UrlOrgStatusAccessChecker(
[org_model.Status.ACCEPTED, org_model.Status.PRE_ACCEPTED,
org_model.Status.PRE_REJECTED, org_model.Status.APPLYING]),
])
class StartConnectionAsOrg(base.RequestHandler):
"""View to start connections with users as organization administrators."""
access_checker = START_CONNECTION_AS_ORG_ACCESS_CHECKER
def __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names, template_path, form_factory):
"""Initializes a new instance of the request handler for the specified
parameters.
Args:
initializer: Implementation of initialize.Initializer interface.
linker: Instance of links.Linker class.
renderer: Implementation of render.Renderer interface.
error_handler: Implementation of error.ErrorHandler interface.
url_pattern_constructor:
Implementation of url_patterns.UrlPatternConstructor.
url_names: Instance of url_names.UrlNames.
template_path: The path of the template to be used.
form_factory: Instance of ConnectionFormFactory.
"""
super(StartConnectionAsOrg, self).__init__(
initializer, linker, renderer, error_handler)
self.url_pattern_constructor = url_pattern_constructor
self.url_names = url_names
self.template_path = template_path
self.form_factory = form_factory
def djangoURLPatterns(self):
"""See base.RequestHandler.djangoURLPatterns for specification."""
return [
self.url_pattern_constructor.construct(
r'connection/start/org/%s$' % url_patterns.ORG,
self, name=self.url_names.CONNECTION_START_AS_ORG)
]
def templatePath(self):
"""See base.RequestHandler.templatePath for specification."""
return self.template_path
def context(self, data, check, mutator):
"""See base.RequestHandler.context for specification."""
form = self.form_factory.createFormToStartConnectionAsOrg(
data=data.POST or None, request_data=data)
return {
'page_name': START_CONNECTION_AS_ORG_PAGE_NAME,
'forms': [form],
'error': bool(form.errors)
}
def post(self, data, check, mutator):
"""See base.RequestHandler.post for specification."""
form = self.form_factory.createFormToStartConnectionAsOrg(
data=data.POST, request_data=data)
if form.is_valid():
profiles, _, _ = form.cleaned_data['users']
notification_context_provider = (
notifications.StartConnectionByOrgContextProvider(
links.ABSOLUTE_LINKER, self.url_names))
connections = []
for profile in profiles:
# notification is send based on user's preferences
recipients = (
[profile.contact.email]
if profile.notification_settings.user_connections
else [])
connections.append(createConnectionTxn(
data, profile.key, data.program, data.url_ndb_org, None,
message=form.cleaned_data['message'],
notification_context_provider=notification_context_provider,
recipients=recipients,
org_role=form.cleaned_data['role'],
org_admin=data.ndb_profile, seen_by_org=True))
url = self.linker.profile(
data.ndb_profile, self.url_names.CONNECTION_LIST_FOR_ORG_ADMIN)
return http.HttpResponseRedirect(url)
else:
# TODO(nathaniel): problematic self-call.
return self.get(data, check, mutator)
class NoConnectionExistsAccessChecker(access.AccessChecker):
"""AccessChecker that ensures that no connection exists between the user,
who is currently logged-in, and organization which is specified in the URL.
"""
def __init__(self, url_names):
"""Initializes a new instance of this access checker for the specified
parameters.
Args:
url_names: Instance of url_names.UrlNames.
"""
self.url_names = url_names
def checkAccess(self, data, check):
"""See access.AccessChecker.checkAccess for specification."""
connection = connection_logic.connectionForProfileAndOrganization(
data.ndb_profile.key, data.url_ndb_org.key)
if connection:
url = links.LINKER.userId(
data.ndb_profile.key, connection.key.id(),
self.url_names.CONNECTION_MANAGE_AS_USER)
raise exception.Redirect(url)
class UnacceptedConnectionsLimitNotReachedAccessChecker(access.AccessChecker):
"""AccessChecker that ensures that the current user has not reached the limit
of connections which have been initiated by them but not accepted by the
organization.
"""
def checkAccess(self, data, check):
"""See access.AccessChecker.checkAccess for specification."""
if connection_logic.unacceptedConnectionsLimitReached(data.ndb_profile.key):
raise exception.Forbidden(
message=connection_logic.UNACCEPTED_CONNECTIONS_LIMIT_REACHED)
UNACCEPTED_CONNECTIONS_LIMIT_NOT_REACHED_ACCESS_CHECKER = (
UnacceptedConnectionsLimitNotReachedAccessChecker())
class StartConnectionAsUser(base.RequestHandler):
"""View to start connections with organizations as users."""
def __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names, template_path,
access_checker, form_factory):
"""Initializes a new instance of the request handler for the specified
parameters.
Args:
initializer: Implementation of initialize.Initializer interface.
linker: Instance of links.Linker class.
renderer: Implementation of render.Renderer interface.
error_handler: Implementation of error.ErrorHandler interface.
url_pattern_constructor:
Implementation of url_patterns.UrlPatternConstructor.
url_names: Instance of url_names.UrlNames.
template_path: The path of the template to be used.
access_checker: Implementation of access.AccessChecker interface.
form_factory: Instance of ConnectionFormFactory.
"""
super(StartConnectionAsUser, self).__init__(
initializer, linker, renderer, error_handler)
self.url_pattern_constructor = url_pattern_constructor
self.url_names = url_names
self.template_path = template_path
self.access_checker = access_checker
self.form_factory = form_factory
def djangoURLPatterns(self):
"""See base.RequestHandler.djangoURLPatterns for specification."""
return [
self.url_pattern_constructor.construct(
r'connection/start/user/%s$' % url_patterns.ORG,
self, name=self.url_names.CONNECTION_START_AS_USER)
]
def templatePath(self):
"""See base.RequestHandler.templatePath for specification."""
return self.template_path
def context(self, data, check, mutator):
"""See base.RequestHandler.context for specification."""
return {
'form_top_msg': top_message.startConnectionAsUserTopMessage(data),
'page_name': START_CONNECTION_AS_USER_PAGE_NAME,
'organization': data.url_ndb_org.org_id,
'forms': [self.form_factory.createFormToStartConnectionAsUser()]
}
def post(self, data, check, mutator):
"""See base.RequestHandler.post for specification."""
form = self.form_factory.createFormToStartConnectionAsUser(data=data.POST)
if form.is_valid():
# create notification that will be sent to organization administrators
# based on their notification settings
org_admins = profile_logic.getOrgAdmins(data.url_ndb_org)
emails = [
org_admin.contact.email for org_admin in org_admins
if org_admin.notification_settings.org_connections]
context_provider = notifications.StartConnectionByUserContextProvider(
links.ABSOLUTE_LINKER, self.url_names)
connection = createConnectionTxn(
data, data.ndb_profile.key, data.program, data.url_ndb_org,
None, message=form.cleaned_data['message'],
notification_context_provider=context_provider,
recipients=emails, user_role=connection_model.ROLE, seen_by_user=True,
init_by_user=True)
url = links.LINKER.userId(
data.ndb_profile.key, connection.key.id(),
self.url_names.CONNECTION_MANAGE_AS_USER)
return http.HttpResponseRedirect(url)
else:
# TODO(nathaniel): problematic self-use.
return self.get(data, check, mutator)
class UrlConnectionIsForCurrentUserAccessChecker(access.AccessChecker):
"""AccessChecker that ensures that connection which is retrieved from URL
data belongs to the user who is currently logged in.
"""
def checkAccess(self, data, check):
"""See AccessChecker.checkAccess for specification."""
access.ensureLoggedIn(data)
if (not data.ndb_profile or
data.url_connection.key.parent() != data.ndb_profile.key):
raise exception.Forbidden(message=MESSAGE_CONNECTION_CANNOT_BE_ACCESSED)
_CONNECTION_NOT_MANAGEABLE_REJECTED_ORG = translation.ugettext(
'Connections for rejected organizations cannot be managed.')
_CONNECTION_NOT_MANAGEABLE_INACTIVE_PROGRAM = translation.ugettext(
'Connections for inactive programs cannot be managed.')
class IsUrlConnectionManageable(access.AccessChecker):
"""AccessChecker that ensures that the connection specified in the URL
can be managed at this time.
This is another level of security that ensures that POST requests are
immediately rejected for non-manageable connections.
"""
def checkAccess(self, data, check):
"""See AccessChecker.checkAccess for specification."""
# connections can always be viewed in read-only mode - GET method is fine
if data.request.method == 'POST':
is_manageable = connection_logic.isConnectionManageable(
data.url_connection, data.url_connection.organization.get(),
data.program_timeline)
if not is_manageable:
if (is_manageable.extra ==
connection_logic.IsConnectionManageableError.program_not_active):
raise exception.Forbidden(
message=_CONNECTION_NOT_MANAGEABLE_INACTIVE_PROGRAM)
elif (is_manageable.extra ==
connection_logic.IsConnectionManageableError.org_rejected):
raise exception.Forbidden(
message=_CONNECTION_NOT_MANAGEABLE_REJECTED_ORG)
MANAGE_CONNECTION_AS_USER_ACCESS_CHECKER = access.ConjuctionAccessChecker([
access.PROGRAM_ACTIVE_ACCESS_CHECKER,
UrlConnectionIsForCurrentUserAccessChecker(),
IsUrlConnectionManageable()
])
class ManageConnectionAsUser(base.RequestHandler):
"""View to manage an existing connection by the user."""
access_checker = MANAGE_CONNECTION_AS_USER_ACCESS_CHECKER
def __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names, template_path, form_factory,
role_eligibility_manager):
"""Initializes a new instance of the request handler for the specified
parameters.
Args:
initializer: Implementation of initialize.Initializer interface.
linker: Instance of links.Linker class.
renderer: Implementation of render.Renderer interface.
error_handler: Implementation of error.ErrorHandler interface.
url_pattern_constructor:
Implementation of url_patterns.UrlPatternConstructor.
url_names: Instance of url_names.UrlNames.
template_path: The path of the template to be used.
form_factory: Instance of ConnectionFormFactory.
role_eligibility_manager: Implementation of RoleEligibilityManager
interface to determine role eligibility.
"""
super(ManageConnectionAsUser, self).__init__(
initializer, linker, renderer, error_handler)
self.url_pattern_constructor = url_pattern_constructor
self.url_names = url_names
self.template_path = template_path
self.form_factory = form_factory
self.role_eligibility_manager = role_eligibility_manager
def djangoURLPatterns(self):
"""See base.GCIRequestHandler.djangoURLPatterns for specification."""
return [
self.url_pattern_constructor.construct(
r'connection/manage/user/%s$' % url_patterns.USER_ID,
self, name=self.url_names.CONNECTION_MANAGE_AS_USER)
]
def templatePath(self):
"""See base.GCIRequestHandler.templatePath for specification."""
return self.template_path
def context(self, data, check, mutator):
"""See base.GCIRequestHandler.context for specification."""
summary_items = collections.OrderedDict()
summary_items[USER_ID_ITEM_LABEL] = data.url_ndb_profile.profile_id
summary_items[ORGANIZATION_ITEM_LABEL] = (
data.url_connection.organization.get().name)
summary_items[USER_ROLE_ITEM_LABEL] = _getValueForUserRoleItem(data)
summary_items[ORG_ROLE_ITEM_LABEL] = _getValueForOrgRoleItem(data)
summary_items[ACTUAL_ROLE_ITEM_LABEL] = _getValueForActualRoleItem(data)
summary_items[INITIALIZED_ON_LABEL] = data.url_connection.created_on
messages = connection_logic.getConnectionMessages(data.url_connection.key)
mark_as_seen_url = links.LINKER.userId(
data.url_ndb_profile.key, data.url_connection.key.id(),
self.url_names.CONNECTION_MARK_AS_SEEN_BY_USER)
context = {
'page_name': MANAGE_CONNECTION_PAGE_NAME,
'items': summary_items,
'messages': messages,
'mark_as_seen_url': mark_as_seen_url,
}
is_manageable = connection_logic.isConnectionManageable(
data.url_connection, data.url_connection.organization.get(),
data.program.timeline)
if is_manageable:
form_data = {'role': data.url_connection.user_role}
context['actions_form'] = (
self.form_factory.createFormToManageConnectionAsUser(
data=data.POST or form_data, name=ACTIONS_FORM_NAME))
context['message_form'] = self.form_factory.createMessageForm(
data=data.POST or None, name=MESSAGE_FORM_NAME)
return context
def post(self, data, check, mutator):
"""See base.GCIRequestHandler.post for specification."""
handler = self._dispatchPostData(data)
return handler.handle(data, check, mutator)
def _dispatchPostData(self, data):
"""Picks form handler that is capable of handling the data that was sent
in the the current request.
Args:
data: request_data.RequestData for the current request.
Returns:
FormHandler implementation to handler the received data.
"""
if ACTIONS_FORM_NAME in data.POST:
url = links.LINKER.userId(
data.url_ndb_profile.key, data.url_connection.key.id(),
self.url_names.CONNECTION_MANAGE_AS_USER)
# TODO(daniel): eliminate passing self object.
return UserActionsFormHandler(
self, self.form_factory, self.role_eligibility_manager, url=url)
elif MESSAGE_FORM_NAME in data.POST:
# TODO(daniel): eliminate passing self object.
return MessageFormHandler(
self, data.url_ndb_profile.key,
self.url_names.CONNECTION_MANAGE_AS_USER,
self.form_factory)
else:
raise exception.BadRequest('No valid form data is found in POST.')
class IsUserOrgAdminForUrlConnection(access.AccessChecker):
"""AccessChecker that ensures that the logged in user is organization
administrator for the connection which is retrieved from the URL data.
"""
def checkAccess(self, data, check):
"""See AccessChecker.checkAccess for specification."""
if not data.ndb_profile:
raise exception.Forbidden(message=access._MESSAGE_NO_PROFILE)
if data.url_connection.organization not in data.ndb_profile.admin_for:
raise exception.Forbidden(
message=access._MESSAGE_NOT_ORG_ADMIN_FOR_ORG % (
data.url_connection.organization.id()))
MANAGE_CONNECTION_AS_ORG_ACCESS_CHECKER = access.ConjuctionAccessChecker([
access.PROGRAM_ACTIVE_ACCESS_CHECKER,
access.HAS_PROFILE_ACCESS_CHECKER,
IsUserOrgAdminForUrlConnection(),
IsUrlConnectionManageable(),
])
class ManageConnectionAsOrg(base.RequestHandler):
"""View to manage an existing connection by the organization."""
access_checker = MANAGE_CONNECTION_AS_ORG_ACCESS_CHECKER
def __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names, template_path, form_factory,
role_eligibility_manager):
"""Initializes a new instance of the request handler for the specified
parameters.
Args:
initializer: Implementation of initialize.Initializer interface.
linker: Instance of links.Linker class.
renderer: Implementation of render.Renderer interface.
error_handler: Implementation of error.ErrorHandler interface.
url_pattern_constructor:
Implementation of url_patterns.UrlPatternConstructor.
url_names: Instance of url_names.UrlNames.
template_path: The path of the template to be used.
form_factory: Instance of ConnectionFormFactory.
role_eligibility_manager: Implementation of RoleEligibilityManager
interface to determine role eligibility.
"""
super(ManageConnectionAsOrg, self).__init__(
initializer, linker, renderer, error_handler)
self.url_pattern_constructor = url_pattern_constructor
self.url_names = url_names
self.template_path = template_path
self.form_factory = form_factory
self.role_eligibility_manager = role_eligibility_manager
def djangoURLPatterns(self):
"""See base.GCIRequestHandler.djangoURLPatterns for specification."""
return [
self.url_pattern_constructor.construct(
r'connection/manage/org/%s$' % url_patterns.USER_ID,
self, name=self.url_names.CONNECTION_MANAGE_AS_ORG)
]
def templatePath(self):
"""See base.GCIRequestHandler.templatePath for specification."""
return self.template_path
def context(self, data, check, mutator):
"""See base.GCIRequestHandler.context for specification."""
summary_items = collections.OrderedDict()
summary_items[USER_ID_ITEM_LABEL] = data.url_ndb_profile.profile_id
summary_items[PUBLIC_NAME_ITEM_LABEL] = data.url_ndb_profile.public_name
summary_items[ORGANIZATION_ITEM_LABEL] = (
data.url_connection.organization.get().name)
summary_items[USER_ROLE_ITEM_LABEL] = _getValueForUserRoleItem(data)
summary_items[ORG_ROLE_ITEM_LABEL] = _getValueForOrgRoleItem(data)
summary_items[ACTUAL_ROLE_ITEM_LABEL] = _getValueForActualRoleItem(data)
summary_items[INITIALIZED_ON_LABEL] = data.url_connection.created_on
messages = connection_logic.getConnectionMessages(data.url_connection.key)
mark_as_seen_url = links.LINKER.userId(
data.url_ndb_profile.key, data.url_connection.key.id(),
self.url_names.CONNECTION_MARK_AS_SEEN_BY_ORG)
context = {
'page_name': MANAGE_CONNECTION_PAGE_NAME,
'items': summary_items,
'messages': messages,
'mark_as_seen_url': mark_as_seen_url,
}
is_manageable = connection_logic.isConnectionManageable(
data.url_connection, data.url_connection.organization.get(),
data.program.timeline)
if is_manageable:
form_data = {
'role': data.url_connection.org_role,
'ignored': data.url_connection.ignored_by_org,
}
context['actions_form'] = (
self.form_factory.createFormToManageConnectionAsOrg(
data=data.POST or form_data, name=ACTIONS_FORM_NAME))
context['message_form'] = self.form_factory.createMessageForm(
data=data.POST or None, name=MESSAGE_FORM_NAME)
return context
def post(self, data, check, mutator):
"""See base.GCIRequestHandler.post for specification."""
handler = self._dispatchPostData(data)
return handler.handle(data, check, mutator)
def _dispatchPostData(self, data):
"""Picks form handler that is capable of handling the data that was sent
in the the current request.
Args:
data: request_data.RequestData for the current request.
Returns:
FormHandler implementation to handler the received data.
"""
if ACTIONS_FORM_NAME in data.POST:
url = links.LINKER.userId(
data.url_ndb_profile.key, data.url_connection.key.id(),
self.url_names.CONNECTION_MANAGE_AS_ORG)
# TODO(daniel): eliminate passing self object.
return OrgActionsFormHandler(
self, self.form_factory, self.role_eligibility_manager, url=url)
elif MESSAGE_FORM_NAME in data.POST:
# TODO(daniel): eliminate passing self object.
return MessageFormHandler(
self, data.ndb_profile.key,
self.url_names.CONNECTION_MANAGE_AS_ORG,
self.form_factory)
else:
raise exception.BadRequest('No valid form data is found in POST.')
class RoleEligibilityManager(object):
"""Interface which provides methods that determine whether certain roles
are eligible to the user defined in the URL.
"""
def isNoRoleEligibleForOrg(self, data):
"""Tells whether having no role for the organization specified in the URL
is eligible for the profile specified in the URL.
Args:
data: request_data.RequestData for the current request.
Returns:
RichBool whose value is set to True, if the user is eligible for no
role for the specified organization. Otherwise, RichBool whose value is
set to False and extra part is a string that represents a reason why the
user is not eligible to resign from role at this time.
"""
raise NotImplementedError
class UserActionsFormHandler(form_handler.FormHandler):
"""Form handler implementation to handle incoming data that is supposed to
take an action on the existing connection by users.
"""
def __init__(self, view, form_factory, role_eligibility_manager, **kwargs):
"""Initializes new instance of form handler.
Args:
view: callback to implementation of base.RequestHandler
that creates this object.
form_factory: Instance of ConnectionFormFactory.
role_eligibility_manager: Implementation of RoleEligibilityManager
interface to determine role eligibility.
"""
super(UserActionsFormHandler, self).__init__(view, **kwargs)
self.form_factory = form_factory
self.role_eligibility_manager = role_eligibility_manager
def handle(self, data, check, mutator):
"""Takes an action on the connection based on the data that was sent
in the current request.
See form_handler.FormHandler.handle for specification.
"""
actions_form = self.form_factory.createFormToManageConnectionAsUser(
data=data.POST)
if actions_form.is_valid():
role = actions_form.cleaned_data['role']
if role == connection_model.NO_ROLE:
success = self._handleNoRoleSelection(data)
else:
success = self._handleRoleSelection(data)
if success:
return http.HttpResponseRedirect(self._url)
else:
raise exception.BadRequest(success.extra)
else:
# TODO(nathaniel): problematic self-use.
return self._view.get(data, check, mutator)
def _handleNoRoleSelection(self, data):
"""Makes all necessary changes if user selects connection_model.NO_ROLE.
Args:
data: A soc.views.helper.request_data.RequestData.
Returns:
RichBool whose value is set to True, if the selection has been handled
successfully. Otherwise, RichBool whose value is set to False and extra
part is a string representation of the reason why the picked selection
is not possible.
"""
is_eligible = self.role_eligibility_manager.isNoRoleEligibleForOrg(data)
if is_eligible:
handleUserNoRoleSelectionTxn(data.url_connection, None)
return is_eligible
def _handleRoleSelection(self, data):
"""Makes all necessary changes if user selects connection_model.ROLE.
Args:
data: A soc.views.helper.request_data.RequestData.
Returns:
RichBool whose value is set to True, if the selection has been handled
successfully. Otherwise, RichBool whose value is set to False and extra
part is a string representation of the reason why the picked selection
is not possible.
"""
if data.url_connection.orgOfferedMentorRole():
is_eligible = profile_logic.isMentorRoleEligibleForOrg(
data.url_ndb_profile, data.url_connection.organization)
else:
is_eligible = True
if is_eligible:
# TODO(daniel): eliminate these calls by removing data from
# the call below. without these now XG transactions may be needed
data.program # pylint: disable=pointless-statement
data.site # pylint: disable=pointless-statement
handleUserRoleSelectionTxn(
data.url_connection, None, data.program,
data.program.getProgramMessages(), data.site)
return is_eligible
class OrgActionsFormHandler(form_handler.FormHandler):
"""Form handler implementation to handle incoming data that is supposed to
take an action on the existing connection by organization administrators.
"""
def __init__(self, view, form_factory, role_eligibility_manager, **kwargs):
"""Initializes new instance of form handler.
Args:
view: callback to implementation of base.RequestHandler
that creates this object.
form_factory: Instance of ConnectionFormFactory.
role_eligibility_manager: Implementation of RoleEligibilityManager
interface to determine role eligibility.
"""
super(OrgActionsFormHandler, self).__init__(view, **kwargs)
self.form_factory = form_factory
self.role_eligibility_manager = role_eligibility_manager
def handle(self, data, check, mutator):
"""Takes an action on the connection based on the data that was sent
in the current request.
See form_handler.FormHandler.handle for specification.
"""
actions_form = self.form_factory.createFormToManageConnectionAsOrg(
data=data.POST)
if actions_form.is_valid():
role = actions_form.cleaned_data['role']
ignored_by_org = actions_form.cleaned_data['ignored']
if role == connection_model.NO_ROLE:
is_eligible = self.role_eligibility_manager.isNoRoleEligibleForOrg(data)
elif role == connection_model.MENTOR_ROLE:
is_eligible = profile_logic.isMentorRoleEligibleForOrg(
data.url_ndb_profile, data.url_connection.organization)
elif role == connection_model.ORG_ADMIN_ROLE:
is_eligible = rich_bool.TRUE
else:
raise ValueError('Invalid role: %s' % role)
if is_eligible:
handleUpdateByOrganization(
data.url_connection.key, data.ndb_profile,
role, ignored_by_org, data.program,
data.program.getProgramMessages(), data.site)
return http.HttpResponseRedirect(self._url)
else:
raise exception.BadRequest(is_eligible.extra)
else:
# TODO(nathaniel): problematic self-use.
return self._view.get(data, check, mutator)
class MessageFormHandler(form_handler.FormHandler):
"""Form handler implementation to handle incoming data that is supposed to
create a new connection message.
"""
def __init__(self, view, author_key, url_name, form_factory):
"""Initializes new instance of form handler.
Args:
view: callback to implementation of base.RequestHandler
that creates this object.
author_key: profile key of the user who is the author of the message.
url_name: name of the URL that should be used for redirect after
the request is handled successfully.
form_factory: Instance of ConnectionFormFactory.
"""
super(MessageFormHandler, self).__init__(view)
self._author_key = author_key
self._url_name = url_name
self._form_factory = form_factory
def handle(self, data, check, mutator):
"""Creates and persists a new connection message based on the data
that was sent in the current request.
See form_handler.FormHandler.handle for specification.
"""
message_form = self._form_factory.createMessageForm(data=data.request.POST)
if message_form.is_valid():
content = message_form.cleaned_data['content']
createConnectionMessageTxn(
data.url_connection.key, self._author_key, content)
url = links.LINKER.userId(
data.url_ndb_profile.key, data.url_connection.key.id(),
self._url_name)
return http.HttpResponseRedirect(url)
else:
# TODO(nathaniel): problematic self-use.
return self._view.get(data, check, mutator)
LIST_CONNECTIONS_FOR_ORG_ADMIN_ACCESS_CHECKER = (
access.ConjuctionAccessChecker([
access.PROGRAM_ACTIVE_ACCESS_CHECKER,
access.NON_STUDENT_URL_PROFILE_ACCESS_CHECKER,
access.IS_URL_USER_ACCESS_CHECKER]))
class ListConnectionsForOrgAdmin(base.RequestHandler):
"""View to list all connections for an organization administrator."""
access_checker = LIST_CONNECTIONS_FOR_ORG_ADMIN_ACCESS_CHECKER
def __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names, template_path,
list_template_path):
"""Initializes a new instance of the request handler for the specified
parameters.
Args:
initializer: Implementation of initialize.Initializer interface.
linker: Instance of links.Linker class.
renderer: Implementation of render.Renderer interface.
error_handler: Implementation of error.ErrorHandler interface.
url_pattern_constructor:
Implementation of url_patterns.UrlPatternConstructor.
url_names: Instance of url_names.UrlNames.
template_path: The path of the template to be used.
list_template_path: The path of the template to be used to render list.
"""
super(ListConnectionsForOrgAdmin, self).__init__(
initializer, linker, renderer, error_handler)
self.url_pattern_constructor = url_pattern_constructor
self.url_names = url_names
self.template_path = template_path
self.list_template_path = list_template_path
def djangoURLPatterns(self):
"""See base.RequestHandler.djangoURLPatterns for specification."""
return [
self.url_pattern_constructor.construct(
r'connection/list/org/%s$' % url_patterns.PROFILE,
self, name=self.url_names.CONNECTION_LIST_FOR_ORG_ADMIN),
self.url_pattern_constructor.construct(
r'connection/list/all/org/%s$' % url_patterns.PROFILE,
self, name=self.url_names.CONNECTION_LIST_ALL_FOR_ORG_ADMIN)
]
def templatePath(self):
"""See base.RequestHandler.templatePath for specification."""
return self.template_path
def context(self, data, check, mutator):
"""See base.RequestHandler.context for specification."""
list_all_connections = self._listAllConnections(data)
return {
'connection_list':
connection_list.OrgAdminConnectionList(
self.url_names, self.list_template_path, data,
self._listAllConnections(data)),
'page_name': LIST_CONNECTIONS_FOR_ORG_ADMIN_PAGE_NAME,
'list_all_connections': list_all_connections,
}
def jsonContext(self, data, check, mutator):
"""See base.RequestHandler.jsonContext for specification."""
list_content = connection_list.OrgAdminConnectionList(
self.url_names, self.template_path, data,
self._listAllConnections(data)).getListData()
if list_content:
return list_content.content()
else:
raise exception.BadRequest(message='This data cannot be accessed.')
def _listAllConnections(self, data):
"""Tells whether all (i.e. including ignored) connections should be included
in the list.
The decision is based on the URL which was used to initialize the request.
Returns:
True, if all connections are supposed to be in the list. False, otherwise.
"""
all_connections_url = links.LINKER.profile(
data.ndb_profile, self.url_names.CONNECTION_LIST_ALL_FOR_ORG_ADMIN)
return data.request.path == all_connections_url
LIST_CONNECTIONS_FOR_USER_ACCESS_CHECKER = access.ConjuctionAccessChecker([
access.PROGRAM_ACTIVE_ACCESS_CHECKER,
access.NON_STUDENT_PROFILE_ACCESS_CHECKER])
class ListConnectionsForUser(base.RequestHandler):
"""View to list all connections for the currently logged in user."""
access_checker = LIST_CONNECTIONS_FOR_USER_ACCESS_CHECKER
def __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names, template_path,
list_template_path):
"""Initializes a new instance of the request handler for the specified
parameters.
Args:
initializer: Implementation of initialize.Initializer interface.
linker: Instance of links.Linker class.
renderer: Implementation of render.Renderer interface.
error_handler: Implementation of error.ErrorHandler interface.
url_pattern_constructor:
Implementation of url_patterns.UrlPatternConstructor.
url_names: Instance of url_names.UrlNames.
template_path: The path of the template to be used.
list_template_path: The path of the template to be used to render list.
"""
super(ListConnectionsForUser, self).__init__(
initializer, linker, renderer, error_handler)
self.url_pattern_constructor = url_pattern_constructor
self.url_names = url_names
self.template_path = template_path
self.list_template_path = list_template_path
def djangoURLPatterns(self):
"""See base.RequestHandler.djangoURLPatterns for specification."""
return [
self.url_pattern_constructor.construct(
r'connection/list/user/%s$' % url_patterns.PROGRAM,
self, name=self.url_names.CONNECTION_LIST_FOR_USER)
]
def templatePath(self):
"""See base.RequestHandler.templatePath for specification."""
return self.template_path
def context(self, data, check, mutator):
"""See base.RequestHandler.context for specification."""
page_name = (
LIST_CONNECTIONS_FOR_USER_PAGE_NAME % data.ndb_profile.profile_id)
return {
# TODO(daniel): template path should not be hardcoded here.
'connection_list':
connection_list.UserConnectionList(
self.url_names, self.list_template_path, data),
'page_name': page_name,
}
def jsonContext(self, data, check, mutator):
"""See base.RequestHandler.jsonContext for specification."""
list_content = connection_list.UserConnectionList(
self.url_names, self.template_path, data).getListData()
if list_content:
return list_content.content()
else:
raise exception.BadRequest(message='This data cannot be accessed.')
class PickOrganizationToConnectListRowRedirect(melange_lists.RedirectCustomRow):
"""Class which provides redirects for rows of pick organization to connect
with list."""
def __init__(self, data, url_names):
"""Initializes a new instance of the row redirect.
Args:
data: request_data.RequestData for the current request.
url_names: Instance of url_names.UrlNames.
"""
super(PickOrganizationToConnectListRowRedirect, self).__init__()
self.data = data
self.url_names = url_names
def getLink(self, item):
"""See lists.RedirectCustomRow.getLink for specification."""
org_key = ndb.Key(
self.data.models.ndb_org_model._get_kind(), item['columns']['key'])
return links.LINKER.organization(
org_key, self.url_names.CONNECTION_START_AS_USER)
# TODO(daniel): replace this class with a new style list
class PickOrganizationToConnectList(template.Template):
"""List of organizations with options to connect with."""
def __init__(self, data):
"""See template.Template.__init__ for specification."""
super(PickOrganizationToConnectList, self).__init__(data)
self._list_config = lists.ListConfiguration()
self._list_config.addSimpleColumn('org_id', 'Organization ID', hidden=True)
self._list_config.addPlainTextColumn(
'name', 'Name', lambda e, *args: e.name.strip())
def templatePath(self):
"""See template.Template.templatePath for specification."""
# TODO(daniel): this path should not be hardcoded to gsoc
return 'modules/gsoc/admin/_accepted_orgs_list.html'
def context(self):
"""See template.Template.context for specification."""
description = PICK_ORGANIZATION_TO_CONNECT_LIST_DESCRIPTION
list_configuration_response = lists.ListConfigurationResponse(
self.data, self._list_config, 0, description)
return {
'lists': [list_configuration_response],
}
PICK_ORGANIZATION_TO_CONNECT_ACCESS_CHECKER = (
access.ConjuctionAccessChecker([
access.PROGRAM_ACTIVE_ACCESS_CHECKER,
access.NON_STUDENT_PROFILE_ACCESS_CHECKER]))
class PickOrganizationToConnectPage(base.RequestHandler):
"""Page for non-student users to pick organization to start connection."""
access_checker = PICK_ORGANIZATION_TO_CONNECT_ACCESS_CHECKER
def __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names, template_path):
"""Initializes a new instance of the request handler for the specified
parameters.
Args:
initializer: Implementation of initialize.Initializer interface.
linker: Instance of links.Linker class.
renderer: Implementation of render.Renderer interface.
error_handler: Implementation of error.ErrorHandler interface.
url_pattern_constructor:
Implementation of url_patterns.UrlPatternConstructor.
url_names: Instance of url_names.UrlNames.
template_path: The path of the template to be used.
"""
super(PickOrganizationToConnectPage, self).__init__(
initializer, linker, renderer, error_handler)
self.url_pattern_constructor = url_pattern_constructor
self.url_names = url_names
self.template_path = template_path
def templatePath(self):
"""See base.RequestHandler.templatePath for specification."""
return self.template_path
def djangoURLPatterns(self):
"""See base.RequestHandler.djangoURLPatterns for specification."""
return [
self.url_pattern_constructor.construct(
r'connection/pick/%s$' % url_patterns.PROGRAM,
self, name=self.url_names.CONNECTION_PICK_ORG),
]
def jsonContext(self, data, check, mutator):
"""See base.RequestHandler.jsonContext for specification."""
query = data.models.ndb_org_model.query(
(org_model.Organization.program ==
ndb.Key.from_old_key(data.program.key())),
org_model.Organization.status == org_model.Status.ACCEPTED)
response = melange_lists.JqgridResponse(
melange_lists.ORGANIZATION_LIST_ID,
row=PickOrganizationToConnectListRowRedirect(data, self.url_names))
start = html.escape(data.GET.get('start', ''))
return response.getData(query, start=start)
def context(self, data, check, mutator):
"""See base.GCIRequestHandler.context for specification."""
return {
'page_name': PICK_ORGANIZATION_TO_CONNECT_PAGE_NAME,
'accepted_orgs_list': PickOrganizationToConnectList(data),
}
MARK_CONNECTION_AS_SEEN_BY_ORG_ACCESS_CHECKER = (
access.ConjuctionAccessChecker([
access.PROGRAM_ACTIVE_ACCESS_CHECKER,
IsUserOrgAdminForUrlConnection()
]))
class MarkConnectionAsSeenByOrg(base.RequestHandler):
"""Handler to mark connection as seen by organization."""
access_checker = MARK_CONNECTION_AS_SEEN_BY_ORG_ACCESS_CHECKER
def __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names):
"""Initializes a new instance of the request handler for the specified
parameters.
Args:
initializer: Implementation of initialize.Initializer interface.
linker: Instance of links.Linker class.
renderer: Implementation of render.Renderer interface.
error_handler: Implementation of error.ErrorHandler interface.
url_pattern_constructor:
Implementation of url_patterns.UrlPatternConstructor.
url_names: Instance of url_names.UrlNames.
"""
super(MarkConnectionAsSeenByOrg, self).__init__(
initializer, linker, renderer, error_handler)
self.url_pattern_constructor = url_pattern_constructor
self.url_names = url_names
def djangoURLPatterns(self):
"""See base.RequestHandler.djangoURLPatterns for specification."""
return [
self.url_pattern_constructor.construct(
r'connection/mark_as_seen/org/%s$' % url_patterns.USER_ID,
self, name=self.url_names.CONNECTION_MARK_AS_SEEN_BY_ORG)
]
def post(self, data, check, mutator):
"""See base.RequestHandler.post for specification."""
markConnectionAsSeenByOrg(data.url_connection.key)
return http.HttpResponse()
MARK_CONNECION_AS_SEEN_BY_USER_ACCESS_CHECKER = (
access.ConjuctionAccessChecker([
access.PROGRAM_ACTIVE_ACCESS_CHECKER,
UrlConnectionIsForCurrentUserAccessChecker(),
]))
class MarkConnectionAsSeenByUser(base.RequestHandler):
"""Handler to mark connection as seen by user."""
access_checker = MARK_CONNECION_AS_SEEN_BY_USER_ACCESS_CHECKER
def __init__(self, initializer, linker, renderer, error_handler,
url_pattern_constructor, url_names):
"""Initializes a new instance of the request handler for the specified
parameters.
Args:
initializer: Implementation of initialize.Initializer interface.
linker: Instance of links.Linker class.
renderer: Implementation of render.Renderer interface.
error_handler: Implementation of error.ErrorHandler interface.
url_pattern_constructor:
Implementation of url_patterns.UrlPatternConstructor.
url_names: Instance of url_names.UrlNames.
"""
super(MarkConnectionAsSeenByUser, self).__init__(
initializer, linker, renderer, error_handler)
self.url_pattern_constructor = url_pattern_constructor
self.url_names = url_names
def djangoURLPatterns(self):
"""See base.RequestHandler.djangoURLPatterns for specification."""
return [
self.url_pattern_constructor.construct(
r'connection/mark_as_seen/user/%s$' % url_patterns.USER_ID,
self, name=self.url_names.CONNECTION_MARK_AS_SEEN_BY_USER)
]
def post(self, data, check, mutator):
"""See base.RequestHandler.post for specification."""
markConnectionAsSeenByUser(data.url_connection.key)
return http.HttpResponse()
@ndb.transactional
def createConnectionTxn(
data, profile_key, program, organization,
conversation_updater, message=None, notification_context_provider=None,
recipients=None, org_role=connection_model.NO_ROLE,
user_role=connection_model.NO_ROLE, org_admin=None,
send_org_admin_welcome_email=None, program_messages=None,
seen_by_org=None, seen_by_user=None, init_by_user=None):
"""Creates a new Connection entity, attach any messages provided by the
initiator and send a notification email to the recipient(s).
Args:
data: RequestData object for the current request.
profile_key: Profile key with which to connect.
program: program_model.Program entity for which the connection
is to be created.
organization: Organization with which to connect.
conversation_updater: A ConversationUpdater object to be called if the
profile's conversations need updating.
message: User-provided message for the connection.
context: The notification context method.
notification_context_provider: A provider to obtain context of the
notification email.
recipients: List of one or more recipients for the notification email.
org_role: Org role for the connection.
user_role: User role for the connection.
org_admin: profile entity of organization administrator who started
the connection. Should be supplied only if the connection was initialized
by organization.
send_org_admin_welcome_email: Optional bool value. If True, the organization
member welcome email will be sent, provided an actual role is assigned.
program_messages: program_model.ProgramMessages entity for the
specified program. It needs to be passed only if the welcome email
is requested.
seen_by_org: A bool telling whether the connection should be marked
as seen by the organization.
seen_by_user: A bool telling whether the connection should be marked
as seen by the user.
init_by_user: A bool telling whether the connection is being started by
the user.
Returns:
The newly created Connection entity.
"""
if init_by_user and org_admin:
raise ValueError(
'org_admin cannot be specified for a connection started by the user')
profile = profile_key.get()
can_create = connection_logic.canCreateConnection(
profile, organization.key, init_by_user)
if not can_create:
raise exception.BadRequest(message=can_create.extra)
else:
# create the new connection.
connection = connection_logic.createConnection(
profile, organization.key, user_role, org_role,
seen_by_org=seen_by_org, seen_by_user=seen_by_user,
init_by_user=init_by_user)
# handle possible role assignment
if connection.getRole() == connection_model.MENTOR_ROLE:
profile_logic.assignMentorRoleForOrg(profile, organization.key)
elif connection.getRole() == connection_model.ORG_ADMIN_ROLE:
profile_logic.assignOrgAdminRoleForOrg(profile, organization.key)
# auto-generate a message indicated that the connection has been started
if org_admin:
# connection has been initialized by organization
connection_logic.generateMessageOnStartByOrg(connection, org_admin)
# attach any user-provided messages to the connection.
if message:
connection_logic.createConnectionMessage(
connection.key, message, author_key=org_admin.key).put()
else:
# connection has been initialized by user
connection_logic.generateMessageOnStartByUser(connection.key)
# attach any user-provided messages to the connection.
if message:
connection_logic.createConnectionMessage(
connection.key, message, author_key=profile.key).put()
# dispatch an email to the users.
if notification_context_provider and recipients:
notification_context = notification_context_provider.getContext(
recipients, organization, profile, program, data.site,
connection.key, message)
sub_txn = mailer.getSpawnMailTaskTxn(
notification_context, parent=connection)
sub_txn()
# spawn task to update this user's messages
if conversation_updater:
conversation_updater.updateConversationsForProfile(profile)
return connection
@db.transactional
def createAnonymousConnectionTxn(data, organization, org_role, email, message):
"""Create an AnonymousConnection so that an unregistered user can join
an organization and dispatch an email to the newly Connected user.
Args:
data: RequestData for the current request.
organization: Organization with which to connect.
org_role: Role offered to the user.
email: Email address of the user to which to send the notification.
message: Any message provided by the organization to the user(s).
Returns:
Newly created AnonymousConnection entity.
"""
anonymous_connection = connection_logic.createAnonymousConnection(
org=organization, org_role=org_role, email=email)
notification = notifications.anonymousConnectionContext(
data=data, connection=anonymous_connection, email=email, message=message)
sub_txn = mailer.getSpawnMailTaskTxn(
notification, parent=anonymous_connection)
sub_txn()
return anonymous_connection
@ndb.transactional
def createConnectionMessageTxn(connection_key, author_key, content):
"""Creates a new connection message with the specified content
for the specified connection.
Args:
connection_key: connection key.
author_key: ndb.Key of profile of an author of the comment.
content: a string containing content of the message.
Returns:
a newly created ConnectionMessage entity.
"""
# Retrieve the connection in order to update its last_modified property.
connection = connection_key.get()
if connection_key.parent() == author_key:
# the message is posted by the connection owner
connection.seen_by_org = False
else:
# the message is posted by an organization member
connection.seen_by_user = False
connection.last_modified = datetime.datetime.now()
message = connection_logic.createConnectionMessage(
connection_key, content, author_key=author_key)
ndb.put_multi([connection, message])
# TODO(daniel): emails should be enqueued
return message
@ndb.transactional
def handleUserNoRoleSelectionTxn(connection, conversation_updater):
"""Updates user role of the specified connection and all corresponding
entities with connection_model.NO_ROLE selection.
Please note that it should be checked if the user is actually allowed to
have no role for the organization prior to calling this function.
Args:
connection: connection entity.
conversation_updater: A ConversationUpdater object to be called if the
profile's conversations need updating.
"""
connection = connection.key.get()
if connection.user_role != connection_model.NO_ROLE:
old_user_role = connection.user_role
connection.user_role = connection_model.NO_ROLE
connection.last_modified = datetime.datetime.now()
connection = connection_logic._updateSeenByProperties(
connection, connection_logic.USER_ACTION_ORIGIN)
message = connection_logic.generateMessageOnUpdateByUser(
connection, old_user_role)
ndb.put_multi([connection, message])
profile = connection.key.parent().get()
profile_logic.assignNoRoleForOrg(profile, connection.organization)
if conversation_updater:
conversation_updater.updateConversationsForProfile(profile)
@ndb.transactional
def handleUserRoleSelectionTxn(
connection, conversation_updater, program, program_messages, site):
"""Updates user role of the specified connection and all corresponding
entities with connection_model.ROLE selection.
Please note that it should be checked if the user is actually allowed to
have a role for the organization prior to calling this function.
Args:
connection: connection entity.
conversation_updater: A ConversationUpdater object to be called if the
profile's conversations need updating.
program: program_model.Program entity for the specified connection.
program_messages: program_model.ProgramMessages entity for the specified
program.
site: site_model.Site entity.
"""
connection = connection.key.get()
if connection.user_role != connection_model.ROLE:
old_user_role = connection.user_role
connection.user_role = connection_model.ROLE
connection.last_modified = datetime.datetime.now()
connection = connection_logic._updateSeenByProperties(
connection, connection_logic.USER_ACTION_ORIGIN)
message = connection_logic.generateMessageOnUpdateByUser(
connection, old_user_role)
ndb.put_multi([connection, message])
profile = connection.key.parent().get()
if connection.orgOfferedMentorRole():
profile_logic.assignMentorRoleForOrg(
profile, connection.organization, send_org_member_welcome_email=True,
program=program, program_messages=program_messages, site=site)
# TODO(daniel): generate connection message
elif connection.orgOfferedOrgAdminRole():
profile_logic.assignOrgAdminRoleForOrg(
profile, connection.organization, send_org_member_welcome_email=True,
program=program, program_messages=program_messages, site=site)
# TODO(daniel): generate connection message
if conversation_updater:
conversation_updater.updateConversationsForProfile(profile)
@ndb.transactional
def handleUpdateByOrganization(
connection_key, updated_by, org_role, ignored_by_org,
program, program_messages, site):
"""Updates the specified connection with the specified properties.
This function should be used when the connection is updated by one of
the organization members.
Please note that if role assigned by the organization is changed by this
function, it should be checked whether such a change is actually allowed
for the specified user prior to calling this function.
Args:
connection: connection_model.Connection entity.
updated_by: profile_model.Profile entity of organization administrator who
updates organization role for the connection.
org_role: Role assigned by the organization. Must be one of
connection_model.NO_ROLE, connection_model.MENTOR_ROLE or
connection_model.ORG_ADMIN_ROLE
ignored_by_org: A bool telling whether the connection is ignored by
the organization or not.
program: program_model.Program entity for the specified connection.
program_messages: program_model.ProgramMessages entity for the specified
program.
site: site_model.Site entity.
"""
connection = connection_key.get()
old_properties = connection.to_dict()
is_updated = False
if connection.org_role != org_role:
is_updated = True
profile = connection.key.parent().get()
if org_role == connection_model.NO_ROLE:
connection.org_role = connection_model.NO_ROLE
profile_logic.assignNoRoleForOrg(profile, connection.organization)
elif org_role == connection_model.MENTOR_ROLE:
connection.org_role = connection_model.MENTOR_ROLE
if connection.userRequestedRole():
profile_logic.assignMentorRoleForOrg(
profile, connection.organization, program_messages=program_messages,
send_org_member_welcome_email=True, program=program, site=site)
elif org_role == connection_model.ORG_ADMIN_ROLE:
connection.org_role = connection_model.ORG_ADMIN_ROLE
if connection.userRequestedRole():
profile_logic.assignOrgAdminRoleForOrg(
profile, connection.organization, program_messages=program_messages,
send_org_member_welcome_email=True, program=program, site=site)
if connection.ignored_by_org != ignored_by_org:
is_updated = True
connection.ignored_by_org = ignored_by_org
if is_updated:
connection.last_modified = datetime.datetime.now()
connection = connection_logic._updateSeenByProperties(
connection, connection_logic.ORG_ACTION_ORIGIN)
message = connection_logic.generateMessageOnUpdateByOrg(
connection, updated_by, old_properties)
ndb.put_multi([connection, message])
@ndb.transactional
def markConnectionAsSeenByOrg(connection_key):
"""Marks the specified connection as seen by organization.
Args:
connection: Connection key.
"""
connection = connection_key.get()
connection.seen_by_org = True
connection.put()
@ndb.transactional
def markConnectionAsSeenByUser(connection_key):
"""Marks the specified connection as seen by organization.
Args:
connection: Connection key.
"""
connection = connection_key.get()
connection.seen_by_user = True
connection.put()