Skip to content

Commit

Permalink
Update learner onboarding status view to consider attempts from other…
Browse files Browse the repository at this point in the history
… courses
  • Loading branch information
bseverino committed Mar 1, 2021
1 parent 87f24e6 commit e91aa7f
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 117 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Change Log
Unreleased
~~~~~~~~~~

[3.7.0] - 2021-03-01
~~~~~~~~~~~~~~~~~~~~
* Update the learner onboarding status view to consider verified attempts from other courses.

[3.6.7] - 2021-02-24
~~~~~~~~~~~~~~~~~~~~
* Fix requirements file
Expand Down
2 changes: 1 addition & 1 deletion edx_proctoring/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"""

# Be sure to update the version number in edx_proctoring/package.json
__version__ = '3.6.7'
__version__ = '3.7.0'

default_app_config = 'edx_proctoring.apps.EdxProctoringConfig' # pylint: disable=invalid-name
26 changes: 24 additions & 2 deletions edx_proctoring/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
# pylint: disable=model-missing-unicode


from datetime import datetime, timedelta

import pytz
from model_utils.models import TimeStampedModel

from django.contrib.auth import get_user_model
Expand Down Expand Up @@ -272,9 +275,10 @@ def get_all_exam_attempts_by_exam_id(self, exam_id):
"""
return self.filter(proctored_exam_id=exam_id)

def get_onboarding_attempts_by_course_id(self, course_id, users=None):
# pylint: disable=invalid-name
def get_proctored_practice_attempts_by_course_id(self, course_id, users=None):
"""
Returns all onboarding attempts for a course, ordered by descending modified field.
Returns all proctored practice attempts for a course, ordered by descending modified field.
Parameters:
* course_id: ID of the course
Expand All @@ -289,6 +293,24 @@ def get_onboarding_attempts_by_course_id(self, course_id, users=None):
queryset = queryset.filter(user__in=users)
return queryset

# pylint: disable=invalid-name
def get_last_verified_proctored_practice_attempt(self, user_id, proctoring_backend):
"""
Returns the user's last verified proctored practice attempt for a specific backend,
if it exists. This only considers attempts within the last two years, as attempts
before this point are considered expired.
Parameters:
* user_id: ID of the user
* proctoring_backend: The name of the proctoring backend
"""
earliest_allowed_date = datetime.now(pytz.UTC) - timedelta(days=730)
return self.filter(
user_id=user_id, taking_as_proctored=True, proctored_exam__is_practice_exam=True,
proctored_exam__backend=proctoring_backend, modified__gt=earliest_allowed_date,
status=ProctoredExamStudentAttemptStatus.verified
).order_by('-modified').first()

def get_filtered_exam_attempts(self, course_id, search_by):
"""
Returns the Student Exam Attempts for the given course_id filtered by search_by.
Expand Down
41 changes: 36 additions & 5 deletions edx_proctoring/static/proctoring/js/views/proctored_exam_info.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,26 @@
error: {
status: gettext('Error'),
message: gettext('An error has occurred during your onboarding exam. Please retry onboarding.')
},
other_course_approved: {
status: gettext('Approved in Another Course'),
message: gettext(
'Your onboarding profile has been approved in another course, ' +
'so you are eligible to take proctored exams in this course. ' +
'However, it is highly recommended that you complete this ' +
'course\'s onboarding exam in order to ensure that your device ' +
'still meets the requirements for proctoring.'
)
},
expiring_soon: {
status: gettext('Expiring Soon'),
message: gettext(
'Your onboarding profile has been approved in another course, ' +
'so you are eligible to take proctored exams in this course. ' +
'However, your onboarding status is expiring soon. Please ' +
'complete onboarding again to ensure that you will be ' +
'able to continue taking proctored exams.'
)
}
};

Expand All @@ -54,9 +74,9 @@
updateCss: function() {
var $el = $(this.el);
var color = '#b20610';
if (this.status === 'verified') {
if (['verified', 'other_course_approved'].includes(this.status)) {
color = '#008100';
} else if (['submitted', 'second_review_required'].includes(this.status)) {
} else if (['submitted', 'second_review_required', 'expiring_soon'].includes(this.status)) {
color = '#0d4e6c';
}

Expand Down Expand Up @@ -104,6 +124,13 @@
}
},

isExpiringSoon: function(expirationDate) {
var today = new Date();
var expirationDateObject = new Date(expirationDate);
// Return true if the expiration date is within 28 days
return today.getTime() > expirationDateObject.getTime() - 2419200000;
},

