fix typo and add more security on API fix some bugs and typos address PR feedback be sure to send emails when accounts already exist PR feedback fix multiple uploads pep8 fixes pep8 fix pylint fixes fix url mapping WL-98 - Complete code coverage - Update code for error and warning messages. - improve code as per some suggestions updated the UI of the auto_enroll feature fixed the errors PR feedback add test add back file filtering add some more error handling of input remove unneeded coffeescript code pylint fixes add pep8 space WL-98 - Updated and added test cases. - Updated membership coffee file for errors display handling. - fixed minor text issues. allow for blank lines and add a test add blank line (pep8)
366 lines
13 KiB
Python
366 lines
13 KiB
Python
"""
|
|
Enrollment operations for use by instructor APIs.
|
|
|
|
Does not include any access control, be sure to check access before calling.
|
|
"""
|
|
|
|
import json
|
|
from django.contrib.auth.models import User
|
|
from django.conf import settings
|
|
from django.core.urlresolvers import reverse
|
|
from django.core.mail import send_mail
|
|
|
|
from student.models import CourseEnrollment, CourseEnrollmentAllowed
|
|
from courseware.models import StudentModule
|
|
from edxmako.shortcuts import render_to_string
|
|
|
|
from submissions import api as sub_api # installed from the edx-submissions repository
|
|
from student.models import anonymous_id_for_user
|
|
|
|
from microsite_configuration import microsite
|
|
|
|
|
|
class EmailEnrollmentState(object):
|
|
""" Store the complete enrollment state of an email in a class """
|
|
def __init__(self, course_id, email):
|
|
exists_user = User.objects.filter(email=email).exists()
|
|
if exists_user:
|
|
user = User.objects.get(email=email)
|
|
exists_ce = CourseEnrollment.is_enrolled(user, course_id)
|
|
full_name = user.profile.name
|
|
else:
|
|
exists_ce = False
|
|
full_name = None
|
|
ceas = CourseEnrollmentAllowed.objects.filter(course_id=course_id, email=email).all()
|
|
exists_allowed = len(ceas) > 0
|
|
state_auto_enroll = exists_allowed and ceas[0].auto_enroll
|
|
|
|
self.user = exists_user
|
|
self.enrollment = exists_ce
|
|
self.allowed = exists_allowed
|
|
self.auto_enroll = bool(state_auto_enroll)
|
|
self.full_name = full_name
|
|
|
|
def __repr__(self):
|
|
return "{}(user={}, enrollment={}, allowed={}, auto_enroll={})".format(
|
|
self.__class__.__name__,
|
|
self.user,
|
|
self.enrollment,
|
|
self.allowed,
|
|
self.auto_enroll,
|
|
)
|
|
|
|
def to_dict(self):
|
|
"""
|
|
example: {
|
|
'user': False,
|
|
'enrollment': False,
|
|
'allowed': True,
|
|
'auto_enroll': True,
|
|
}
|
|
"""
|
|
return {
|
|
'user': self.user,
|
|
'enrollment': self.enrollment,
|
|
'allowed': self.allowed,
|
|
'auto_enroll': self.auto_enroll,
|
|
}
|
|
|
|
|
|
def enroll_email(course_id, student_email, auto_enroll=False, email_students=False, email_params=None):
|
|
"""
|
|
Enroll a student by email.
|
|
|
|
`student_email` is student's emails e.g. "foo@bar.com"
|
|
`auto_enroll` determines what is put in CourseEnrollmentAllowed.auto_enroll
|
|
if auto_enroll is set, then when the email registers, they will be
|
|
enrolled in the course automatically.
|
|
`email_students` determines if student should be notified of action by email.
|
|
`email_params` parameters used while parsing email templates (a `dict`).
|
|
|
|
returns two EmailEnrollmentState's
|
|
representing state before and after the action.
|
|
"""
|
|
previous_state = EmailEnrollmentState(course_id, student_email)
|
|
|
|
if previous_state.user:
|
|
CourseEnrollment.enroll_by_email(student_email, course_id)
|
|
if email_students:
|
|
email_params['message'] = 'enrolled_enroll'
|
|
email_params['email_address'] = student_email
|
|
email_params['full_name'] = previous_state.full_name
|
|
send_mail_to_student(student_email, email_params)
|
|
else:
|
|
cea, _ = CourseEnrollmentAllowed.objects.get_or_create(course_id=course_id, email=student_email)
|
|
cea.auto_enroll = auto_enroll
|
|
cea.save()
|
|
if email_students:
|
|
email_params['message'] = 'allowed_enroll'
|
|
email_params['email_address'] = student_email
|
|
send_mail_to_student(student_email, email_params)
|
|
|
|
after_state = EmailEnrollmentState(course_id, student_email)
|
|
|
|
return previous_state, after_state
|
|
|
|
|
|
def unenroll_email(course_id, student_email, email_students=False, email_params=None):
|
|
"""
|
|
Unenroll a student by email.
|
|
|
|
`student_email` is student's emails e.g. "foo@bar.com"
|
|
`email_students` determines if student should be notified of action by email.
|
|
`email_params` parameters used while parsing email templates (a `dict`).
|
|
|
|
returns two EmailEnrollmentState's
|
|
representing state before and after the action.
|
|
"""
|
|
previous_state = EmailEnrollmentState(course_id, student_email)
|
|
|
|
if previous_state.enrollment:
|
|
CourseEnrollment.unenroll_by_email(student_email, course_id)
|
|
if email_students:
|
|
email_params['message'] = 'enrolled_unenroll'
|
|
email_params['email_address'] = student_email
|
|
email_params['full_name'] = previous_state.full_name
|
|
send_mail_to_student(student_email, email_params)
|
|
|
|
if previous_state.allowed:
|
|
CourseEnrollmentAllowed.objects.get(course_id=course_id, email=student_email).delete()
|
|
if email_students:
|
|
email_params['message'] = 'allowed_unenroll'
|
|
email_params['email_address'] = student_email
|
|
# Since no User object exists for this student there is no "full_name" available.
|
|
send_mail_to_student(student_email, email_params)
|
|
|
|
after_state = EmailEnrollmentState(course_id, student_email)
|
|
|
|
return previous_state, after_state
|
|
|
|
|
|
def send_beta_role_email(action, user, email_params):
|
|
"""
|
|
Send an email to a user added or removed as a beta tester.
|
|
|
|
`action` is one of 'add' or 'remove'
|
|
`user` is the User affected
|
|
`email_params` parameters used while parsing email templates (a `dict`).
|
|
"""
|
|
if action == 'add':
|
|
email_params['message'] = 'add_beta_tester'
|
|
email_params['email_address'] = user.email
|
|
email_params['full_name'] = user.profile.name
|
|
|
|
elif action == 'remove':
|
|
email_params['message'] = 'remove_beta_tester'
|
|
email_params['email_address'] = user.email
|
|
email_params['full_name'] = user.profile.name
|
|
|
|
else:
|
|
raise ValueError("Unexpected action received '{}' - expected 'add' or 'remove'".format(action))
|
|
|
|
send_mail_to_student(user.email, email_params)
|
|
|
|
|
|
def reset_student_attempts(course_id, student, module_state_key, delete_module=False):
|
|
"""
|
|
Reset student attempts for a problem. Optionally deletes all student state for the specified problem.
|
|
|
|
In the previous instructor dashboard it was possible to modify/delete
|
|
modules that were not problems. That has been disabled for safety.
|
|
|
|
`student` is a User
|
|
`problem_to_reset` is the name of a problem e.g. 'L2Node1'.
|
|
To build the module_state_key 'problem/' and course information will be appended to `problem_to_reset`.
|
|
|
|
Raises:
|
|
ValueError: `problem_state` is invalid JSON.
|
|
StudentModule.DoesNotExist: could not load the student module.
|
|
submissions.SubmissionError: unexpected error occurred while resetting the score in the submissions API.
|
|
|
|
"""
|
|
# Reset the student's score in the submissions API
|
|
# Currently this is used only by open assessment (ORA 2)
|
|
# We need to do this *before* retrieving the `StudentModule` model,
|
|
# because it's possible for a score to exist even if no student module exists.
|
|
if delete_module:
|
|
sub_api.reset_score(
|
|
anonymous_id_for_user(student, course_id),
|
|
course_id.to_deprecated_string(),
|
|
module_state_key.to_deprecated_string(),
|
|
)
|
|
|
|
module_to_reset = StudentModule.objects.get(
|
|
student_id=student.id,
|
|
course_id=course_id,
|
|
module_state_key=module_state_key
|
|
)
|
|
|
|
if delete_module:
|
|
module_to_reset.delete()
|
|
else:
|
|
_reset_module_attempts(module_to_reset)
|
|
|
|
|
|
def _reset_module_attempts(studentmodule):
|
|
"""
|
|
Reset the number of attempts on a studentmodule.
|
|
|
|
Throws ValueError if `problem_state` is invalid JSON.
|
|
"""
|
|
# load the state json
|
|
problem_state = json.loads(studentmodule.state)
|
|
# old_number_of_attempts = problem_state["attempts"]
|
|
problem_state["attempts"] = 0
|
|
|
|
# save
|
|
studentmodule.state = json.dumps(problem_state)
|
|
studentmodule.save()
|
|
|
|
|
|
def get_email_params(course, auto_enroll, secure=True):
|
|
"""
|
|
Generate parameters used when parsing email templates.
|
|
|
|
`auto_enroll` is a flag for auto enrolling non-registered students: (a `boolean`)
|
|
Returns a dict of parameters
|
|
"""
|
|
|
|
protocol = 'https' if secure else 'http'
|
|
|
|
stripped_site_name = microsite.get_value(
|
|
'SITE_NAME',
|
|
settings.SITE_NAME
|
|
)
|
|
# TODO: Use request.build_absolute_uri rather than '{proto}://{site}{path}'.format
|
|
# and check with the Services team that this works well with microsites
|
|
registration_url = u'{proto}://{site}{path}'.format(
|
|
proto=protocol,
|
|
site=stripped_site_name,
|
|
path=reverse('student.views.register_user')
|
|
)
|
|
course_url = u'{proto}://{site}{path}'.format(
|
|
proto=protocol,
|
|
site=stripped_site_name,
|
|
path=reverse('course_root', kwargs={'course_id': course.id.to_deprecated_string()})
|
|
)
|
|
|
|
# We can't get the url to the course's About page if the marketing site is enabled.
|
|
course_about_url = None
|
|
if not settings.FEATURES.get('ENABLE_MKTG_SITE', False):
|
|
course_about_url = u'{proto}://{site}{path}'.format(
|
|
proto=protocol,
|
|
site=stripped_site_name,
|
|
path=reverse('about_course', kwargs={'course_id': course.id.to_deprecated_string()})
|
|
)
|
|
|
|
is_shib_course = uses_shib(course)
|
|
|
|
# Composition of email
|
|
email_params = {
|
|
'site_name': stripped_site_name,
|
|
'registration_url': registration_url,
|
|
'course': course,
|
|
'auto_enroll': auto_enroll,
|
|
'course_url': course_url,
|
|
'course_about_url': course_about_url,
|
|
'is_shib_course': is_shib_course,
|
|
}
|
|
return email_params
|
|
|
|
|
|
def send_mail_to_student(student, param_dict):
|
|
"""
|
|
Construct the email using templates and then send it.
|
|
`student` is the student's email address (a `str`),
|
|
|
|
`param_dict` is a `dict` with keys
|
|
[
|
|
`site_name`: name given to edX instance (a `str`)
|
|
`registration_url`: url for registration (a `str`)
|
|
`course_id`: id of course (a `str`)
|
|
`auto_enroll`: user input option (a `str`)
|
|
`course_url`: url of course (a `str`)
|
|
`email_address`: email of student (a `str`)
|
|
`full_name`: student full name (a `str`)
|
|
`message`: type of email to send and template to use (a `str`)
|
|
`is_shib_course`: (a `boolean`)
|
|
]
|
|
|
|
Returns a boolean indicating whether the email was sent successfully.
|
|
"""
|
|
|
|
# add some helpers and microconfig subsitutions
|
|
if 'course' in param_dict:
|
|
param_dict['course_name'] = param_dict['course'].display_name_with_default
|
|
|
|
param_dict['site_name'] = microsite.get_value(
|
|
'SITE_NAME',
|
|
param_dict['site_name']
|
|
)
|
|
|
|
subject = None
|
|
message = None
|
|
|
|
# see if we are running in a microsite and that there is an
|
|
# activation email template definition available as configuration, if so, then render that
|
|
message_type = param_dict['message']
|
|
|
|
email_template_dict = {
|
|
'allowed_enroll': (
|
|
'emails/enroll_email_allowedsubject.txt',
|
|
'emails/enroll_email_allowedmessage.txt'
|
|
),
|
|
'enrolled_enroll': (
|
|
'emails/enroll_email_enrolledsubject.txt',
|
|
'emails/enroll_email_enrolledmessage.txt'
|
|
),
|
|
'allowed_unenroll': (
|
|
'emails/unenroll_email_subject.txt',
|
|
'emails/unenroll_email_allowedmessage.txt'
|
|
),
|
|
'enrolled_unenroll': (
|
|
'emails/unenroll_email_subject.txt',
|
|
'emails/unenroll_email_enrolledmessage.txt'
|
|
),
|
|
'add_beta_tester': (
|
|
'emails/add_beta_tester_email_subject.txt',
|
|
'emails/add_beta_tester_email_message.txt'
|
|
),
|
|
'remove_beta_tester': (
|
|
'emails/remove_beta_tester_email_subject.txt',
|
|
'emails/remove_beta_tester_email_message.txt'
|
|
),
|
|
'account_creation_and_enrollment': (
|
|
'emails/enroll_email_enrolledsubject.txt',
|
|
'emails/account_creation_and_enroll_emailMessage.txt'
|
|
),
|
|
}
|
|
|
|
subject_template, message_template = email_template_dict.get(message_type, (None, None))
|
|
if subject_template is not None and message_template is not None:
|
|
subject = render_to_string(subject_template, param_dict)
|
|
message = render_to_string(message_template, param_dict)
|
|
|
|
if subject and message:
|
|
# Remove leading and trailing whitespace from body
|
|
message = message.strip()
|
|
|
|
# Email subject *must not* contain newlines
|
|
subject = ''.join(subject.splitlines())
|
|
from_address = microsite.get_value(
|
|
'email_from_address',
|
|
settings.DEFAULT_FROM_EMAIL
|
|
)
|
|
|
|
send_mail(subject, message, from_address, [student], fail_silently=False)
|
|
|
|
|
|
def uses_shib(course):
|
|
"""
|
|
Used to return whether course has Shibboleth as the enrollment domain
|
|
|
|
Returns a boolean indicating if Shibboleth authentication is set for this course.
|
|
"""
|
|
return course.enrollment_domain and course.enrollment_domain.startswith(settings.SHIBBOLETH_DOMAIN_PREFIX)
|