blob: e37a41eba1bb3452bdad532a7ce0f3eef3a45a55 [file] [log] [blame]
# Copyright 2013 the Melange authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Logic for profiles."""
from google.appengine.api import datastore_errors
from google.appengine.ext import ndb
from melange import types
from melange.models import profile as profile_model
from melange.utils import rich_bool
from melange.appengine import db as melange_db
from soc.logic import mail_dispatcher
from soc.models import program as program_model
ONLY_ORG_ADMIN = 'only_org_admin'
PROFILE_EXISTS = unicode(
'A profile has already been registered for this program and this user.')
PROFILE_DOES_NOT_EXIST = unicode(
'No profile exists for the specified key: %s')
_DEF_ORG_MEMBER_WELCOME_MAIL_SUBJECT = unicode('Welcome as organization member')
def canResignAsOrgAdminForOrg(profile, org_key, models=types.MELANGE_MODELS):
"""Tells whether the specified profile can resign from their organization
administrator role for the specified organization.
An organization administrator may be removed from the list of administrators
of an organization, if there is at least one other user with this role.
Args:
profile: Profile entity.
org_key: Organization key.
models: Instance of types.Models that represent appropriate models.
Returns:
RichBool whose value is set to True, if the organization administrator
is allowed to resign. Otherwise, RichBool whose value is set to False
and extra part is a string that represents the reason why the user
is not allowed to resign.
"""
if org_key not in profile.admin_for:
raise ValueError(
'The specified profile is not an organization administrator for %s' %
org_key.id())
# retrieve keys of other org admins
org_admin_keys = getOrgAdmins(org_key, keys_only=True, models=models)
org_admin_keys.remove(profile.key)
# try to retrieve the first org admin from the list
# therefore, it can be safely used within a XG transaction
if org_admin_keys and org_admin_keys[0].get():
return rich_bool.TRUE
else:
return rich_bool.RichBool(False, extra=ONLY_ORG_ADMIN)
def isMentorRoleEligibleForOrg(profile, org_key):
"""Tells whether the specified user is eligible to have only mentor role
for the specified organization.
A user is eligible for mentor role only if he or she can resign from
organization administrator role, if the person has one.
Please note that this function executes a non-ancestor query, so it cannot
be safely used within transactions.
Args:
profile: profile entity.
org_key: organization key.
Returns:
RichBool whose value is set to True, if the user is eligible for mentor
only 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 have mentor role only at this time.
"""
if org_key in profile.admin_for:
return canResignAsOrgAdminForOrg(profile, org_key)
else:
return rich_bool.TRUE
def queryAllMentorsForProgram(program_key):
"""Returns a query that fetches all profiles of mentors for the specified
program.
Args:
program_key: Program key.
Returns:
A query instance that fetches all profiles of mentors for the specified
program.
"""
program_key = ndb.Key.from_old_key(program_key)
return profile_model.Profile.query(
profile_model.Profile.program == program_key,
profile_model.Profile.is_mentor == True)
def queryAllMentorsForOrg(org_key):
"""Returns a query that fetches all profiles of mentors for the specified
organization.
Args:
org_key: Organization key.
returns:
A query instance that fetches all profiles of mentors for the specified
organization.
"""
return profile_model.Profile.query(
profile_model.Profile.mentor_for == org_key)
def queryAllStudentsForProgram(program_key):
"""Returns a query that fetches all profiles of students for the specified
program.
Args:
program_key: Program key.
Returns:
A query instance that fetches all profiles of students for the specified
program.
"""
program_key = ndb.Key.from_old_key(program_key)
return profile_model.Profile.query(
profile_model.Profile.program == program_key,
profile_model.Profile.is_student == True)
def getOrgAdmins(org_key, keys_only=False, extra_attrs=None,
models=types.MELANGE_MODELS):
"""Returns organization administrators for the specified organization.
Additional constraints on administrators may be specified by passing a custom
extra_attrs dictionary. Each element of the dictionary maps a property
with a requested value. The value must be a sequence.
Please note that this function executes a non-ancestor query, so it cannot
be safely used within transactions.
Args:
org_key: organization key
keys_only: If true, return only keys instead of complete entities
extra_args: a dictionary containing additional constraints on
organization administrators to retrieve
models: instance of types.Models that represent appropriate models.
Returns:
list of profiles entities or keys of organization administrators
"""
query = profile_model.Profile.query(
profile_model.Profile.admin_for == org_key,
profile_model.Profile.status == profile_model.Status.ACTIVE)
query = _handleExtraAttrs(query, extra_attrs)
return query.fetch(limit=1000, keys_only=keys_only)
def assignNoRoleForOrg(profile, org_key):
"""Removes any elevated role for the specified profile for the
specified organization.
Args:
profile: Profile entity.
org_key: Organization key.
"""
if org_key in profile.mentor_for:
profile.mentor_for.remove(org_key)
if org_key in profile.admin_for:
profile.admin_for.remove(org_key)
profile.put()
def assignMentorRoleForOrg(profile, org_key,
send_org_member_welcome_email=None, program=None,
program_messages=None, site=None):
"""Assigns the specified profile a mentor role for the specified
organization. If a user is currently an organization administrator,
they will be lowered to a mentor role.
Args:
profile: Profile entity.
org_key: Organization key.
send_org_member_welcome_email: Optional bool value. If set to True
the welcome email will be sent to the user provided he or she
has not received one so far.
program: Optional program_model.Program entity. It needs to be specified
if the welcome email is supposed to be sent out.
program_messages: Optional program_model.ProgramMessages entity. It needs
to be specified if the welcome email is supposed to be sent out.
site: Optional site_model.Site entity. It needs
to be specified if the welcome email is supposed to be sent out.
"""
if (send_org_member_welcome_email and
not (program and program_messages and site)):
raise ValueError(
'If the welcome email is supposed to be sent, all of program, '
'program_messages and site attributes must be set.')
if org_key in profile.admin_for:
profile.admin_for.remove(org_key)
profile.mentor_for = list(set(profile.mentor_for + [org_key]))
if (send_org_member_welcome_email and
profile_model.MessageType.ORG_MEMBER_WELCOME_MSG
not in profile.sent_messages):
profile.sent_messages.append(
profile_model.MessageType.ORG_MEMBER_WELCOME_MSG)
dispatchOrgMemberWelcomeEmail(profile, program, program_messages, site)
profile.put()
def assignOrgAdminRoleForOrg(profile, org_key,
send_org_member_welcome_email=None, program=None,
program_messages=None, site=None):
"""Assigns the specified profile an organization administrator role
for the specified organization.
Args:
profile: Profile entity.
org_key: Organization key.
send_org_member_welcome_email: Optional bool value. If set to True
the welcome email will be sent to the user provided he or she
has not received one so far.
program: Optional program_model.Program entity. It needs to be specified
if the welcome email is supposed to be sent out.
program_messages: Optional program_model.ProgramMessages entity. It needs
to be specified if the welcome email is supposed to be sent out.
site: Optional site_model.Site entity. It needs
to be specified if the welcome email is supposed to be sent out.
"""
if (send_org_member_welcome_email and
not (program and program_messages and site)):
raise ValueError(
'If the welcome email is supposed to be sent, all of program, '
'program_messages and site attributes must be set.')
if org_key not in profile.admin_for:
if org_key not in profile.mentor_for:
profile.mentor_for.append(org_key)
profile.admin_for.append(org_key)
if (send_org_member_welcome_email and
profile_model.MessageType.ORG_MEMBER_WELCOME_MSG
not in profile.sent_messages):
profile.sent_messages.append(
profile_model.MessageType.ORG_MEMBER_WELCOME_MSG)
dispatchOrgMemberWelcomeEmail(profile, program, program_messages, site)
profile.put()
def getProfileForUsername(username, program_key, models=types.MELANGE_MODELS):
"""Returns profile entity for a user with the specified username and
for the specified program.
Args:
username: a string containing username of the user.
program_key: program key.
models: instance of types.Models that represent appropriate models.
Returns:
profile entity for the specified user and program or None if the user
does not have a profile for this program.
"""
profile_key = ndb.Key(
models.user_model._get_kind(), username,
models.ndb_profile_model._get_kind(),
'%s/%s' % (program_key.name(), username))
return profile_key.get()
def _handleExtraAttrs(query, extra_attrs):
"""Extends the specified query by handling extra attributes.
The attributes are specified in the passed dictionary. Each element of
the dictionary maps a property with a requested value. The value must
be a sequence (list or tuple).
Args:
query: Query to extend.
extra_attrs: A dict containing additional constraints on the query.
Returns:
A new query instance with additional filters applied.
"""
if extra_attrs:
for prop, value in extra_attrs.iteritems():
query = melange_db.addFilterToNDBQuery(query, prop, value)
return query
def getProfileKey(sponsor_id, program_id, user_id, models=None):
"""Constructs ndb.Key of a profile for the specified sponsor,
program and user identifiers.
Args:
sponsor_id: Sponsor identifier.
program_id: Program identifier.
user_id: User identifier.
models: instance of types.Models that represent appropriate models.
Returns:
ndb.Key instance of a profile entity with the specified properties.
"""
models = models or types.MELANGE_MODELS
return ndb.Key(
models.user_model._get_kind(), user_id,
models.ndb_profile_model._get_kind(),
'%s/%s/%s' % (sponsor_id, program_id, user_id))
def createProfile(
user_key, program_key, profile_properties, models=types.MELANGE_MODELS):
"""Creates a new profile entity based on the supplied properties.
Args:
user_key: User key for the profile to register.
program: Program key.
profile_properties: A dict mapping profile properties to their values.
models: instance of types.Models that represent appropriate models.
Returns:
RichBool whose value is set to True if profile has been successfully
created. In that case, extra part points to the newly created profile
entity. Otherwise, RichBool whose value is set to False and extra part is
a string that represents the reason why the action could not be completed.
"""
# check if a profile entity for the user and the program already exists.
profile_key = getProfileKey(
program_model.getSponsorId(program_key),
program_model.getProgramId(program_key),
user_key.id(), models=models)
if profile_key.get():
return rich_bool.RichBool(False, PROFILE_EXISTS)
else:
try:
program_key = ndb.Key.from_old_key(program_key)
profile = models.ndb_profile_model(
key=profile_key, program=program_key, **profile_properties)
profile.put()
return rich_bool.RichBool(True, profile)
except datastore_errors.BadValueError as e:
return rich_bool.RichBool(False, str(e))
def editProfile(profile_key, profile_properties):
"""Edits profile with the specified key based on the supplied properties.
Args:
profile_key: Profile key of an existing profile to edit.
profile_properties: A dict mapping profile properties to their values.
Returns:
RichBool whose value is set to True if profile has been successfully
updated. In that case, extra part points to the updated profile entity.
Otherwise, RichBool whose value is set to False and extra part is a string
that represents the reason why the action could not be completed.
"""
profile = profile_key.get()
if not profile:
return rich_bool.RichBool(False, PROFILE_DOES_NOT_EXIST % profile_key.id())
else:
try:
profile.populate(**profile_properties)
profile.put()
return rich_bool.RichBool(True, profile)
except datastore_errors.BadValueError as e:
return rich_bool.RichBool(False, str(e))
def createStudentData(student_data_properties, models=types.MELANGE_MODELS):
"""Creates a new student data object based on the specified properties.
Args:
student_data_properties: A dict mapping profile properties to their values.
models: Instance of types.Models that represent appropriate models.
Returns:
Newly created student data entity.
"""
return models.student_data_model(**student_data_properties)
def dispatchOrgMemberWelcomeEmail(
profile, program, program_messages, site, parent=None):
"""Dispatches a task to send organization member welcome email for
the program to the specified profile.
Args:
profile: profile_model.Profile entity to which the email should be sent.
program: program_model.Program entity.
program_messages: program_model.ProgramMessages entity.
site: site_model.Site entity.
parent: Optional entity to use as the parent of the entity which is
created during the process. If not specified, the specified profile
entity is used.
"""
if program_messages.mentor_welcome_msg:
sender_name, sender = mail_dispatcher.getDefaultMailSender(site=site)
parent = parent or profile
context = {
'to': profile.contact.email,
'sender': sender,
'sender_name': sender_name,
'subject': _DEF_ORG_MEMBER_WELCOME_MAIL_SUBJECT,
'program_name': program.name,
'to_name': profile.public_name,
}
mail_dispatcher.getSendMailFromTemplateStringTxn(
program_messages.mentor_welcome_msg, context, parent=parent)()
def createtNotificationSettings(notification_settings_properties):
"""Creates a new notification settings object based on
the specified properties.
Args:
notification_settings_properties: A dict mapping notification
settings properties to their values.
Returns:
Newly created notification settings object.
"""
return profile_model.NotificationSettings(**notification_settings_properties)
def enrollmentFormSubmitted(profile):
"""Tells whether the specified student has submitted their enrollment form.
Args:
profile: profile_model.Profile entity.
Returns:
True, if the specified profile has submitted their enrollment form.
Otherwise, False is returned.
Raises:
ValueError: if the specified profile is not a student.
"""
if not profile.is_student:
raise ValueError('The specified profile is not a student')
else:
return profile.student_data.enrollment_form is not None