shouldShowExamLink: function(status) {
// show the exam link if the user should retry onboarding, or if they haven't submitted the exam
var NO_SHOW_STATES = ['submitted', 'second_review_required', 'verified'];
Expand All @@ -114,12 +141,16 @@
var statusText = {};
var data = this.model.toJSON();
if (this.template) {
this.status = data.onboarding_status;
statusText = this.getExamAttemptText(data.onboarding_status);
if (data.expiration_date && this.isExpiringSoon(data.expiration_date)) {
this.status = 'expiring_soon';
} else {
this.status = data.onboarding_status;
}
statusText = this.getExamAttemptText(this.status);
data = {
onboardingStatus: statusText.status,
onboardingMessage: statusText.message,
showOnboardingReminder: data.onboarding_status !== 'verified',
showOnboardingReminder: !['verified', 'other_course_approved'].includes(data.onboarding_status),
showOnboardingExamLink: this.shouldShowExamLink(data.onboarding_status),
onboardingLink: data.onboarding_link
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,18 @@ describe('ProctoredExamInfo', function() {
return (
{
onboarding_status: status,
onboarding_link: 'onboarding_link'
onboarding_link: 'onboarding_link',
expiration_date: null
}
);
}

function expectedOtherCourseApprovedJson(expirationDate) {
return (
{
onboarding_status: 'other_course_approved',
onboarding_link: 'onboarding_link',
expiration_date: expirationDate
}
);
}
Expand Down Expand Up @@ -337,4 +348,59 @@ describe('ProctoredExamInfo', function() {
expect(this.proctored_exam_info.$el.find('.action-onboarding').html())
.toContain('Complete Onboarding');
});

it('should render proctoring info panel correctly for other course approved', function() {
var expirationDate = new Date();
// Set the expiration date 50 days in the future
expirationDate.setTime(expirationDate.getTime() + 3456900000);
this.server.respondWith('GET', '/api/edx_proctoring/v1/user_onboarding/status?course_id=test_course_id',
[
200,
{
'Content-Type': 'application/json'
},
JSON.stringify(expectedOtherCourseApprovedJson(expirationDate.toString()))
]
);
this.proctored_exam_info = new edx.courseware.proctored_exam.ProctoredExamInfo({
el: $('.proctoring-info-panel'),
model: new LearnerOnboardingModel()
});
this.server.respond();
this.server.respond();
expect(this.proctored_exam_info.$el.find('.onboarding-status').html())
.toContain('Approved in Another Course');
expect(this.proctored_exam_info.$el.find('.onboarding-status-message').html())
.toContain('it is highly recommended that you complete this course\'s onboarding exam');
expect(this.proctored_exam_info.$el.find('.action-onboarding').html())
.toContain('Complete Onboarding');
});

it('should render proctoring info panel when expiring soon', function() {
var expirationDate = new Date();
// This message will render if the expiration date is within 28 days
// Set the expiration date 10 days in future
expirationDate.setTime(expirationDate.getTime() + 864800000);
this.server.respondWith('GET', '/api/edx_proctoring/v1/user_onboarding/status?course_id=test_course_id',
[
200,
{
'Content-Type': 'application/json'
},
JSON.stringify(expectedOtherCourseApprovedJson(expirationDate.toString()))
]
);
this.proctored_exam_info = new edx.courseware.proctored_exam.ProctoredExamInfo({
el: $('.proctoring-info-panel'),
model: new LearnerOnboardingModel()
});
this.server.respond();
this.server.respond();
expect(this.proctored_exam_info.$el.find('.onboarding-status').html())
.toContain('Expiring Soon');
expect(this.proctored_exam_info.$el.find('.onboarding-status-message').html())
.toContain('However, your onboarding status is expiring soon.');
expect(this.proctored_exam_info.$el.find('.action-onboarding').html())
.toContain('Complete Onboarding');
});
});
4 changes: 4 additions & 0 deletions edx_proctoring/statuses.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ class InstructorDashboardOnboardingAttemptStatus:
verified = 'verified'
error = 'error'

# The following status is not a true attempt status, but is used when the
# user's onboarding profile is approved in a different course.
other_course_approved = 'other_course_approved'

onboarding_statuses = {
ProctoredExamStudentAttemptStatus.created: setup_started,
ProctoredExamStudentAttemptStatus.download_software_clicked: setup_started,
Expand Down
Loading

0 comments on commit e91aa7f

Please sign in to comment.