Integration of edx_proctoring into the LMS

This commit is contained in:
Chris Dodge
2015-07-24 20:22:38 -04:00
parent 5b9b0a8339
commit 6cf5516a84
59 changed files with 2541 additions and 98 deletions

View 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
)

View File

@@ -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 = (

View 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')

View File

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