|
| 1 | +#!/usr/bin/env python |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | +""" |
| 4 | +Backend for the create_edxapp_user that works under the open-release/lilac.master tag |
| 5 | +""" |
| 6 | +# pylint: disable=import-error, protected-access |
| 7 | +import datetime |
| 8 | +import logging |
| 9 | + |
| 10 | +from common.djangoapps.course_modes.models import CourseMode |
| 11 | +from common.djangoapps.student.models import CourseEnrollment |
| 12 | +from django.contrib.auth.models import User |
| 13 | +from opaque_keys import InvalidKeyError |
| 14 | +from opaque_keys.edx.keys import CourseKey |
| 15 | +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview |
| 16 | +from openedx.core.djangoapps.enrollments import api # pylint: disable=ungrouped-imports |
| 17 | +from openedx.core.djangoapps.enrollments.errors import ( # pylint: disable=ungrouped-imports |
| 18 | + CourseEnrollmentExistsError, |
| 19 | + CourseModeNotFoundError, |
| 20 | +) |
| 21 | +from openedx.core.lib.exceptions import CourseNotFoundError |
| 22 | +from pytz import utc |
| 23 | +from rest_framework.exceptions import APIException, NotFound |
| 24 | + |
| 25 | +from eox_core.edxapp_wrapper.backends.edxfuture_i_v1 import get_program |
| 26 | +from eox_core.edxapp_wrapper.coursekey import get_valid_course_key, validate_org |
| 27 | +from eox_core.edxapp_wrapper.users import check_edxapp_account_conflicts |
| 28 | + |
| 29 | +LOG = logging.getLogger(__name__) |
| 30 | + |
| 31 | + |
| 32 | +def create_enrollment(user, *args, **kwargs): |
| 33 | + """ |
| 34 | + backend function to create enrollment |
| 35 | +
|
| 36 | + Example: |
| 37 | + >>>create_enrollment( |
| 38 | + user_object, |
| 39 | + { |
| 40 | + "course_id": "course-v1-edX-DemoX-1T2015", |
| 41 | + ... |
| 42 | + } |
| 43 | + ) |
| 44 | +
|
| 45 | + """ |
| 46 | + kwargs = dict(kwargs) |
| 47 | + program_uuid = kwargs.pop('bundle_id', None) |
| 48 | + course_id = kwargs.pop('course_id', None) |
| 49 | + |
| 50 | + if program_uuid: |
| 51 | + return _enroll_on_program(user, program_uuid, *args, **kwargs) |
| 52 | + if course_id: |
| 53 | + return _enroll_on_course(user, course_id, *args, **kwargs) |
| 54 | + |
| 55 | + raise APIException("You have to provide a course_id or bundle_id") |
| 56 | + |
| 57 | + |
| 58 | +def update_enrollment(user, course_id, mode, *args, **kwargs): |
| 59 | + """ |
| 60 | + Update enrollment of given user in the course provided. |
| 61 | +
|
| 62 | + Example: |
| 63 | + >>>update_enrollment( |
| 64 | + user_object, |
| 65 | + course_id, |
| 66 | + mode, |
| 67 | + is_active=False, |
| 68 | + enrollment_attributes=[ |
| 69 | + { |
| 70 | + "namespace": "credit", |
| 71 | + "name": "provider_id", |
| 72 | + "value": "hogwarts", |
| 73 | + }, |
| 74 | + {...} |
| 75 | + ] |
| 76 | + } |
| 77 | + ) |
| 78 | + """ |
| 79 | + username = user.username |
| 80 | + |
| 81 | + is_active = kwargs.get('is_active', True) |
| 82 | + enrollment_attributes = kwargs.get('enrollment_attributes', None) |
| 83 | + |
| 84 | + LOG.info('Updating enrollment for student: %s of course: %s mode: %s', username, course_id, mode) |
| 85 | + enrollment = api._data_api().update_course_enrollment(username, course_id, mode, is_active) |
| 86 | + if not enrollment: |
| 87 | + raise NotFound('No enrollment found for {}'.format(username)) |
| 88 | + if enrollment_attributes is not None: |
| 89 | + api.set_enrollment_attributes(username, course_id, enrollment_attributes) |
| 90 | + |
| 91 | + return { |
| 92 | + 'user': enrollment['user'], |
| 93 | + 'course_id': course_id, |
| 94 | + 'mode': enrollment['mode'], |
| 95 | + 'is_active': enrollment['is_active'], |
| 96 | + 'enrollment_attributes': enrollment_attributes, |
| 97 | + } |
| 98 | + |
| 99 | + |
| 100 | +def get_enrollment(*args, **kwargs): |
| 101 | + """ |
| 102 | + Return enrollment of given user in the course provided. |
| 103 | +
|
| 104 | + Example: |
| 105 | + >>>get_enrollment( |
| 106 | + { |
| 107 | + "username": "Bob", |
| 108 | + "course_id": "course-v1-edX-DemoX-1T2015" |
| 109 | + } |
| 110 | + ) |
| 111 | + """ |
| 112 | + errors = [] |
| 113 | + course_id = kwargs.pop('course_id', None) |
| 114 | + username = kwargs.get('username', None) |
| 115 | + |
| 116 | + try: |
| 117 | + LOG.info('Getting enrollment information of student: %s course: %s', username, course_id) |
| 118 | + enrollment = api.get_enrollment(username, course_id) |
| 119 | + if not enrollment: |
| 120 | + errors.append('No enrollment found for user:`{}`'.format(username)) |
| 121 | + return None, errors |
| 122 | + except InvalidKeyError: |
| 123 | + errors.append('No course found for course_id `{}`'.format(course_id)) |
| 124 | + return None, errors |
| 125 | + enrollment['enrollment_attributes'] = api.get_enrollment_attributes(username, course_id) |
| 126 | + enrollment['course_id'] = course_id |
| 127 | + return enrollment, errors |
| 128 | + |
| 129 | + |
| 130 | +def delete_enrollment(*args, **kwargs): |
| 131 | + """ |
| 132 | + Delete enrollment and enrollment attributes of given user in the course provided. |
| 133 | +
|
| 134 | + Example: |
| 135 | + >>>delete_enrollment( |
| 136 | + { |
| 137 | + "user": user_object, |
| 138 | + "course_id": course-v1-edX-DemoX-1T2015" |
| 139 | + ) |
| 140 | + """ |
| 141 | + course_id = kwargs.pop('course_id', None) |
| 142 | + user = kwargs.get('user') |
| 143 | + try: |
| 144 | + course_key = get_valid_course_key(course_id) |
| 145 | + except InvalidKeyError: |
| 146 | + raise NotFound('No course found by course id `{}`'.format(course_id)) |
| 147 | + |
| 148 | + username = user.username |
| 149 | + |
| 150 | + LOG.info('Deleting enrollment. User: `%s` course: `%s`', username, course_id) |
| 151 | + enrollment = CourseEnrollment.get_enrollment(user, course_key) |
| 152 | + if not enrollment: |
| 153 | + raise NotFound('No enrollment found for user: `{}` on course_id `{}`'.format(username, course_id)) |
| 154 | + try: |
| 155 | + enrollment.delete() |
| 156 | + except Exception: |
| 157 | + raise NotFound('Error: Enrollment could not be deleted for user: `{}` on course_id `{}`'.format(username, course_id)) |
| 158 | + |
| 159 | + |
| 160 | +def _enroll_on_course(user, course_id, *args, **kwargs): |
| 161 | + """ |
| 162 | + enroll user on a single course |
| 163 | +
|
| 164 | + Example: |
| 165 | + >>>_enroll_on_course( |
| 166 | + { |
| 167 | + "user": user_object, |
| 168 | + "course_id": course-v1-edX-DemoX-1T2015", |
| 169 | + "is_active": "False", |
| 170 | + "mode": "audit", |
| 171 | + "enrollment_attributes": [ |
| 172 | + { |
| 173 | + "namespace": "credit", |
| 174 | + "name": "provider_id", |
| 175 | + "value": "hogwarts", |
| 176 | + }, |
| 177 | + {...} |
| 178 | + ] |
| 179 | + } |
| 180 | + ) |
| 181 | + """ |
| 182 | + errors = [] |
| 183 | + |
| 184 | + username = user.username |
| 185 | + |
| 186 | + mode = kwargs.get('mode', 'audit') |
| 187 | + is_active = kwargs.get('is_active', True) |
| 188 | + force = kwargs.get('force', False) |
| 189 | + enrollment_attributes = kwargs.get('enrollment_attributes', None) |
| 190 | + |
| 191 | + enrollment_valid_query = { |
| 192 | + 'course_id': course_id, |
| 193 | + 'force': force, |
| 194 | + 'mode': mode, |
| 195 | + 'username': username, |
| 196 | + } |
| 197 | + validation_errors = check_edxapp_enrollment_is_valid(**enrollment_valid_query) |
| 198 | + if validation_errors: |
| 199 | + return None, [", ".join(validation_errors)] |
| 200 | + |
| 201 | + try: |
| 202 | + LOG.info('Creating regular enrollment %s, %s, %s', username, course_id, mode) |
| 203 | + enrollment = _create_or_update_enrollment(username, course_id, mode, is_active, force) |
| 204 | + except CourseNotFoundError as err: |
| 205 | + raise NotFound(repr(err)) |
| 206 | + except Exception as err: # pylint: disable=broad-except |
| 207 | + if force: |
| 208 | + LOG.info('Force create enrollment %s, %s, %s', username, course_id, mode) |
| 209 | + enrollment = _force_create_enrollment(username, course_id, mode, is_active) |
| 210 | + else: |
| 211 | + if not str(err): |
| 212 | + err = err.__class__.__name__ |
| 213 | + raise APIException(detail=err) |
| 214 | + |
| 215 | + if enrollment_attributes is not None: |
| 216 | + api.set_enrollment_attributes(username, course_id, enrollment_attributes) |
| 217 | + try: |
| 218 | + enrollment['enrollment_attributes'] = enrollment_attributes |
| 219 | + enrollment['course_id'] = course_id |
| 220 | + except TypeError: |
| 221 | + pass |
| 222 | + return enrollment, errors |
| 223 | + |
| 224 | + |
| 225 | +def _enroll_on_program(user, program_uuid, *arg, **kwargs): |
| 226 | + """ |
| 227 | + enroll user on each of the courses of a program |
| 228 | + """ |
| 229 | + results = [] |
| 230 | + errors = [] |
| 231 | + LOG.info('Enrolling on program: %s', program_uuid) |
| 232 | + try: |
| 233 | + data = get_program(program_uuid) |
| 234 | + except Exception as err: # pylint: disable=broad-except |
| 235 | + raise NotFound(repr(err)) |
| 236 | + if not data['courses']: |
| 237 | + raise NotFound("No courses found for this program") |
| 238 | + for course in data['courses']: |
| 239 | + if course['course_runs']: |
| 240 | + course_run = _get_preferred_course_run(course) |
| 241 | + LOG.info('Enrolling on course_run: %s', course_run['key']) |
| 242 | + course_id = course_run['key'] |
| 243 | + try: |
| 244 | + result, errors_list = _enroll_on_course(user, course_id, *arg, **kwargs) |
| 245 | + except APIException as error: |
| 246 | + result = { |
| 247 | + 'username': user.username, |
| 248 | + 'mode': None, |
| 249 | + 'course_id': course_id, |
| 250 | + } |
| 251 | + errors_list = [error.detail] |
| 252 | + |
| 253 | + results.append(result) |
| 254 | + errors.append(errors_list) |
| 255 | + else: |
| 256 | + raise NotFound("No course runs available for this course") |
| 257 | + return results, errors |
| 258 | + |
| 259 | + |
| 260 | +def _get_preferred_course_run(course): |
| 261 | + """ |
| 262 | + Returns the course run more likely to be the intended one |
| 263 | + """ |
| 264 | + sorted_course_runs = sorted(course['course_runs'], key=lambda run: run['start']) |
| 265 | + |
| 266 | + for run in sorted_course_runs: |
| 267 | + default_enrollment_start_date = datetime.datetime(1900, 1, 1, tzinfo=utc) |
| 268 | + course_run_key = CourseKey.from_string(run['key']) |
| 269 | + course_overview = CourseOverview.get_from_id(course_run_key) |
| 270 | + enrollment_end = course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=utc) |
| 271 | + enrollment_start = course_overview.enrollment_start or default_enrollment_start_date |
| 272 | + run['is_enrollment_open'] = enrollment_start <= datetime.datetime.now(utc) < enrollment_end |
| 273 | + |
| 274 | + open_course_runs = [run for run in sorted_course_runs if run['is_enrollment_open']] |
| 275 | + course_run = open_course_runs[0] if open_course_runs else sorted_course_runs[-1] |
| 276 | + return course_run |
| 277 | + |
| 278 | + |
| 279 | +# pylint: disable=invalid-name |
| 280 | +def check_edxapp_enrollment_is_valid(*args, **kwargs): |
| 281 | + """ |
| 282 | + backend function to check if enrollment is valid |
| 283 | + """ |
| 284 | + errors = [] |
| 285 | + is_active = kwargs.get("is_active", True) |
| 286 | + course_id = kwargs.get("course_id") |
| 287 | + force = kwargs.get('force', False) |
| 288 | + mode = kwargs.get("mode") |
| 289 | + program_uuid = kwargs.get('bundle_id') |
| 290 | + username = kwargs.get("username") |
| 291 | + email = kwargs.get("email") |
| 292 | + |
| 293 | + if program_uuid and course_id: |
| 294 | + return ['You have to provide a course_id or bundle_id but not both'] |
| 295 | + if not program_uuid and not course_id: |
| 296 | + return ['You have to provide a course_id or bundle_id'] |
| 297 | + if not email and not username: |
| 298 | + return ['Email or username needed'] |
| 299 | + if not check_edxapp_account_conflicts(email=email, username=username): |
| 300 | + return ['User not found'] |
| 301 | + if mode not in CourseMode.ALL_MODES: |
| 302 | + return ['Invalid mode given:' + mode] |
| 303 | + if course_id: |
| 304 | + if not validate_org(course_id): |
| 305 | + errors.append('Enrollment not allowed for given org') |
| 306 | + if course_id and not force: |
| 307 | + try: |
| 308 | + api.validate_course_mode(course_id, mode, is_active=is_active) |
| 309 | + except CourseModeNotFoundError: |
| 310 | + errors.append('Mode not found') |
| 311 | + except CourseNotFoundError: |
| 312 | + errors.append('Course not found') |
| 313 | + return errors |
| 314 | + |
| 315 | + |
| 316 | +def _create_or_update_enrollment(username, course_id, mode, is_active, try_update): |
| 317 | + """ |
| 318 | + non-forced create or update enrollment internal function |
| 319 | + """ |
| 320 | + try: |
| 321 | + enrollment = api._data_api().create_course_enrollment(username, course_id, mode, is_active) |
| 322 | + except CourseEnrollmentExistsError as err: |
| 323 | + if try_update: |
| 324 | + enrollment = api._data_api().update_course_enrollment(username, course_id, mode, is_active) |
| 325 | + else: |
| 326 | + raise Exception(repr(err) + ", use force to update the existing enrollment") |
| 327 | + return enrollment |
| 328 | + |
| 329 | + |
| 330 | +def _force_create_enrollment(username, course_id, mode, is_active): |
| 331 | + """ |
| 332 | + forced create enrollment internal function |
| 333 | + """ |
| 334 | + try: |
| 335 | + course_key = get_valid_course_key(course_id) |
| 336 | + user = User.objects.get(username=username) |
| 337 | + enrollment = CourseEnrollment.enroll(user, course_key, check_access=False) |
| 338 | + api._data_api()._update_enrollment(enrollment, is_active=is_active, mode=mode) |
| 339 | + except Exception as err: # pylint: disable=broad-except |
| 340 | + raise APIException(repr(err)) |
| 341 | + return enrollment |
0 commit comments