Commit e01228a7 by Chris Dodge

allow for some rutime services to be registered with our subsystem, so we can…

allow for some rutime services to be registered with our subsystem, so we can get information in models that are outside of edx_proctoring
parent 85b670d9
...@@ -34,6 +34,7 @@ from edx_proctoring.serializers import ( ...@@ -34,6 +34,7 @@ from edx_proctoring.serializers import (
from edx_proctoring.utils import humanized_time from edx_proctoring.utils import humanized_time
from edx_proctoring.backends import get_backend_provider from edx_proctoring.backends import get_backend_provider
from edx_proctoring.runtime import get_runtime_service
def is_feature_enabled(): def is_feature_enabled():
...@@ -235,13 +236,24 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): ...@@ -235,13 +236,24 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False):
) )
) )
# get the name of the user, if the service is available
full_name = None
profile_service = get_runtime_service('profile')
if profile_service:
profile = profile_service(user_id)
full_name = profile['name']
# now call into the backend provider to register exam attempt # now call into the backend provider to register exam attempt
external_id = get_backend_provider().register_exam_attempt( external_id = get_backend_provider().register_exam_attempt(
exam, exam,
allowed_time_limit_mins, context={
attempt_code, 'time_limit_mins': allowed_time_limit_mins,
False, 'attempt_code': attempt_code,
callback_url 'is_sample_attempt': False,
'callback_url': callback_url,
'full_name': full_name,
}
) )
attempt = ProctoredExamStudentAttempt.create_exam_attempt( attempt = ProctoredExamStudentAttempt.create_exam_attempt(
......
...@@ -14,8 +14,7 @@ class ProctoringBackendProvider(object): ...@@ -14,8 +14,7 @@ class ProctoringBackendProvider(object):
__metaclass__ = abc.ABCMeta __metaclass__ = abc.ABCMeta
@abc.abstractmethod @abc.abstractmethod
def register_exam_attempt(self, exam, time_limit_mins, attempt_code, def register_exam_attempt(self, exam, context):
is_sample_attempt, callback_url):
""" """
Called when the exam attempt has been created but not started Called when the exam attempt has been created but not started
""" """
......
...@@ -10,8 +10,7 @@ class NullBackendProvider(ProctoringBackendProvider): ...@@ -10,8 +10,7 @@ class NullBackendProvider(ProctoringBackendProvider):
Implementation of the ProctoringBackendProvider that does nothing Implementation of the ProctoringBackendProvider that does nothing
""" """
def register_exam_attempt(self, exam, time_limit_mins, attempt_code, def register_exam_attempt(self, exam, context):
is_sample_attempt, callback_url):
""" """
Called when the exam attempt has been created but not started Called when the exam attempt has been created but not started
""" """
......
...@@ -40,19 +40,17 @@ class SoftwareSecureBackendProvider(ProctoringBackendProvider): ...@@ -40,19 +40,17 @@ class SoftwareSecureBackendProvider(ProctoringBackendProvider):
self.timeout = 10 self.timeout = 10
self.software_download_url = software_download_url self.software_download_url = software_download_url
def register_exam_attempt(self, exam, time_limit_mins, attempt_code, def register_exam_attempt(self, exam, context):
is_sample_attempt, callback_url):
""" """
Method that is responsible for communicating with the backend provider Method that is responsible for communicating with the backend provider
to establish a new proctored exam to establish a new proctored exam
""" """
attempt_code = context['attempt_code']
data = self._get_payload( data = self._get_payload(
exam, exam,
time_limit_mins, context
attempt_code,
is_sample_attempt,
callback_url
) )
headers = { headers = {
"Content-Type": 'application/json' "Content-Type": 'application/json'
...@@ -114,11 +112,25 @@ class SoftwareSecureBackendProvider(ProctoringBackendProvider): ...@@ -114,11 +112,25 @@ class SoftwareSecureBackendProvider(ProctoringBackendProvider):
encrypted_text = cipher.encrypt(pad(pwd)) encrypted_text = cipher.encrypt(pad(pwd))
return base64.b64encode(encrypted_text) return base64.b64encode(encrypted_text)
def _get_payload(self, exam, time_limit_mins, attempt_code, def _get_payload(self, exam, context):
is_sample_attempt, callback_url):
""" """
Constructs the data payload that Software Secure expects Constructs the data payload that Software Secure expects
""" """
attempt_code = context['attempt_code']
time_limit_mins = context['time_limit_mins']
is_sample_attempt = context['is_sample_attempt']
callback_url = context['callback_url']
full_name = context['full_name']
first_name = ''
last_name = ''
if full_name:
name_elements = full_name.split(' ')
first_name = name_elements[0]
if len(name_elements) > 1:
last_name = ' '.join(name_elements[1:])
now = datetime.datetime.utcnow() now = datetime.datetime.utcnow()
start_time_str = now.strftime("%a, %d %b %Y %H:%M:%S GMT") start_time_str = now.strftime("%a, %d %b %Y %H:%M:%S GMT")
end_time_str = (now + datetime.timedelta(minutes=time_limit_mins)).strftime("%a, %d %b %Y %H:%M:%S GMT") end_time_str = (now + datetime.timedelta(minutes=time_limit_mins)).strftime("%a, %d %b %Y %H:%M:%S GMT")
...@@ -140,6 +152,8 @@ class SoftwareSecureBackendProvider(ProctoringBackendProvider): ...@@ -140,6 +152,8 @@ class SoftwareSecureBackendProvider(ProctoringBackendProvider):
"noOfStudents": 1, "noOfStudents": 1,
"examID": exam['id'], "examID": exam['id'],
"courseID": exam['course_id'], "courseID": exam['course_id'],
"firstName": first_name,
"lastName": last_name,
} }
} }
......
...@@ -12,8 +12,7 @@ class TestBackendProvider(ProctoringBackendProvider): ...@@ -12,8 +12,7 @@ class TestBackendProvider(ProctoringBackendProvider):
Implementation of the ProctoringBackendProvider that does nothing Implementation of the ProctoringBackendProvider that does nothing
""" """
def register_exam_attempt(self, exam, time_limit_mins, attempt_code, def register_exam_attempt(self, exam, context):
is_sample_attempt, callback_url):
""" """
Called when the exam attempt has been created but not started Called when the exam attempt has been created but not started
""" """
...@@ -46,17 +45,13 @@ class PassthroughBackendProvider(ProctoringBackendProvider): ...@@ -46,17 +45,13 @@ class PassthroughBackendProvider(ProctoringBackendProvider):
Implementation of the ProctoringBackendProvider that just calls the base class Implementation of the ProctoringBackendProvider that just calls the base class
""" """
def register_exam_attempt(self, exam, time_limit_mins, attempt_code, def register_exam_attempt(self, exam, context):
is_sample_attempt, callback_url):
""" """
Called when the exam attempt has been created but not started Called when the exam attempt has been created but not started
""" """
return super(PassthroughBackendProvider, self).register_exam_attempt( return super(PassthroughBackendProvider, self).register_exam_attempt(
exam, exam,
time_limit_mins, context
attempt_code,
is_sample_attempt,
callback_url
) )
def start_exam_attempt(self, exam, attempt): def start_exam_attempt(self, exam, attempt):
...@@ -100,7 +95,7 @@ class TestBackends(TestCase): ...@@ -100,7 +95,7 @@ class TestBackends(TestCase):
provider = PassthroughBackendProvider() provider = PassthroughBackendProvider()
with self.assertRaises(NotImplementedError): with self.assertRaises(NotImplementedError):
provider.register_exam_attempt(None, None, None, None, None) provider.register_exam_attempt(None, None)
with self.assertRaises(NotImplementedError): with self.assertRaises(NotImplementedError):
provider.start_exam_attempt(None, None) provider.start_exam_attempt(None, None)
...@@ -118,7 +113,7 @@ class TestBackends(TestCase): ...@@ -118,7 +113,7 @@ class TestBackends(TestCase):
provider = NullBackendProvider() provider = NullBackendProvider()
self.assertIsNone(provider.register_exam_attempt(None, None, None, None, None)) self.assertIsNone(provider.register_exam_attempt(None, None))
self.assertIsNone(provider.start_exam_attempt(None, None)) self.assertIsNone(provider.start_exam_attempt(None, None))
self.assertIsNone(provider.stop_exam_attempt(None, None)) self.assertIsNone(provider.stop_exam_attempt(None, None))
self.assertIsNone(provider.get_software_download_url()) self.assertIsNone(provider.get_software_download_url())
...@@ -8,13 +8,13 @@ import json ...@@ -8,13 +8,13 @@ import json
from django.test import TestCase from django.test import TestCase
from django.contrib.auth.models import User from django.contrib.auth.models import User
from edx_proctoring.runtime import set_runtime_service
from edx_proctoring.backends import get_backend_provider from edx_proctoring.backends import get_backend_provider
from edx_proctoring.exceptions import BackendProvideCannotRegisterAttempt from edx_proctoring.exceptions import BackendProvideCannotRegisterAttempt
from edx_proctoring.api import get_exam_attempt_by_id
from edx_proctoring.api import ( from edx_proctoring.api import (
get_exam_attempt_by_id,
create_exam, create_exam,
create_exam_attempt, create_exam_attempt,
) )
...@@ -71,6 +71,20 @@ class SoftwareSecureTests(TestCase): ...@@ -71,6 +71,20 @@ class SoftwareSecureTests(TestCase):
self.user = User(username='foo', email='foo@bar.com') self.user = User(username='foo', email='foo@bar.com')
self.user.save() self.user.save()
def mock_profile_service(user_id): # pylint: disable=unused-argument
"""
Mocked out Profile callback endpoint
"""
return {'name': 'Wolfgang von Strucker'}
set_runtime_service('profile', mock_profile_service)
def tearDown(self):
"""
When tests are done
"""
set_runtime_service('profile', None)
def test_provider_instance(self): def test_provider_instance(self):
""" """
Makes sure the instance of the proctoring module can be created Makes sure the instance of the proctoring module can be created
...@@ -108,6 +122,31 @@ class SoftwareSecureTests(TestCase): ...@@ -108,6 +122,31 @@ class SoftwareSecureTests(TestCase):
self.assertEqual(attempt['external_id'], 'foobar') self.assertEqual(attempt['external_id'], 'foobar')
self.assertIsNone(attempt['started_at']) self.assertIsNone(attempt['started_at'])
def test_single_name_attempt(self):
"""
Tests to make sure we can parse a fullname which does not have any spaces in it
"""
def mock_profile_service(user_id): # pylint: disable=unused-argument
"""
Mocked out Profile callback endpoint
"""
return {'name': 'Bono'}
set_runtime_service('profile', mock_profile_service)
exam_id = create_exam(
course_id='foo/bar/baz',
content_id='content',
exam_name='Sample Exam',
time_limit_mins=10,
is_proctored=True
)
with HTTMock(response_content):
attempt_id = create_exam_attempt(exam_id, self.user.id, taking_as_proctored=True)
self.assertIsNotNone(attempt_id)
def test_failing_register_attempt(self): def test_failing_register_attempt(self):
""" """
Makes sure we can register an attempt Makes sure we can register an attempt
......
"""
Runtime services that the LMS can register than we can callback on
"""
_RUNTIME_SERVICES = {}
def set_runtime_service(name, callback):
"""
Adds a service provided by the runtime (aka LMS) to our directory
"""
_RUNTIME_SERVICES[name] = callback
def get_runtime_service(name):
"""
Returns a registered runtime service, None if no match is found
"""
return _RUNTIME_SERVICES.get(name)
...@@ -26,8 +26,12 @@ ...@@ -26,8 +26,12 @@
</div> </div>
</div> </div>
<div class="footer-sequence border-b-0 padding-b-0"> <div class="footer-sequence border-b-0 padding-b-0">
<span> {% trans "Note: As soon as you finish installing and setting up the proctoring software, <span>
you will be prompted to start your timed exam." %} </span> {% blocktrans %}
Note: As soon as you finish installing and setting up the proctoring software,
you will be prompted to start your timed exam.
{% endblocktrans %}
</span>
<p> <p>
{% blocktrans %} {% blocktrans %}
Be prepared to start your exam and to complete it while adhering to the {{platform_name}} Be prepared to start your exam and to complete it while adhering to the {{platform_name}}
......
...@@ -3,7 +3,9 @@ Test for the xBlock service ...@@ -3,7 +3,9 @@ Test for the xBlock service
""" """
import unittest import unittest
from edx_proctoring.services import ProctoringService from edx_proctoring.services import (
ProctoringService
)
from edx_proctoring import api as edx_proctoring_api from edx_proctoring import api as edx_proctoring_api
import types import types
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment