Skip to content

Commit 50c05c9

Browse files
Merge pull request #146 from eduNEXT/MJG/lilac_missing_backends
fix: add missing backends for lilac release
2 parents fa48452 + 9e3bdf7 commit 50c05c9

File tree

1 file changed

+341
-0
lines changed

1 file changed

+341
-0
lines changed
Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
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

Comments
 (0)