Skip to content

Commit

Permalink
Show exam time remaining based on PrairieTest (#4842)
Browse files Browse the repository at this point in the history
* Show exam time remaining based on PrairieTest

* Change redirection rule if POST for timeLimitFinish passes access validation but time limit hasn't passed

* Remove PS handling

* Change tests that finish on time limit to set an expiring date

* Proper restore of reservation checks
  • Loading branch information
jonatanschroeder committed Oct 1, 2021
1 parent 1760ad0 commit 66a39f7
Show file tree
Hide file tree
Showing 12 changed files with 48 additions and 17 deletions.
7 changes: 3 additions & 4 deletions middlewares/selectAndAuthzAssessmentInstance.sql
Expand Up @@ -11,13 +11,12 @@ SELECT
jsonb_set(to_jsonb(ai), '{formatted_date}',
to_jsonb(format_date_full_compact(ai.date, COALESCE(ci.display_timezone, c.display_timezone)))) AS assessment_instance,
CASE
WHEN ai.date_limit IS NULL THEN NULL
ELSE floor(extract(epoch from (ai.date_limit - $req_date::timestamptz)) * 1000)
WHEN COALESCE(aai.exam_access_end, ai.date_limit) IS NOT NULL THEN floor(extract(epoch from (LEAST(aai.exam_access_end, ai.date_limit) - $req_date::timestamptz)) * 1000)
END AS assessment_instance_remaining_ms,
CASE
WHEN ai.date_limit IS NULL THEN NULL
ELSE floor(extract(epoch from (ai.date_limit - ai.date)) * 1000)
WHEN COALESCE(aai.exam_access_end, ai.date_limit) IS NOT NULL THEN floor(extract(epoch from (LEAST(aai.exam_access_end, ai.date_limit) - ai.date)) * 1000)
END AS assessment_instance_time_limit_ms,
(ai.date_limit IS NOT NULL AND ai.date_limit <= $req_date::timestamptz) AS assessment_instance_time_limit_expired,
to_jsonb(u) AS instance_user,
users_get_displayed_role(u.user_id, ci.id) AS instance_role,
to_jsonb(a) AS assessment,
Expand Down
7 changes: 3 additions & 4 deletions middlewares/selectAndAuthzInstanceQuestion.sql
Expand Up @@ -27,13 +27,12 @@ SELECT
jsonb_set(to_jsonb(ai), '{formatted_date}',
to_jsonb(format_date_full_compact(ai.date, COALESCE(ci.display_timezone, c.display_timezone)))) AS assessment_instance,
CASE
WHEN ai.date_limit IS NULL THEN NULL
ELSE floor(extract(epoch from (date_limit - $req_date::timestamptz)) * 1000)
WHEN COALESCE(aai.exam_access_end, ai.date_limit) IS NOT NULL THEN floor(extract(epoch from (LEAST(aai.exam_access_end, ai.date_limit) - $req_date::timestamptz)) * 1000)
END AS assessment_instance_remaining_ms,
CASE
WHEN ai.date_limit IS NULL THEN NULL
ELSE floor(extract(epoch from (ai.date_limit - ai.date)) * 1000)
WHEN COALESCE(aai.exam_access_end, ai.date_limit) IS NOT NULL THEN floor(extract(epoch from (LEAST(aai.exam_access_end, ai.date_limit) - ai.date)) * 1000)
END AS assessment_instance_time_limit_ms,
(ai.date_limit IS NOT NULL AND ai.date_limit <= $req_date::timestamptz) AS assessment_instance_time_limit_expired,
to_jsonb(u) AS instance_user,
users_get_displayed_role(u.user_id, ci.id) AS instance_role,
to_jsonb(g) AS instance_group,
Expand Down
5 changes: 4 additions & 1 deletion pages/partials/countdown.ejs
Expand Up @@ -73,7 +73,10 @@ $(function() {
var updateServerRemainingMS = function() {
<% if (typeof serverUpdateURL !== 'undefined' && serverUpdateURL) { %>
$.get("<%= serverUpdateURL %>", handleServerResponseRemainingMS, 'json');
$.get("<%= serverUpdateURL %>", handleServerResponseRemainingMS, 'json')
<% if (typeof serverUpdateFailFunction !== 'undefined' && serverUpdateFailFunction) { %>
.fail(<%= serverUpdateFailFunction %>);
<% } %>
<% } %>
};
Expand Down
6 changes: 5 additions & 1 deletion pages/partials/examTimeLimitCountdown.ejs
Expand Up @@ -20,6 +20,9 @@
return 'bg-danger';
}
};
window.serverUpdateFail = function(jqXHR, textStatus, error) {
window.location.reload(true);
}
</script>

<%- include('countdown', {
Expand All @@ -28,6 +31,7 @@
progressSelector: progressSelector,
displaySelector: displaySelector,
serverUpdateURL: typeof serverUpdateURL === "undefined" ? null : serverUpdateURL,
serverUpdateFailFunction: "window.serverUpdateFail",
timerOutCode: "window.submitTimeLimitFinish();",
backgroundColorFunction: "window.timeLimitBGColor",
}) %>
}) %>
Expand Up @@ -42,6 +42,10 @@ router.post('/', function(req, res, next) {
} else if (req.body.__action == 'finish') {
closeExam = true;
} else if (req.body.__action == 'timeLimitFinish') {
// Only close if the timer expired due to time limit, not for access end
if (!res.locals.assessment_instance_time_limit_expired) {
return res.redirect(req.originalUrl);
}
closeExam = true;
} else {
next(error.make(400, 'unknown __action', {locals: res.locals, body: req.body}));
Expand Down
Expand Up @@ -75,6 +75,10 @@ router.post('/', function(req, res, next) {
} else if (req.body.__action == 'timeLimitFinish') {
const closeExam = true;
const overrideGradeRate = false;
// Only close if the timer expired due to time limit, not for access end
if (!res.locals.assessment_instance_time_limit_expired) {
return res.redirect(req.originalUrl);
}
assessment.gradeAssessmentInstance(res.locals.assessment_instance.id, res.locals.authn_user.user_id, closeExam, overrideGradeRate, function(err) {
if (ERR(err, next)) return;
res.redirect(res.locals.urlPrefix + '/assessment_instance/' + res.locals.assessment_instance.id + '?timeLimitExpired=true');
Expand Down
2 changes: 2 additions & 0 deletions sprocs/authz_assessment.sql
Expand Up @@ -5,6 +5,7 @@ CREATE FUNCTION
IN req_date timestamptz,
IN display_timezone text,
OUT authorized boolean, -- Is this assessment available for the given user?
OUT exam_access_end timestamptz, -- If in exam mode, when will access end?
OUT credit integer, -- How much credit will they receive?
OUT credit_date_string TEXT, -- For display to the user.
OUT time_limit_min integer, -- The time limit (if any) for this assessment.
Expand Down Expand Up @@ -83,6 +84,7 @@ BEGIN
authorized := user_result.authorized;

-- all other variables are from the effective user authorization
exam_access_end := user_result.exam_access_end;
credit := user_result.credit;
credit_date_string := user_result.credit_date_string;
time_limit_min := user_result.time_limit_min;
Expand Down
2 changes: 2 additions & 0 deletions sprocs/authz_assessment_instance.sql
Expand Up @@ -7,6 +7,7 @@ CREATE FUNCTION
IN group_work boolean,
OUT authorized boolean, -- Is this assessment available for the given user?
OUT authorized_edit boolean, -- Is this assessment available for editing by the given user?
OUT exam_access_end timestamptz, -- If in exam mode, when does access end?
OUT credit integer, -- How much credit will they receive?
OUT credit_date_string TEXT, -- For display to the user.
OUT time_limit_min integer, -- Time limit (if any) for this assessment.
Expand Down Expand Up @@ -35,6 +36,7 @@ BEGIN
FROM authz_assessment(assessment_instance.assessment_id, authz_data, req_date, display_timezone);

-- take most data directly from the assessment_result
exam_access_end := assessment_result.exam_access_end;
credit := assessment_result.credit;
credit_date_string := assessment_result.credit_date_string;
time_limit_min := assessment_result.time_limit_min;
Expand Down
3 changes: 3 additions & 0 deletions sprocs/check_assessment_access.sql
Expand Up @@ -9,6 +9,7 @@ CREATE FUNCTION
IN date TIMESTAMP WITH TIME ZONE,
IN display_timezone text,
OUT authorized boolean, -- Is this assessment available for the given user?
OUT exam_access_end timestamp with time zone, -- If in exam mode, when does access end?
OUT credit integer, -- How much credit will they receive?
OUT credit_date_string TEXT, -- For display to the user.
OUT time_limit_min integer, -- What is the time limit (if any) for this assessment.
Expand All @@ -29,6 +30,7 @@ BEGIN
-- Choose the access rule which grants access ('authorized' is TRUE), if any, and has the highest 'credit'.
SELECT
caar.authorized,
caar.exam_access_end,
aar.credit,
CASE
WHEN aar.credit > 0 AND aar.active THEN
Expand Down Expand Up @@ -56,6 +58,7 @@ BEGIN
aar.id
INTO
authorized,
exam_access_end,
credit,
credit_date_string,
time_limit_min,
Expand Down
6 changes: 4 additions & 2 deletions sprocs/check_assessment_access_rule.sql
Expand Up @@ -6,7 +6,8 @@ CREATE FUNCTION
IN uid text,
IN date TIMESTAMP WITH TIME ZONE,
IN use_date_check BOOLEAN, -- use a separate flag for safety, rather than having 'date = NULL' indicate this
OUT authorized boolean
OUT authorized boolean,
OUT exam_access_end TIMESTAMP WITH TIME ZONE
) AS $$
DECLARE
ps_linked boolean;
Expand Down Expand Up @@ -80,7 +81,8 @@ BEGIN
END IF;

-- is there a checked-in pt_reservation?
PERFORM 1
SELECT r.access_end
INTO exam_access_end
FROM
pt_reservations AS r
JOIN pt_enrollments AS e ON (e.id = r.enrollment_id)
Expand Down
11 changes: 8 additions & 3 deletions tests/testShowClosedAssessment.js
Expand Up @@ -19,7 +19,12 @@ describe('Exam assessment with showCloseAssessment access rule', function() {
context.courseInstanceBaseUrl= `${context.baseUrl}/course_instance/1`;

const headers = {
cookie: 'pl_test_user=test_student', // need student mode to get a timed exam (instructor override bypasses this)
cookie: 'pl_test_user=test_student; pl_test_date=2000-01-19T00:00:01',
// need student mode to get a timed exam (instructor override bypasses this)
};

const headersTimeLimit = {
cookie: 'pl_test_user=test_student; pl_test_date=2000-01-19T12:00:01',
};

before('set up testing server', async function() {
Expand Down Expand Up @@ -54,7 +59,7 @@ describe('Exam assessment with showCloseAssessment access rule', function() {
__action: 'new_instance',
__csrf_token: context.__csrf_token,
};
const response = await helperClient.fetchCheerio(context.assessmentUrl, { method: 'POST', form , headers});
const response = await helperClient.fetchCheerio(context.assessmentUrl, { method: 'POST', form , headers });
assert.isTrue(response.ok);

// We should have been redirected to the assessment instance
Expand All @@ -74,7 +79,7 @@ describe('Exam assessment with showCloseAssessment access rule', function() {
__action: 'timeLimitFinish',
__csrf_token: context.__csrf_token,
};
const response = await helperClient.fetchCheerio(context.assessmentInstanceUrl, { method: 'POST', form , headers});
const response = await helperClient.fetchCheerio(context.assessmentInstanceUrl, { method: 'POST', form , headers: headersTimeLimit});
assert.equal(response.status, 403);

// We should have been redirected back to the same assessment instance
Expand Down
8 changes: 6 additions & 2 deletions tests/testShowClosedAssessmentScore.js
Expand Up @@ -21,7 +21,11 @@ describe('Exam assessment with showClosedAssessment AND showClosedAssessmentScor
context.gradeBookUrl = `${context.courseInstanceBaseUrl}/gradebook`;

const headers = {
cookie: 'pl_test_user=test_student', // need student mode to get a timed exam (instructor override bypasses this)
cookie: 'pl_test_user=test_student; pl_test_date=2000-01-19T00:00:01',
// need student mode to get a timed exam (instructor override bypasses this)
};
const headersTimeLimit = {
cookie: 'pl_test_user=test_student; pl_test_date=2000-01-19T12:00:01',
};

before('set up testing server', async function() {
Expand Down Expand Up @@ -76,7 +80,7 @@ describe('Exam assessment with showClosedAssessment AND showClosedAssessmentScor
__action: 'timeLimitFinish',
__csrf_token: context.__csrf_token,
};
const response = await helperClient.fetchCheerio(context.assessmentInstanceUrl, { method: 'POST', form , headers});
const response = await helperClient.fetchCheerio(context.assessmentInstanceUrl, { method: 'POST', form , headers: headersTimeLimit });
assert.equal(response.status, 403);

// We should have been redirected back to the same assessment instance
Expand Down

0 comments on commit 66a39f7

Please sign in to comment.