Unverified Commit 12cb635d by Sylvia Pearce Committed by GitHub

Merge branch 'master' into sylvia/DOC-3842

parents 76bffefa 8039cfa7
......@@ -11,6 +11,8 @@ before_install:
install:
- pip install setuptools==32.3.1 # need newer version than Travis default
- make install
before_script:
- npm install -g gulp-cli
script:
- make test-all
- make test-js
......
......@@ -4,6 +4,6 @@ The exam proctoring subsystem for the Open edX platform.
from __future__ import absolute_import
__version__ = '1.2.0'
__version__ = '1.3.1'
default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name
......@@ -313,6 +313,10 @@ def get_exam_by_content_id(course_id, content_id):
"""
proctored_exam = ProctoredExam.get_exam_by_content_id(course_id, content_id)
if proctored_exam is None:
log.exception(
'Cannot find the proctored exams in this course %s with content_id: %s',
course_id, content_id
)
raise ProctoredExamNotFoundException
serialized_exam_object = ProctoredExamSerializer(proctored_exam)
......@@ -349,12 +353,12 @@ def add_allowance_for_user(exam_id, user_info, key, value):
emit_event(exam, 'allowance.{action}'.format(action=action), override_data=data)
def get_allowances_for_course(course_id, timed_exams_only=False):
def get_allowances_for_course(course_id):
"""
Get all the allowances for the course.
"""
student_allowances = ProctoredExamStudentAllowance.get_allowances_for_course(
course_id, timed_exams_only=timed_exams_only
course_id
)
return [ProctoredExamStudentAllowanceSerializer(allowance).data for allowance in student_allowances]
......@@ -1981,15 +1985,15 @@ def get_exam_violation_report(course_id, include_practice_exams=False):
for review in reviews:
attempt_code = review.attempt_code
if attempt_code in attempts_by_code:
attempts_by_code[attempt_code]['review_status'] = review.review_status
attempts_by_code[attempt_code]['review_status'] = review.review_status
for comment in review.proctoredexamsoftwaresecurecomment_set.all():
comments_key = '{status} Comments'.format(status=comment.status)
for comment in review.proctoredexamsoftwaresecurecomment_set.all():
comments_key = '{status} Comments'.format(status=comment.status)
if comments_key not in attempts_by_code[attempt_code]:
attempts_by_code[attempt_code][comments_key] = []
if comments_key not in attempts_by_code[attempt_code]:
attempts_by_code[attempt_code][comments_key] = []
attempts_by_code[attempt_code][comments_key].append(comment.comment)
attempts_by_code[attempt_code][comments_key].append(comment.comment)
return sorted(attempts_by_code.values(), key=lambda a: a['exam_name'])
......@@ -10,7 +10,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-15 17:16-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ADRIAN ABREU GONZALEZ <aabreuglez@gmail.com>, 2016\n"
"Last-Translator: URJConline <online.mooc@urjc.es>, 2017\n"
"Language-Team: Spanish (Spain) (https://www.transifex.com/open-edx/teams/6205/es_ES/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
......
......@@ -10,7 +10,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-15 17:16-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Mariangeles Fernandez <mariangelesfm@gmail.com>, 2016\n"
"Last-Translator: Miguel Angel Cordova <miguel.angel.cordova.uned@gmail.com>, 2017\n"
"Language-Team: Spanish (Spain) (https://www.transifex.com/open-edx/teams/6205/es_ES/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
......
......@@ -10,7 +10,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-15 17:16-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Matheus Gomes Correia <matheus.gomes03@hotmail.com>, 2017\n"
"Last-Translator: Paulo Romano <pauloromanocarvalho@gmail.com>, 2017\n"
"Language-Team: Portuguese (Brazil) (https://www.transifex.com/open-edx/teams/6205/pt_BR/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
......
......@@ -10,7 +10,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-15 17:16-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Matheus Gomes Correia <matheus.gomes03@hotmail.com>, 2017\n"
"Last-Translator: Paulo Romano <pauloromanocarvalho@gmail.com>, 2017\n"
"Language-Team: Portuguese (Brazil) (https://www.transifex.com/open-edx/teams/6205/pt_BR/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
......
......@@ -10,7 +10,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-15 17:16-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Manuel Pacurar <manuel@pcdoctor.ro>, 2017\n"
"Last-Translator: Mihaela Caraulasu <mihaela@accelerole.com>, 2017\n"
"Language-Team: Romanian (https://www.transifex.com/open-edx/teams/6205/ro/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
......
......@@ -10,7 +10,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-15 17:16-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Manuel Pacurar <manuel@pcdoctor.ro>, 2017\n"
"Last-Translator: Mihaela Caraulasu <mihaela@accelerole.com>, 2017\n"
"Language-Team: Romanian (https://www.transifex.com/open-edx/teams/6205/ro/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
......
......@@ -10,7 +10,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-15 17:16-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Mehlika Pişkin <mehlikapiskin@gmail.com>, 2016\n"
"Last-Translator: Ali Işıngör <ali@artistanbul.io>, 2017\n"
"Language-Team: Turkish (https://www.transifex.com/open-edx/teams/6205/tr/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
......
......@@ -10,7 +10,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-15 17:16-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Oğuzhan KAYAR <oguzhank77@gmail.com>, 2016\n"
"Last-Translator: Ali Işıngör <ali@artistanbul.io>, 2017\n"
"Language-Team: Turkish (https://www.transifex.com/open-edx/teams/6205/tr/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
......
......@@ -10,7 +10,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-15 17:16-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: junnyml <inactive+junnyml@transifex.com>, 2016\n"
"Last-Translator: olexiim <olexiim@gmail.com>, 2016\n"
"Language-Team: Ukrainian (https://www.transifex.com/open-edx/teams/6205/uk/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
......@@ -56,7 +56,7 @@ msgstr ""
#: admin.py:383
msgid "Started"
msgstr ""
msgstr "Розпочався"
#: admin.py:384
msgid "Ready To Submit"
......@@ -80,7 +80,7 @@ msgstr ""
#: admin.py:389
msgid "Verified"
msgstr ""
msgstr "Перевірений"
#: admin.py:390
msgid "Rejected"
......@@ -504,7 +504,7 @@ msgstr ""
#: templates/proctored_exam/instructions.html:50
msgid "Start Proctored Exam"
msgstr ""
msgstr "Розпочати контрольовані іспити"
#: templates/proctored_exam/instructions.html:60
msgid "Close"
......
......@@ -10,7 +10,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-15 17:16-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: IvanPrimachenko <jastudent1@gmail.com>, 2016\n"
"Last-Translator: olexiim <olexiim@gmail.com>, 2016\n"
"Language-Team: Ukrainian (https://www.transifex.com/open-edx/teams/6205/uk/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
......@@ -56,7 +56,7 @@ msgstr ""
#: admin.py:383
msgid "Started"
msgstr ""
msgstr "Розпочався"
#: admin.py:384
msgid "Ready To Submit"
......@@ -80,7 +80,7 @@ msgstr ""
#: admin.py:389
msgid "Verified"
msgstr ""
msgstr "Перевірений"
#: admin.py:390
msgid "Rejected"
......@@ -504,7 +504,7 @@ msgstr ""
#: templates/proctored_exam/instructions.html:50
msgid "Start Proctored Exam"
msgstr ""
msgstr "Розпочати контрольовані іспити"
#: templates/proctored_exam/instructions.html:60
msgid "Close"
......
......@@ -10,7 +10,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-15 17:16-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Anh Phan <vietnamesel10n@gmail.com>, 2016\n"
"Last-Translator: Hoà Lê Thanh <hoa.lethanh@gmail.com>, 2017\n"
"Language-Team: Vietnamese (https://www.transifex.com/open-edx/teams/6205/vi/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
......
......@@ -10,7 +10,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-15 17:16-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Anh Phan <vietnamesel10n@gmail.com>, 2016\n"
"Last-Translator: Hoà Lê Thanh <hoa.lethanh@gmail.com>, 2017\n"
"Language-Team: Vietnamese (https://www.transifex.com/open-edx/teams/6205/vi/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
......
......@@ -10,7 +10,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-15 17:16-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: jsgang <jsgang9@gmail.com>, 2017\n"
"Last-Translator: 张太红 <zth@xjau.edu.cn>, 2017\n"
"Language-Team: Chinese (China) (https://www.transifex.com/open-edx/teams/6205/zh_CN/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
......
......@@ -10,7 +10,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-15 17:16-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: jsgang <jsgang9@gmail.com>, 2017\n"
"Last-Translator: 张太红 <zth@xjau.edu.cn>, 2017\n"
"Language-Team: Chinese (China) (https://www.transifex.com/open-edx/teams/6205/zh_CN/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
......
......@@ -394,27 +394,20 @@ class ProctoredExamStudentAttemptManager(models.Manager):
exam_attempt_obj = None
return exam_attempt_obj
def get_all_exam_attempts(self, course_id, timed_exams_only=False):
def get_all_exam_attempts(self, course_id):
"""
Returns the Student Exam Attempts for the given course_id.
"""
filtered_query = Q(proctored_exam__course_id=course_id)
if timed_exams_only:
filtered_query = filtered_query & Q(proctored_exam__is_proctored=False)
return self.filter(filtered_query).order_by('-created')
def get_filtered_exam_attempts(self, course_id, search_by, timed_exams_only=False):
def get_filtered_exam_attempts(self, course_id, search_by):
"""
Returns the Student Exam Attempts for the given course_id filtered by search_by.
"""
filtered_query = Q(proctored_exam__course_id=course_id) & (
Q(user__username__contains=search_by) | Q(user__email__contains=search_by)
)
if timed_exams_only:
filtered_query = filtered_query & Q(proctored_exam__is_proctored=False)
return self.filter(filtered_query).order_by('-created') # pylint: disable=no-member
def get_proctored_exam_attempts(self, course_id, username):
......@@ -720,14 +713,11 @@ class ProctoredExamStudentAllowance(TimeStampedModel):
verbose_name = 'proctored allowance'
@classmethod
def get_allowances_for_course(cls, course_id, timed_exams_only=False):
def get_allowances_for_course(cls, course_id):
"""
Returns all the allowances for a course.
"""
filtered_query = Q(proctored_exam__course_id=course_id)
if timed_exams_only:
filtered_query = filtered_query & Q(proctored_exam__is_proctored=False)
return cls.objects.filter(filtered_query)
@classmethod
......
......@@ -49,6 +49,9 @@ var edx = edx || {};
/* will call into the rendering */
this.model.fetch();
},
events: {
'click #toggle_timer': 'toggleTimerVisibility'
},
detectScroll: function(event) {
if ($(event.currentTarget).scrollTop() > this.timerBarTopPosition) {
$(".proctored_exam_status").addClass('is-fixed');
......@@ -171,6 +174,22 @@ var edx = edx || {};
// refresh the page when the timer expired
self.reloadPage();
}
},
toggleTimerVisibility: function (event) {
var button = $(event.currentTarget);
var icon = button.find('i');
var timer = this.$el.find('span#time_remaining_id b');
if (timer.hasClass('timer-hidden')) {
timer.removeClass('timer-hidden');
button.attr('aria-pressed', 'false');
icon.removeClass('fa-eye').addClass('fa-eye-slash');
} else {
timer.addClass('timer-hidden');
button.attr('aria-pressed', 'true');
icon.removeClass('fa-eye-slash').addClass('fa-eye');
}
event.stopPropagation();
event.preventDefault();
}
});
this.edx.coursware.proctored_exam.ProctoredExamView = edx.coursware.proctored_exam.ProctoredExamView;
......
......@@ -9,7 +9,11 @@ describe('ProctoredExamView', function () {
'You are taking "' +
'<a href="<%= exam_url_path %>"> <%= exam_display_name %> </a>' +
'" as a proctored exam. The timer on the right shows the time remaining in the exam' +
'<span id="time_remaining_id" class="pull-right"> <b> </b> </span> </div>' +
'<span class="exam-timer-clock"> <span id="time_remaining_id">' +
'<b> </b> <button role="button" id="toggle_timer" aria-label="Hide Timer" aria-pressed="false">' +
'<i class="fa fa-eye-slash" aria-hidden="true"></i></button>' +
'</span> </span>' +
'</div>' +
'</script>'+
'</div>'
);
......@@ -55,6 +59,15 @@ describe('ProctoredExamView', function () {
this.proctored_exam_view.render();
expect(this.proctored_exam_view.$el.find('div.exam-timer')).toHaveClass('low-time critical');
});
it('toggles timer visibility correctly', function() {
var button = this.proctored_exam_view.$el.find('#toggle_timer');
var timer = this.proctored_exam_view.$el.find('span#time_remaining_id b');
expect(timer).not.toHaveClass('timer-hidden');
button.click();
expect(timer).toHaveClass('timer-hidden');
button.click();
expect(timer).not.toHaveClass('timer-hidden');
});
it("reload the page when the exam time finishes", function(){
this.proctored_exam_view.secondsLeft = -10;
var reloadPage = spyOn(this.proctored_exam_view, 'reloadPage');
......
......@@ -409,7 +409,7 @@ class ProctoredExamApiTests(ProctoredExamTestCase):
Test to get all the allowances for a course.
"""
allowance = self._add_allowance_for_user()
course_allowances = get_allowances_for_course(self.course_id, False)
course_allowances = get_allowances_for_course(self.course_id)
self.assertEqual(len(course_allowances), 1)
self.assertEqual(course_allowances[0]['proctored_exam']['course_id'], allowance.proctored_exam.course_id)
......@@ -1817,3 +1817,37 @@ class ProctoredExamApiTests(ProctoredExamTestCase):
self.assertEqual(report[1]['review_status'], 'Clean')
self.assertIsNone(report[0]['review_status'])
def test_get_exam_violation_report_with_deleted_exam_attempt(self):
"""
Tests that get_exam_violation_report does not fail in scenerio
where an exam attempt does not exist for related review.
"""
test_exam_id = create_exam(
course_id=self.course_id,
content_id='test_content_1',
exam_name='test_exam',
time_limit_mins=self.default_time_limit
)
test_attempt_id = create_exam_attempt(
exam_id=test_exam_id,
user_id=self.user_id
)
exam1_attempt = ProctoredExamStudentAttempt.objects.get_exam_attempt_by_id(test_attempt_id)
ProctoredExamSoftwareSecureReview.objects.create(
exam=ProctoredExam.get_exam_by_id(test_exam_id),
attempt_code=exam1_attempt.attempt_code,
review_status="Suspicious"
)
# exam attempt is deleted but corresponding review instance exists.
exam1_attempt.delete()
report = get_exam_violation_report(self.course_id)
# call to get_exam_violation_report did not fail. Assert that report is empty as
# the only exam atempt was deleted.
self.assertEqual(len(report), 0)
......@@ -1190,11 +1190,10 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
response_data = json.loads(response.content)
self.assertEqual(len(response_data['proctored_exam_attempts']), 1)
def test_exam_attempts_not_staff(self):
def test_exam_attempts_not_global_staff(self):
"""
Test to get the exam attempts in a course as a not
staff user but still we get the timed exams attempts
but not the proctored exam attempts
Test to get both timed and proctored exam attempts
in a course as a course staff
"""
# Create an timed_exam.
timed_exam = ProctoredExam.objects.create(
......@@ -1241,11 +1240,15 @@ class TestStudentProctoredExamAttempt(LoggedInTestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content)
# we should only get the timed exam attempt in this case
# so the len should be 1
self.assertEqual(len(response_data['proctored_exam_attempts']), 1)
# assert that both timed and proctored exam attempts are in response data
# so the len should be 2
self.assertEqual(len(response_data['proctored_exam_attempts']), 2)
self.assertEqual(
response_data['proctored_exam_attempts'][0]['proctored_exam']['is_proctored'],
proctored_exam.is_proctored
)
self.assertEqual(
response_data['proctored_exam_attempts'][1]['proctored_exam']['is_proctored'],
timed_exam.is_proctored
)
......@@ -2317,9 +2320,9 @@ class TestExamAllowanceView(LoggedInTestCase):
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content)
# we should only get the timed exam allowance
# We are not logged in as a global user
self.assertEqual(len(response_data), 1)
# assert that both timed and proctored exams allowance are in response data
# so the len should be 2
self.assertEqual(len(response_data), 2)
self.assertEqual(response_data[0]['proctored_exam']['course_id'], timed_exam.course_id)
self.assertEqual(response_data[0]['proctored_exam']['content_id'], timed_exam.content_id)
self.assertEqual(response_data[0]['key'], allowance_data['key'])
......
......@@ -611,18 +611,16 @@ class StudentProctoredExamAttemptsByCourse(AuthenticatedAPIView):
def get(self, request, course_id, search_by=None): # pylint: disable=unused-argument
"""
HTTP GET Handler. Returns the status of the exam attempt.
Course and Global staff can view both timed and proctored exam attempts.
"""
# course staff only views attempts of timed exams. edx staff can view both timed and proctored attempts.
time_exams_only = not request.user.is_staff
if search_by is not None:
exam_attempts = ProctoredExamStudentAttempt.objects.get_filtered_exam_attempts(
course_id, search_by, time_exams_only
course_id, search_by
)
attempt_url = reverse('edx_proctoring.proctored_exam.attempts.search', args=[course_id, search_by])
else:
exam_attempts = ProctoredExamStudentAttempt.objects.get_all_exam_attempts(
course_id, time_exams_only
course_id
)
attempt_url = reverse('edx_proctoring.proctored_exam.attempts.course', args=[course_id])
......@@ -701,13 +699,10 @@ class ExamAllowanceView(AuthenticatedAPIView):
def get(self, request, course_id): # pylint: disable=unused-argument
"""
HTTP GET handler. Get all allowances for a course.
Course and Global staff can view both timed and proctored exam allowances.
"""
# course staff only views attempts of timed exams. edx staff can view both timed and proctored attempts.
time_exams_only = not request.user.is_staff
result_set = get_allowances_for_course(
course_id=course_id,
timed_exams_only=time_exams_only
course_id=course_id
)
return Response(result_set)
......
......@@ -19,15 +19,15 @@ module.exports = function(config) {
plugins:[
'karma-jasmine',
'karma-jasmine-jquery',
'karma-firefox-launcher',
'karma-jasmine-jquery',
'karma-chrome-launcher',
'karma-phantomjs-launcher',
'karma-coverage',
'karma-sinon'
],
// start the browser
browsers: ['Firefox'],
browsers: ['PhantomJS'],
//frameworks to use
frameworks: ['jasmine-jquery', 'jasmine', 'sinon'],
......
......@@ -2,7 +2,7 @@
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
nick: proc
owner: edx/teaching-and-learning
owner: edx/educator-dahlia
tags:
- lms
oeps:
......
......@@ -7,12 +7,15 @@
"devDependencies": {
"gulp": "^3.9.0",
"gulp-karma": "0.0.1",
"jasmine-core": "^2.8.0",
"karma": "^0.13.0",
"karma-chrome-launcher": "^0.2.0",
"karma-coverage": "latest",
"karma-firefox-launcher": "latest",
"karma-coverage": "^1.1.1",
"karma-phantomjs-launcher": "^1.0.4",
"karma-jasmine": "^0.3.6",
"karma-jasmine-jquery": "0.1.1",
"karma-sinon": "latest"
"karma-sinon": "^1.0.5",
"phantomjs-prebuilt": "^2.1.14",
"sinon": "^3.2.1"
}
}
......@@ -3,7 +3,7 @@
# Django/Framework Packages
django>=1.8,<1.9a
django-model-utils>=2.3.1
djangorestframework>=3.1
djangorestframework>=3.1,<3.7
django-ipware>=1.1.0
pytz>=2012h
pycrypto>=2.6
......
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