Integration of edx_proctoring into the LMS
This commit is contained in:
132
openedx/core/djangoapps/credit/services.py
Normal file
132
openedx/core/djangoapps/credit/services.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
Implementation of "credit" XBlock service
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_course_key(course_key_or_id):
|
||||
"""
|
||||
Helper method to get a course key eith from a string or a CourseKey,
|
||||
where the CourseKey will simply be returned
|
||||
"""
|
||||
return (
|
||||
CourseKey.from_string(course_key_or_id)
|
||||
if isinstance(course_key_or_id, basestring)
|
||||
else course_key_or_id
|
||||
)
|
||||
|
||||
|
||||
class CreditService(object):
|
||||
"""
|
||||
Course Credit XBlock service
|
||||
"""
|
||||
|
||||
def get_credit_state(self, user_id, course_key_or_id):
|
||||
"""
|
||||
Return all information about the user's credit state inside of a given
|
||||
course.
|
||||
|
||||
ARGS:
|
||||
- user_id: The PK of the User in question
|
||||
- course_key: The course ID (as string or CourseKey)
|
||||
|
||||
RETURNS:
|
||||
NONE (user not found or is not enrolled or is not credit course)
|
||||
- or -
|
||||
{
|
||||
'enrollment_mode': the mode that the user is enrolled in the course
|
||||
'profile_fullname': the name that the student registered under, used for verification
|
||||
'credit_requirement_status': the user's status in fulfilling those requirements
|
||||
}
|
||||
"""
|
||||
|
||||
# This seems to need to be here otherwise we get
|
||||
# circular references when starting up the app
|
||||
from openedx.core.djangoapps.credit.api.eligibility import (
|
||||
is_credit_course,
|
||||
get_credit_requirement_status,
|
||||
)
|
||||
|
||||
# since we have to do name matching during various
|
||||
# verifications, User must have a UserProfile
|
||||
try:
|
||||
user = User.objects.select_related('profile').get(id=user_id)
|
||||
except ObjectDoesNotExist:
|
||||
# bad user_id
|
||||
return None
|
||||
|
||||
course_key = _get_course_key(course_key_or_id)
|
||||
|
||||
enrollment = CourseEnrollment.get_enrollment(user, course_key)
|
||||
if not enrollment or not enrollment.is_active:
|
||||
# not enrolled
|
||||
return None
|
||||
|
||||
if not is_credit_course(course_key):
|
||||
return None
|
||||
|
||||
return {
|
||||
'enrollment_mode': enrollment.mode,
|
||||
'profile_fullname': user.profile.name,
|
||||
'credit_requirement_status': get_credit_requirement_status(course_key, user.username)
|
||||
}
|
||||
|
||||
def set_credit_requirement_status(self, user_id, course_key_or_id, req_namespace,
|
||||
req_name, status="satisfied", reason=None):
|
||||
"""
|
||||
A simple wrapper around the method of the same name in api.eligibility.py. The only difference is
|
||||
that a user_id is passed in.
|
||||
|
||||
For more information, see documentation on this method name in api.eligibility.py
|
||||
"""
|
||||
|
||||
# always log any update activity to the credit requirements
|
||||
# table. This will be to help debug any issues that might
|
||||
# arise in production
|
||||
log_msg = (
|
||||
'set_credit_requirement_status was called with '
|
||||
'user_id={user_id}, course_key_or_id={course_key_or_id} '
|
||||
'req_namespace={req_namespace}, req_name={req_name}, '
|
||||
'status={status}, reason={reason}'.format(
|
||||
user_id=user_id,
|
||||
course_key_or_id=course_key_or_id,
|
||||
req_namespace=req_namespace,
|
||||
req_name=req_name,
|
||||
status=status,
|
||||
reason=reason
|
||||
)
|
||||
)
|
||||
log.info(log_msg)
|
||||
|
||||
# need to get user_name from the user object
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
course_key = _get_course_key(course_key_or_id)
|
||||
|
||||
# This seems to need to be here otherwise we get
|
||||
# circular references when starting up the app
|
||||
from openedx.core.djangoapps.credit.api.eligibility import (
|
||||
set_credit_requirement_status as api_set_credit_requirement_status
|
||||
)
|
||||
|
||||
api_set_credit_requirement_status(
|
||||
user.username,
|
||||
course_key,
|
||||
req_namespace,
|
||||
req_name,
|
||||
status,
|
||||
reason
|
||||
)
|
||||
@@ -231,12 +231,13 @@ def _get_proctoring_requirements(course_key):
|
||||
requirements = [
|
||||
{
|
||||
'namespace': 'proctored_exam',
|
||||
'name': 'proctored_exam_id:{id}'.format(id=exam['id']),
|
||||
'name': exam['content_id'],
|
||||
'display_name': exam['exam_name'],
|
||||
'criteria': {},
|
||||
}
|
||||
for exam in get_all_exams_for_course(unicode(course_key))
|
||||
if exam['is_proctored'] and exam['is_active']
|
||||
# practice exams do not count towards eligibility
|
||||
if exam['is_proctored'] and exam['is_active'] and not exam['is_practice_exam']
|
||||
]
|
||||
|
||||
log_msg = (
|
||||
|
||||
185
openedx/core/djangoapps/credit/tests/test_services.py
Normal file
185
openedx/core/djangoapps/credit/tests/test_services.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
Tests for the Credit xBlock service
|
||||
"""
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from openedx.core.djangoapps.credit.services import CreditService
|
||||
from openedx.core.djangoapps.credit.models import CreditCourse
|
||||
from openedx.core.djangoapps.credit.api.eligibility import set_credit_requirements
|
||||
|
||||
from student.models import CourseEnrollment, UserProfile
|
||||
|
||||
|
||||
class CreditServiceTests(ModuleStoreTestCase):
|
||||
"""
|
||||
Tests for the Credit xBlock service
|
||||
"""
|
||||
|
||||
def setUp(self, **kwargs):
|
||||
super(CreditServiceTests, self).setUp()
|
||||
|
||||
self.service = CreditService()
|
||||
self.course = CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course')
|
||||
self.credit_course = CreditCourse.objects.create(course_key=self.course.id, enabled=True)
|
||||
self.profile = UserProfile.objects.create(user_id=self.user.id, name='Foo Bar')
|
||||
|
||||
def test_user_not_found(self):
|
||||
"""
|
||||
Makes sure that get_credit_state returns None if user_id cannot be found
|
||||
"""
|
||||
|
||||
self.assertIsNone(self.service.get_credit_state(0, self.course.id))
|
||||
|
||||
def test_user_not_enrolled(self):
|
||||
"""
|
||||
Makes sure that get_credit_state returns None if user_id is not enrolled
|
||||
in the test course
|
||||
"""
|
||||
|
||||
self.assertIsNone(self.service.get_credit_state(self.user.id, self.course.id))
|
||||
|
||||
def test_inactive_enrollment(self):
|
||||
"""
|
||||
Makes sure that get_credit_state returns None if the user's enrollment is
|
||||
inactive
|
||||
"""
|
||||
|
||||
enrollment = CourseEnrollment.enroll(self.user, self.course.id)
|
||||
enrollment.is_active = False
|
||||
enrollment.save()
|
||||
|
||||
self.assertIsNone(self.service.get_credit_state(self.user.id, self.course.id))
|
||||
|
||||
def test_not_credit_course(self):
|
||||
"""
|
||||
Makes sure that get_credit_state returns None if the test course is not
|
||||
Credit eligible
|
||||
"""
|
||||
|
||||
CourseEnrollment.enroll(self.user, self.course.id)
|
||||
|
||||
self.credit_course.enabled = False
|
||||
self.credit_course.save()
|
||||
|
||||
self.assertIsNone(self.service.get_credit_state(self.user.id, self.course.id))
|
||||
|
||||
def test_no_profile_name(self):
|
||||
"""
|
||||
Makes sure that get_credit_state returns None if the user does not
|
||||
have a corresponding UserProfile. This shouldn't happen in
|
||||
real environments
|
||||
"""
|
||||
|
||||
profile = UserProfile.objects.get(user_id=self.user.id)
|
||||
profile.delete()
|
||||
|
||||
self.assertIsNone(self.service.get_credit_state(self.user.id, self.course.id))
|
||||
|
||||
def test_get_and_set_credit_state(self):
|
||||
"""
|
||||
Happy path through the service
|
||||
"""
|
||||
|
||||
CourseEnrollment.enroll(self.user, self.course.id)
|
||||
|
||||
# set course requirements
|
||||
set_credit_requirements(
|
||||
self.course.id,
|
||||
[
|
||||
{
|
||||
"namespace": "grade",
|
||||
"name": "grade",
|
||||
"display_name": "Grade",
|
||||
"criteria": {
|
||||
"min_grade": 0.8
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# mark the grade as satisfied
|
||||
self.service.set_credit_requirement_status(
|
||||
self.user.id,
|
||||
self.course.id,
|
||||
'grade',
|
||||
'grade'
|
||||
)
|
||||
|
||||
credit_state = self.service.get_credit_state(self.user.id, self.course.id)
|
||||
|
||||
self.assertIsNotNone(credit_state)
|
||||
self.assertEqual(credit_state['enrollment_mode'], 'honor')
|
||||
self.assertEqual(credit_state['profile_fullname'], 'Foo Bar')
|
||||
self.assertEqual(len(credit_state['credit_requirement_status']), 1)
|
||||
self.assertEqual(credit_state['credit_requirement_status'][0]['name'], 'grade')
|
||||
self.assertEqual(credit_state['credit_requirement_status'][0]['status'], 'satisfied')
|
||||
|
||||
def test_bad_user(self):
|
||||
"""
|
||||
Try setting requirements status with a bad user_id
|
||||
"""
|
||||
|
||||
# set course requirements
|
||||
set_credit_requirements(
|
||||
self.course.id,
|
||||
[
|
||||
{
|
||||
"namespace": "grade",
|
||||
"name": "grade",
|
||||
"display_name": "Grade",
|
||||
"criteria": {
|
||||
"min_grade": 0.8
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# mark the grade as satisfied
|
||||
retval = self.service.set_credit_requirement_status(
|
||||
0,
|
||||
self.course.id,
|
||||
'grade',
|
||||
'grade'
|
||||
)
|
||||
self.assertIsNone(retval)
|
||||
|
||||
def test_course_id_string(self):
|
||||
"""
|
||||
Make sure we can pass a course_id (string) and get back correct results as well
|
||||
"""
|
||||
|
||||
CourseEnrollment.enroll(self.user, self.course.id)
|
||||
|
||||
# set course requirements
|
||||
set_credit_requirements(
|
||||
self.course.id,
|
||||
[
|
||||
{
|
||||
"namespace": "grade",
|
||||
"name": "grade",
|
||||
"display_name": "Grade",
|
||||
"criteria": {
|
||||
"min_grade": 0.8
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# mark the grade as satisfied
|
||||
self.service.set_credit_requirement_status(
|
||||
self.user.id,
|
||||
unicode(self.course.id),
|
||||
'grade',
|
||||
'grade'
|
||||
)
|
||||
|
||||
credit_state = self.service.get_credit_state(self.user.id, unicode(self.course.id))
|
||||
|
||||
self.assertIsNotNone(credit_state)
|
||||
self.assertEqual(credit_state['enrollment_mode'], 'honor')
|
||||
self.assertEqual(credit_state['profile_fullname'], 'Foo Bar')
|
||||
self.assertEqual(len(credit_state['credit_requirement_status']), 1)
|
||||
self.assertEqual(credit_state['credit_requirement_status'][0]['name'], 'grade')
|
||||
self.assertEqual(credit_state['credit_requirement_status'][0]['status'], 'satisfied')
|
||||
@@ -125,13 +125,14 @@ class TestTaskExecution(ModuleStoreTestCase):
|
||||
|
||||
self.assertEqual(len(requirements), 1)
|
||||
self.assertEqual(requirements[0]['namespace'], 'proctored_exam')
|
||||
self.assertEqual(requirements[0]['name'], 'proctored_exam_id:1')
|
||||
self.assertEqual(requirements[0]['name'], 'foo')
|
||||
self.assertEqual(requirements[0]['display_name'], 'A Proctored Exam')
|
||||
self.assertEqual(requirements[0]['criteria'], {})
|
||||
|
||||
def test_proctored_exam_filtering(self):
|
||||
"""
|
||||
Make sure that timed or inactive exams do not end up in the requirements table
|
||||
Also practice protored exams are not a requirement
|
||||
"""
|
||||
|
||||
self.add_credit_course(self.course.id)
|
||||
@@ -180,6 +181,29 @@ class TestTaskExecution(ModuleStoreTestCase):
|
||||
if requirement['namespace'] == 'proctored_exam'
|
||||
])
|
||||
|
||||
# practice proctored exams aren't requirements
|
||||
create_exam(
|
||||
course_id=unicode(self.course.id),
|
||||
content_id='foo3',
|
||||
exam_name='A Proctored Exam',
|
||||
time_limit_mins=10,
|
||||
is_proctored=True,
|
||||
is_active=True,
|
||||
is_practice_exam=True
|
||||
)
|
||||
|
||||
on_course_publish(self.course.id)
|
||||
|
||||
requirements = get_credit_requirements(self.course.id)
|
||||
self.assertEqual(len(requirements), 1)
|
||||
|
||||
# make sure we don't have a proctoring requirement
|
||||
self.assertFalse([
|
||||
requirement
|
||||
for requirement in requirements
|
||||
if requirement['namespace'] == 'proctored_exam'
|
||||
])
|
||||
|
||||
def test_query_counts(self):
|
||||
self.add_credit_course(self.course.id)
|
||||
self.add_icrv_xblock()
|
||||
|
||||
Reference in New Issue
Block a user