Skip to content

Commit

Permalink
Merge pull request #288 from edx/efischer/hide_timed_exams
Browse files Browse the repository at this point in the history
TNL-4366 Add ability to hide timed exam after due date
  • Loading branch information
Eric Fischer committed Apr 21, 2016
2 parents fcd3a1d + ff1b3d1 commit da76a63
Show file tree
Hide file tree
Showing 10 changed files with 85 additions and 26 deletions.
27 changes: 16 additions & 11 deletions edx_proctoring/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@


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, hide_after_due=False):
"""
Creates a new ProctoredExam entity, if the course_id/content_id pair do not already exist.
If that pair already exists, then raise exception.
Expand All @@ -77,19 +77,20 @@ def create_exam(course_id, content_id, exam_name, time_limit_mins, due_date=None
due_date=due_date,
is_proctored=is_proctored,
is_practice_exam=is_practice_exam,
is_active=is_active
is_active=is_active,
hide_after_due=hide_after_due,
)

log_msg = (
u'Created exam ({exam_id}) with parameters: course_id={course_id}, '
u'content_id={content_id}, exam_name={exam_name}, time_limit_mins={time_limit_mins}, '
u'is_proctored={is_proctored}, is_practice_exam={is_practice_exam}, '
u'external_id={external_id}, is_active={is_active}'.format(
u'external_id={external_id}, is_active={is_active}, hide_after_due={hide_after_due}'.format(
exam_id=proctored_exam.id,
course_id=course_id, content_id=content_id,
exam_name=exam_name, time_limit_mins=time_limit_mins,
is_proctored=is_proctored, is_practice_exam=is_practice_exam,
external_id=external_id, is_active=is_active
external_id=external_id, is_active=is_active, hide_after_due=hide_after_due
)
)
log.info(log_msg)
Expand Down Expand Up @@ -202,7 +203,7 @@ def get_review_policy_by_exam_id(exam_id):


def update_exam(exam_id, exam_name=None, time_limit_mins=None, due_date=constants.MINIMUM_TIME,
is_proctored=None, is_practice_exam=None, external_id=None, is_active=None):
is_proctored=None, is_practice_exam=None, external_id=None, is_active=None, hide_after_due=None):
"""
Given a Django ORM id, update the existing record, otherwise raise exception if not found.
If an argument is not passed in, then do not change it's current value.
Expand All @@ -214,10 +215,10 @@ def update_exam(exam_id, exam_name=None, time_limit_mins=None, due_date=constant
u'Updating exam_id {exam_id} with parameters '
u'exam_name={exam_name}, time_limit_mins={time_limit_mins}, due_date={due_date}'
u'is_proctored={is_proctored}, is_practice_exam={is_practice_exam}, '
u'external_id={external_id}, is_active={is_active}'.format(
u'external_id={external_id}, is_active={is_active}, hide_after_due={hide_after_due}'.format(
exam_id=exam_id, exam_name=exam_name, time_limit_mins=time_limit_mins,
due_date=due_date, is_proctored=is_proctored, is_practice_exam=is_practice_exam,
external_id=external_id, is_active=is_active
external_id=external_id, is_active=is_active, hide_after_due=hide_after_due
)
)
log.info(log_msg)
Expand All @@ -240,6 +241,8 @@ def update_exam(exam_id, exam_name=None, time_limit_mins=None, due_date=constant
proctored_exam.external_id = external_id
if is_active is not None:
proctored_exam.is_active = is_active
if hide_after_due is not None:
proctored_exam.hide_after_due = hide_after_due
proctored_exam.save()

# read back exam so we can emit an event on it
Expand Down Expand Up @@ -1444,9 +1447,10 @@ def _get_timed_exam_view(exam, context, exam_id, user_id, course_id):
elif attempt_status == ProctoredExamStudentAttemptStatus.ready_to_submit:
student_view_template = 'timed_exam/ready_to_submit.html'
elif attempt_status == ProctoredExamStudentAttemptStatus.submitted:
# check if the exam's due_date has passed then we return None
# If we are not hiding the exam after the due_date has passed,
# check if the exam's due_date has passed. If so, return None
# so that the user can see his exam answers in read only mode.
if has_due_date_passed(exam['due_date']):
if not exam['hide_after_due'] and has_due_date_passed(exam['due_date']):
return None

student_view_template = 'timed_exam/submitted.html'
Expand Down Expand Up @@ -1500,7 +1504,7 @@ def _get_timed_exam_view(exam, context, exam_id, user_id, course_id):

django_context.update({
'total_time': total_time,
'has_due_date': has_due_date,
'will_be_revealed': has_due_date and not exam['hide_after_due'],
'exam_id': exam_id,
'exam_name': exam['exam_name'],
'progress_page_url': progress_page_url,
Expand Down Expand Up @@ -1816,7 +1820,8 @@ def get_student_view(user_id, course_id, content_id,
time_limit_mins=context['default_time_limit_mins'],
is_proctored=context.get('is_proctored', False),
is_practice_exam=context.get('is_practice_exam', False),
due_date=context.get('due_date', None)
due_date=context.get('due_date', None),
hide_after_due=context.get('hide_after_due', None),
)
exam = get_exam_by_content_id(course_id, content_id)

Expand Down
19 changes: 19 additions & 0 deletions edx_proctoring/migrations/0005_proctoredexam_hide_after_due.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('edx_proctoring', '0004_auto_20160201_0523'),
]

operations = [
migrations.AddField(
model_name='proctoredexam',
name='hide_after_due',
field=models.BooleanField(default=False),
),
]
3 changes: 3 additions & 0 deletions edx_proctoring/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ class ProctoredExam(TimeStampedModel):
# Whether this exam will be active.
is_active = models.BooleanField(default=False)

# Whether to hide this exam after the due date
hide_after_due = models.BooleanField(default=False)

class Meta:
""" Meta class for this Django model """
unique_together = (('course_id', 'content_id'),)
Expand Down
4 changes: 3 additions & 1 deletion edx_proctoring/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class ProctoredExamSerializer(serializers.ModelSerializer):
is_practice_exam = serializers.BooleanField(required=True)
is_proctored = serializers.BooleanField(required=True)
due_date = serializers.DateTimeField(required=False, format=None)
hide_after_due = serializers.BooleanField(required=True)

class Meta:
"""
Expand All @@ -34,7 +35,8 @@ class Meta:

fields = (
"id", "course_id", "content_id", "external_id", "exam_name",
"time_limit_mins", "is_proctored", "is_practice_exam", "is_active", "due_date"
"time_limit_mins", "is_proctored", "is_practice_exam", "is_active",
"due_date", "hide_after_due"
)


Expand Down
2 changes: 1 addition & 1 deletion edx_proctoring/templates/timed_exam/submitted.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ <h3>
{% blocktrans %}
Your grade for this timed exam will be immediately available on the <a href="{{progress_page_url}}">Progress</a> page.
{% endblocktrans %}
{% if has_due_date %}
{% if will_be_revealed %}
{% blocktrans %}
After the due date has passed, you can review the exam, but you cannot change your answers.
{% endblocktrans %}
Expand Down
41 changes: 33 additions & 8 deletions edx_proctoring/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,18 @@ def test_update_proctored_exam(self):
self.assertEqual(update_proctored_exam.course_id, 'test_course')
self.assertEqual(update_proctored_exam.content_id, 'test_content_id')

def test_update_timed_exam(self):
"""
test update the existing timed exam
"""
updated_timed_exam_id = update_exam(self.timed_exam_id, hide_after_due=True)

self.assertEqual(self.timed_exam_id, updated_timed_exam_id)

update_timed_exam = ProctoredExam.objects.get(id=updated_timed_exam_id)

self.assertEqual(update_timed_exam.hide_after_due, True)

def test_update_non_existing_exam(self):
"""
test to update the non-existing proctored exam
Expand Down Expand Up @@ -1022,7 +1034,8 @@ def test_get_student_view(self):
context={
'is_proctored': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
'default_time_limit_mins': 90,
'hide_after_due': False,
}
)
self.assertIn(
Expand All @@ -1041,6 +1054,7 @@ def test_get_student_view(self):
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'is_practice_exam': True,
'hide_after_due': False,
}
)
self.assertIn(self.start_a_practice_exam_msg.format(exam_name=self.exam_name), rendered_response)
Expand Down Expand Up @@ -1186,7 +1200,8 @@ def test_wrong_exam_combo(self):
'is_proctored': False,
'is_practice_exam': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90
'default_time_limit_mins': 90,
'hide_after_due': False,
},
user_role='student'
)
Expand All @@ -1209,7 +1224,8 @@ def test_proctored_exam_passed_end_date(self):
'is_practice_exam': False,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'due_date': None
'due_date': None,
'hide_after_due': False,
},
user_role='student'
)
Expand Down Expand Up @@ -1250,7 +1266,8 @@ def test_practice_exam_passed_end_date(self):
'is_practice_exam': True,
'display_name': self.exam_name,
'default_time_limit_mins': 90,
'due_date': None
'due_date': None,
'hide_after_due': False,
},
user_role='student'
)
Expand Down Expand Up @@ -1436,16 +1453,19 @@ def test_get_studentview_started_timed_exam(self):

