Commit fa19afd7 by chrisndodge

Merge pull request #221 from edx/rc/release-0.10.x

Rc/release 0.10.x
parents b8332996 b757a025
...@@ -15,13 +15,13 @@ In order to use edx-proctoring, you must obtain an account (and secret configura ...@@ -15,13 +15,13 @@ In order to use edx-proctoring, you must obtain an account (and secret configura
CONFIGURATION: CONFIGURATION:
You will need to turn on the ENABLE_PROCTORED_EXAMS in lms.env.json and cms.env.json FEATURES dictionary: You will need to turn on the ENABLE_SPECIAL_EXAMS in lms.env.json and cms.env.json FEATURES dictionary:
``` ```
: :
"FEATURES": { "FEATURES": {
: :
"ENABLE_PROCTORED_EXAMS": true, "ENABLE_SPECIAL_EXAMS": true,
: :
} }
``` ```
......
...@@ -49,14 +49,6 @@ from edx_proctoring.runtime import get_runtime_service ...@@ -49,14 +49,6 @@ from edx_proctoring.runtime import get_runtime_service
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def is_feature_enabled():
"""
Returns if this feature has been enabled in our FEATURE flags
"""
return hasattr(settings, 'FEATURES') and settings.FEATURES.get('ENABLE_PROCTORED_EXAMS', False)
def create_exam(course_id, content_id, exam_name, time_limit_mins, due_date=None, def create_exam(course_id, content_id, exam_name, time_limit_mins, due_date=None,
is_proctored=True, is_practice_exam=False, external_id=None, is_active=True): is_proctored=True, is_practice_exam=False, external_id=None, is_active=True):
""" """
...@@ -475,7 +467,7 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False): ...@@ -475,7 +467,7 @@ def create_exam_attempt(exam_id, user_id, taking_as_proctored=False):
update_attempt_status( update_attempt_status(
exam_id, exam_id,
user_id, user_id,
ProctoredExamStudentAttemptStatus.declined ProctoredExamStudentAttemptStatus.expired
) )
log_msg = ( log_msg = (
...@@ -1297,15 +1289,12 @@ def _get_timed_exam_view(exam, context, exam_id, user_id, course_id): ...@@ -1297,15 +1289,12 @@ def _get_timed_exam_view(exam, context, exam_id, user_id, course_id):
# time limit, including any accommodations # time limit, including any accommodations
allowed_time_limit_mins = exam['time_limit_mins'] allowed_time_limit_mins = exam['time_limit_mins']
allowance = ProctoredExamStudentAllowance.get_allowance_for_user( allowance_extra_mins = ProctoredExamStudentAllowance.get_additional_time_granted(exam_id, user_id)
exam_id,
user_id,
"Additional time (minutes)"
)
if allowance: if allowance_extra_mins:
allowed_time_limit_mins += int(allowance.value) allowed_time_limit_mins += int(allowance_extra_mins)
# apply any cut off times according to due dates
allowed_time_limit_mins, _ = _calculate_allowed_mins( allowed_time_limit_mins, _ = _calculate_allowed_mins(
exam['due_date'], exam['due_date'],
allowed_time_limit_mins allowed_time_limit_mins
...@@ -1328,6 +1317,7 @@ def _get_timed_exam_view(exam, context, exam_id, user_id, course_id): ...@@ -1328,6 +1317,7 @@ def _get_timed_exam_view(exam, context, exam_id, user_id, course_id):
'total_time': total_time, 'total_time': total_time,
'has_due_date': has_due_date, 'has_due_date': has_due_date,
'exam_id': exam_id, 'exam_id': exam_id,
'exam_name': exam['exam_name'],
'progress_page_url': progress_page_url, 'progress_page_url': progress_page_url,
'does_time_remain': _does_time_remain(attempt), 'does_time_remain': _does_time_remain(attempt),
'enter_exam_endpoint': reverse('edx_proctoring.proctored_exam.attempt.collection'), 'enter_exam_endpoint': reverse('edx_proctoring.proctored_exam.attempt.collection'),
......
...@@ -154,8 +154,8 @@ var edx = edx || {}; ...@@ -154,8 +154,8 @@ var edx = edx || {};
} }
_.each(data_json.proctored_exam_attempts, function(proctored_exam_attempt) { _.each(data_json.proctored_exam_attempts, function(proctored_exam_attempt) {
if (proctored_exam_attempt.taking_as_proctored) { if (proctored_exam_attempt.proctored_exam.is_proctored) {
if (proctored_exam_attempt.is_sample_attempt) { if (proctored_exam_attempt.proctored_exam.is_practice_exam) {
proctored_exam_attempt.exam_attempt_type = gettext('Practice'); proctored_exam_attempt.exam_attempt_type = gettext('Practice');
} else { } else {
proctored_exam_attempt.exam_attempt_type = gettext('Proctored'); proctored_exam_attempt.exam_attempt_type = gettext('Proctored');
......
...@@ -12,9 +12,7 @@ ...@@ -12,9 +12,7 @@
{% endblocktrans %} {% endblocktrans %}
</p> </p>
<button class="gated-sequence start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-attempt-proctored=true data-start-immediately=false> <button class="gated-sequence start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-attempt-proctored=true data-start-immediately=false>
<a>
{% trans "Continue to my practice exam" %} {% trans "Continue to my practice exam" %}
</a>
<p> <p>
{% blocktrans %} {% blocktrans %}
You will be guided through steps to set up online proctoring software and to perform various checks. You will be guided through steps to set up online proctoring software and to perform various checks.
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
{% endblocktrans %} {% endblocktrans %}
</div> </div>
<div class="proctored-exam-skip-actions"> <div class="proctored-exam-skip-actions">
<button class="proctored-exam-skip-confirm-button btn btn-primary btn-base" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}"> <button class="proctored-exam-skip-confirm-button btn btn-pl-primary btn-base" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}">
{% trans "Continue Exam Without Proctoring" %} {% trans "Continue Exam Without Proctoring" %}
</button> </button>
<button class="proctored-exam-skip-cancel-button btn btn-default btn-base"> <button class="proctored-exam-skip-cancel-button btn btn-default btn-base">
......
...@@ -11,9 +11,7 @@ ...@@ -11,9 +11,7 @@
{% endblocktrans %} {% endblocktrans %}
</p> </p>
<button class="gated-sequence start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-attempt-proctored=true data-start-immediately=false> <button class="gated-sequence start-timed-exam" data-ajax-url="{{enter_exam_endpoint}}" data-exam-id="{{exam_id}}" data-attempt-proctored=true data-start-immediately=false>
<a> {% trans "Continue to my proctored exam. I want to be eligible for credit." %}
{% trans "Continue to my proctored exam. I want to be eligible for credit." %}
</a>
<p> <p>
{% blocktrans %} {% blocktrans %}
You will be guided through steps to set up online proctoring software and to perform various checks.</br> You will be guided through steps to set up online proctoring software and to perform various checks.</br>
...@@ -23,9 +21,7 @@ ...@@ -23,9 +21,7 @@
<i class="fa fa-arrow-circle-right"></i> <i class="fa fa-arrow-circle-right"></i>
</button> </button>
<button class="gated-sequence start-timed-exam" data-attempt-proctored=false> <button class="gated-sequence start-timed-exam" data-attempt-proctored=false>
<a> {% trans "Take this exam without proctoring." %}
{% trans "Take this exam without proctoring." %}
</a>
<i class="fa fa-arrow-circle-right"></i> <i class="fa fa-arrow-circle-right"></i>
<p> <p>
{% blocktrans %} {% blocktrans %}
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
{% endblocktrans %} {% endblocktrans %}
</p> </p>
<div> <div>
<button type="button" class="proctored-enter-exam btn btn-primary btn-base" data-action="start" data-exam-id="{{exam_id}}" data-change-state-url="{{change_state_url}}"> <button type="button" class="proctored-enter-exam btn btn-pl-primary btn-base" data-action="start" data-exam-id="{{exam_id}}" data-change-state-url="{{change_state_url}}">
{% blocktrans %} {% blocktrans %}
Start my exam Start my exam
{% endblocktrans %} {% endblocktrans %}
......
...@@ -12,13 +12,13 @@ ...@@ -12,13 +12,13 @@
as well as achieve a final grade that meets credit requirements for the course. as well as achieve a final grade that meets credit requirements for the course.
{% endblocktrans %} {% endblocktrans %}
</p> </p>
<button type="button" name="submit-proctored-exam" class="exam-action-button btn btn-primary btn-base" data-action="submit" data-exam-id="{{exam_id}}" data-change-state-url="{{change_state_url}}"> <button type="button" name="submit-proctored-exam" class="exam-action-button btn btn-pl-primary btn-base" data-action="submit" data-exam-id="{{exam_id}}" data-change-state-url="{{change_state_url}}">
{% blocktrans %} {% blocktrans %}
Yes, end my proctored exam Yes, end my proctored exam
{% endblocktrans %} {% endblocktrans %}
</button> </button>
{% if does_time_remain %} {% if does_time_remain %}
<button type="button" name="goback-proctored-exam" class="exam-action-button btn btn-secondary btn-base" data-action="start" data-exam-id="{{exam_id}}" data-change-state-url="{{change_state_url}}"> <button type="button" name="goback-proctored-exam" class="exam-action-button btn btn-secondary btn-base" data-action="start" data-exam-id="{{exam_id}}" data-change-state-url="{{change_state_url}}" style="box-shadow: none">
{% blocktrans %} {% blocktrans %}
No, I'd like to continue working No, I'd like to continue working
{% endblocktrans %} {% endblocktrans %}
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<div class="sequence timed-exam entrance" data-exam-id="{{exam_id}}"> <div class="sequence timed-exam entrance" data-exam-id="{{exam_id}}">
<h3> <h3>
{% blocktrans %} {% blocktrans %}
{{ display_name }} is a Timed Exam ({{total_time}}) {{ exam_name }} is a Timed Exam ({{total_time}})
{% endblocktrans %} {% endblocktrans %}
</h3> </h3>
<p> <p>
......
...@@ -10,13 +10,13 @@ ...@@ -10,13 +10,13 @@
Make sure your answers are ready to be submitted, and then submit your exam. Your exam will then be graded. Make sure your answers are ready to be submitted, and then submit your exam. Your exam will then be graded.
{% endblocktrans %} {% endblocktrans %}
</p> </p>
<button type="button" name="submit-proctored-exam" class="exam-action-button btn btn-primary btn-base" data-action="submit" data-exam-id="{{exam_id}}" data-change-state-url="{{change_state_url}}"> <button type="button" name="submit-proctored-exam" class="exam-action-button btn btn-pl-primary btn-base" data-action="submit" data-exam-id="{{exam_id}}" data-change-state-url="{{change_state_url}}">
{% blocktrans %} {% blocktrans %}
Yes, submit my timed exam. Yes, submit my timed exam.
{% endblocktrans %} {% endblocktrans %}
</button> </button>
{% if does_time_remain %} {% if does_time_remain %}
<button type="button" name="goback-proctored-exam" class="exam-action-button btn btn-secondary btn-base" data-action="start" data-exam-id="{{exam_id}}" data-change-state-url="{{change_state_url}}"> <button type="button" name="goback-proctored-exam" class="exam-action-button btn btn-secondary btn-base" data-action="start" data-exam-id="{{exam_id}}" data-change-state-url="{{change_state_url}}" style="box-shadow: none">
{% blocktrans %} {% blocktrans %}
No, I want to continue working. No, I want to continue working.
{% endblocktrans %} {% endblocktrans %}
......
...@@ -31,7 +31,6 @@ from edx_proctoring.api import ( ...@@ -31,7 +31,6 @@ from edx_proctoring.api import (
remove_exam_attempt, remove_exam_attempt,
get_all_exam_attempts, get_all_exam_attempts,
get_filtered_exam_attempts, get_filtered_exam_attempts,
is_feature_enabled,
mark_exam_attempt_timeout, mark_exam_attempt_timeout,
mark_exam_attempt_as_ready, mark_exam_attempt_as_ready,
update_attempt_status, update_attempt_status,
...@@ -95,7 +94,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -95,7 +94,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
self.value = '10' self.value = '10'
self.external_id = 'test_external_id' self.external_id = 'test_external_id'
self.proctored_exam_id = self._create_proctored_exam() self.proctored_exam_id = self._create_proctored_exam()
self.timed_exam = self._create_timed_exam() self.timed_exam_id = self._create_timed_exam()
self.practice_exam_id = self._create_practice_exam() self.practice_exam_id = self._create_practice_exam()
self.disabled_exam_id = self._create_disabled_exam() self.disabled_exam_id = self._create_disabled_exam()
...@@ -276,7 +275,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -276,7 +275,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
else: else:
exam_id = self.proctored_exam_id exam_id = self.proctored_exam_id
else: else:
exam_id = self.timed_exam exam_id = self.timed_exam_id
return ProctoredExamStudentAttempt.objects.create( return ProctoredExamStudentAttempt.objects.create(
proctored_exam_id=exam_id, proctored_exam_id=exam_id,
...@@ -291,7 +290,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -291,7 +290,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
Creates the ProctoredExamStudentAttempt object. Creates the ProctoredExamStudentAttempt object.
""" """
return ProctoredExamStudentAttempt.objects.create( return ProctoredExamStudentAttempt.objects.create(
proctored_exam_id=self.proctored_exam_id if is_proctored else self.timed_exam, proctored_exam_id=self.proctored_exam_id if is_proctored else self.timed_exam_id,
user_id=self.user_id, user_id=self.user_id,
external_id=self.external_id, external_id=self.external_id,
started_at=started_at if started_at else datetime.now(pytz.UTC), started_at=started_at if started_at else datetime.now(pytz.UTC),
...@@ -324,18 +323,6 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -324,18 +323,6 @@ class ProctoredExamApiTests(LoggedInTestCase):
proctored_exam_id=self.proctored_exam_id, user_id=self.user_id, key=self.key, value=self.value proctored_exam_id=self.proctored_exam_id, user_id=self.user_id, key=self.key, value=self.value
) )
def test_feature_enabled(self):
"""
Checks the is_feature_enabled method
"""
self.assertFalse(is_feature_enabled())
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_PROCTORED_EXAMS': False}):
self.assertFalse(is_feature_enabled())
with patch.dict('django.conf.settings.FEATURES', {'ENABLE_PROCTORED_EXAMS': True}):
self.assertTrue(is_feature_enabled())
def test_create_duplicate_exam(self): def test_create_duplicate_exam(self):
""" """
Test to create a proctored exam that has already exist in the Test to create a proctored exam that has already exist in the
...@@ -414,7 +401,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -414,7 +401,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
test to get the exam by the exam_id and test to get the exam by the exam_id and
then compare their values. then compare their values.
""" """
timed_exam = get_exam_by_id(self.timed_exam) timed_exam = get_exam_by_id(self.timed_exam_id)
self.assertEqual(timed_exam['course_id'], self.course_id) self.assertEqual(timed_exam['course_id'], self.course_id)
self.assertEqual(timed_exam['content_id'], self.content_id_timed) self.assertEqual(timed_exam['content_id'], self.content_id_timed)
self.assertEqual(timed_exam['exam_name'], self.exam_name) self.assertEqual(timed_exam['exam_name'], self.exam_name)
...@@ -546,7 +533,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -546,7 +533,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
with freeze_time(reset_time): with freeze_time(reset_time):
attempt_id = create_exam_attempt(exam_id, self.user_id) attempt_id = create_exam_attempt(exam_id, self.user_id)
attempt = get_exam_attempt_by_id(attempt_id) attempt = get_exam_attempt_by_id(attempt_id)
self.assertEqual(attempt['status'], ProctoredExamStudentAttemptStatus.declined) self.assertEqual(attempt['status'], ProctoredExamStudentAttemptStatus.expired)
def test_create_an_exam_attempt(self): def test_create_an_exam_attempt(self):
""" """
...@@ -1647,29 +1634,26 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -1647,29 +1634,26 @@ class ProctoredExamApiTests(LoggedInTestCase):
But user has an allowance But user has an allowance
""" """
ProctoredExamStudentAllowance.objects.create( allowed_extra_time = 10
proctored_exam_id=self.timed_exam, add_allowance_for_user(
user_id=self.user_id, self.timed_exam_id,
key='Additional time (minutes)', self.user.username,
value=15 ProctoredExamStudentAllowance.ADDITIONAL_TIME_GRANTED,
str(allowed_extra_time)
) )
rendered_response = get_student_view( rendered_response = get_student_view(
user_id=self.user_id, user_id=self.user_id,
course_id=self.course_id, course_id=self.course_id,
content_id=self.content_id_timed, content_id=self.content_id_timed,
context={ context={}
'is_proctored': False,
'display_name': self.exam_name,
'default_time_limit_mins': 90
}
) )
self.assertNotIn( self.assertNotIn(
'data-exam-id="{proctored_exam_id}"'.format(proctored_exam_id=self.proctored_exam_id), 'data-exam-id="{proctored_exam_id}"'.format(proctored_exam_id=self.proctored_exam_id),
rendered_response rendered_response
) )
self.assertIn(self.timed_exam_msg.format(exam_name=self.exam_name), rendered_response) self.assertIn(self.timed_exam_msg.format(exam_name=self.exam_name), rendered_response)
self.assertIn('36 minutes', rendered_response) self.assertIn('31 minutes', rendered_response)
self.assertNotIn(self.start_an_exam_msg.format(exam_name=self.exam_name), rendered_response) self.assertNotIn(self.start_an_exam_msg.format(exam_name=self.exam_name), rendered_response)
@ddt.data( @ddt.data(
...@@ -1696,9 +1680,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -1696,9 +1680,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
course_id=self.course_id, course_id=self.course_id,
content_id=self.content_id_timed, content_id=self.content_id_timed,
context={ context={
'is_proctored': False,
'display_name': self.exam_name, 'display_name': self.exam_name,
'default_time_limit_mins': 90
} }
) )
self.assertIn(expected_content, rendered_response) self.assertIn(expected_content, rendered_response)
...@@ -2102,7 +2084,7 @@ class ProctoredExamApiTests(LoggedInTestCase): ...@@ -2102,7 +2084,7 @@ class ProctoredExamApiTests(LoggedInTestCase):
Assert that we get the expected status summaries Assert that we get the expected status summaries
for the timed exams. for the timed exams.
""" """
timed_exam = get_exam_by_id(self.timed_exam) timed_exam = get_exam_by_id(self.timed_exam_id)
summary = get_attempt_status_summary( summary = get_attempt_status_summary(
self.user.id, self.user.id,
timed_exam['course_id'], timed_exam['course_id'],
......
...@@ -34,7 +34,7 @@ def load_requirements(*requirements_paths): ...@@ -34,7 +34,7 @@ def load_requirements(*requirements_paths):
setup( setup(
name='edx-proctoring', name='edx-proctoring',
version='0.10.10', version='0.10.15',
description='Proctoring subsystem for Open edX', description='Proctoring subsystem for Open edX',
long_description=open('README.md').read(), long_description=open('README.md').read(),
author='edX', author='edX',
......
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