enrollment.py 10.1 KB
Newer Older
1 2 3 4 5 6
"""
Enrollment operations for use by instructor APIs.

Does not include any access control, be sure to check access before calling.
"""

7
import json
8
from django.contrib.auth.models import User
9 10 11 12
from django.conf import settings
from django.core.urlresolvers import reverse
from django.core.mail import send_mail

13
from student.models import CourseEnrollment, CourseEnrollmentAllowed
14
from courseware.models import StudentModule
David Baumgold committed
15
from edxmako.shortcuts import render_to_string
16

17
from microsite_configuration import microsite
18

19 20
# For determining if a shibboleth course
SHIBBOLETH_DOMAIN_PREFIX = 'shib:'
21 22


23 24 25 26
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()
27 28 29
        if exists_user:
            user = User.objects.get(email=email)
            exists_ce = CourseEnrollment.is_enrolled(user, course_id)
30
            full_name = user.profile.name
31 32
        else:
            exists_ce = False
33
            full_name = None
34 35 36 37 38 39 40 41
        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)
42
        self.full_name = full_name
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69

    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,
        }


70
def enroll_email(course_id, student_email, auto_enroll=False, email_students=False, email_params=None):
71
    """
72
    Enroll a student by email.
73

74 75 76 77
    `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.
78 79
    `email_students` determines if student should be notified of action by email.
    `email_params` parameters used while parsing email templates (a `dict`).
80

81 82
    returns two EmailEnrollmentState's
        representing state before and after the action.
83 84
    """

85
    previous_state = EmailEnrollmentState(course_id, student_email)
86

87
    if previous_state.user:
88
        CourseEnrollment.enroll_by_email(student_email, course_id)
89 90 91 92 93
        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)
94 95 96 97
    else:
        cea, _ = CourseEnrollmentAllowed.objects.get_or_create(course_id=course_id, email=student_email)
        cea.auto_enroll = auto_enroll
        cea.save()
98 99 100 101
        if email_students:
            email_params['message'] = 'allowed_enroll'
            email_params['email_address'] = student_email
            send_mail_to_student(student_email, email_params)
102

103 104 105
    after_state = EmailEnrollmentState(course_id, student_email)

    return previous_state, after_state
106 107


108
def unenroll_email(course_id, student_email, email_students=False, email_params=None):
109
    """
110
    Unenroll a student by email.
111

112
    `student_email` is student's emails e.g. "foo@bar.com"
113 114
    `email_students` determines if student should be notified of action by email.
    `email_params` parameters used while parsing email templates (a `dict`).
115

116 117
    returns two EmailEnrollmentState's
        representing state before and after the action.
118 119
    """

120 121 122
    previous_state = EmailEnrollmentState(course_id, student_email)

    if previous_state.enrollment:
123
        CourseEnrollment.unenroll_by_email(student_email, course_id)
124 125 126 127 128
        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)
129 130 131

    if previous_state.allowed:
        CourseEnrollmentAllowed.objects.get(course_id=course_id, email=student_email).delete()
132 133 134 135 136
        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)
137 138

    after_state = EmailEnrollmentState(course_id, student_email)
139

140
    return previous_state, after_state
141 142


Miles Steele committed
143
def reset_student_attempts(course_id, student, module_state_key, delete_module=False):
144 145 146 147 148 149
    """
    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.

150 151 152 153 154
    `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`.

    Throws ValueError if `problem_state` is invalid JSON.
155 156 157 158 159 160 161 162 163 164 165 166
    """
    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):
167 168 169 170 171
    """
    Reset the number of attempts on a studentmodule.

    Throws ValueError if `problem_state` is invalid JSON.
    """
172 173 174 175 176 177 178 179
    # 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()
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227


def get_email_params(course, auto_enroll):
    """
    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
    """

    stripped_site_name = settings.SITE_NAME
    registration_url = 'https://' + stripped_site_name + reverse('student.views.register_user')
    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': 'https://' + stripped_site_name + '/courses/' + course.id,
        'course_about_url': 'https://' + stripped_site_name + '/courses/' + course.id + '/about',
        '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.
    """

228 229 230 231
    # add some helpers and microconfig subsitutions
    if 'course' in param_dict:
        param_dict['course_name'] = param_dict['course'].display_name_with_default

232
    param_dict['site_name'] = microsite.get_value(
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
        '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'
        )
    }
262

263
    subject_template, message_template = email_template_dict.get(message_type, (None, None))
264 265 266 267
    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)

268
    if subject and message:
269 270 271 272 273
        # Remove leading and trailing whitespace from body
        message = message.strip()

        # Email subject *must not* contain newlines
        subject = ''.join(subject.splitlines())
274
        from_address = microsite.get_value(
275 276 277 278 279
            'email_from_address',
            settings.DEFAULT_FROM_EMAIL
        )

        send_mail(subject, message, from_address, [student], fail_silently=False)
280 281 282 283 284 285 286 287 288


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(SHIBBOLETH_DOMAIN_PREFIX)