@ddt.data(
(datetime.now(pytz.UTC) + timedelta(days=1), False),
(datetime.now(pytz.UTC) - timedelta(days=1), False),
(datetime.now(pytz.UTC) - timedelta(days=1), True),
)
@ddt.unpack
def test_get_studentview_submitted_timed_exam_with_past_due_date(self, due_date, has_due_date_passed):
def test_get_studentview_submitted_timed_exam_with_past_due_date(self, due_date, hide_after_due):
"""
Test for get_student_view timed exam with the due date.
"""

# exam is created with due datetime which has already passed
exam_id = self._create_exam_with_due_time(is_proctored=False, due_date=due_date)
if hide_after_due:
update_exam(exam_id, hide_after_due=hide_after_due)

# now create the timed_exam attempt in the submitted state
self._create_exam_attempt(exam_id, status='submitted')
Expand All @@ -1461,10 +1481,14 @@ def test_get_studentview_submitted_timed_exam_with_past_due_date(self, due_date,
'due_date': due_date,
}
)
if not has_due_date_passed:
if datetime.now(pytz.UTC) < due_date:
self.assertIn(self.timed_exam_submitted, rendered_response)
self.assertIn(self.submitted_timed_exam_msg_with_due_date, rendered_response)
elif hide_after_due:
self.assertIn(self.timed_exam_submitted, rendered_response)
self.assertNotIn(self.submitted_timed_exam_msg_with_due_date, rendered_response)
else:
self.assertIsNone(None)
self.assertIsNone(rendered_response)

def test_proctored_exam_attempt_with_past_due_datetime(self):
"""
Expand Down Expand Up @@ -1925,7 +1949,8 @@ def test_get_studentview_unstarted_timed_exam(self):
context={
'is_proctored': False,
'display_name': self.exam_name,
'default_time_limit_mins': 90
'default_time_limit_mins': 90,
'hide_after_due': False,
}
)
self.assertNotIn(
Expand Down
3 changes: 2 additions & 1 deletion edx_proctoring/tests/test_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ def test_boolean_fields(self):
'external_id': '123',
'is_proctored': 'bla',
'is_practice_exam': 'bla',
'is_active': 'f'
'is_active': 'f',
'hide_after_due': 't',
}
serializer = ProctoredExamSerializer(data=data)

Expand Down
6 changes: 4 additions & 2 deletions edx_proctoring/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ def test_create_exam(self):
'external_id': '123',
'is_proctored': True,
'is_practice_exam': False,
'is_active': True
'is_active': True,
'hide_after_due': False,
}
response = self.client.post(
reverse('edx_proctoring.proctored_exam.exam'),
Expand Down Expand Up @@ -136,7 +137,8 @@ def test_create_duplicate_exam(self):
'external_id': '123',
'is_proctored': True,
'is_practice_exam': False,
'is_active': True
'is_active': True,
'hide_after_due': False,
}
response = self.client.post(
reverse('edx_proctoring.proctored_exam.exam'),
Expand Down
4 changes: 3 additions & 1 deletion edx_proctoring/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ def post(self, request):
is_proctored=request.data.get('is_proctored', None),
is_practice_exam=request.data.get('is_practice_exam', None),
external_id=request.data.get('external_id', None),
is_active=request.data.get('is_active', None)
is_active=request.data.get('is_active', None),
hide_after_due=request.data.get('hide_after_due', None),
)
return Response({'exam_id': exam_id})
else:
Expand All @@ -213,6 +214,7 @@ def put(self, request):
is_practice_exam=request.data.get('is_practice_exam', None),
external_id=request.data.get('external_id', None),
is_active=request.data.get('is_active', None),
hide_after_due=request.data.get('hide_after_due', None),
)
return Response({'exam_id': exam_id})
except ProctoredExamNotFoundException, ex:
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def load_requirements(*requirements_paths):

setup(
name='edx-proctoring',
version='0.12.15',
version='0.12.16',
description='Proctoring subsystem for Open edX',
long_description=open('README.md').read(),
author='edX',
Expand Down

0 comments on commit da76a63

Please sign in to comment.