Skip to content

Commit

Permalink
[FEATURE] Time limit, grace period, late start, late submit, auto sub…
Browse files Browse the repository at this point in the history
…mit and review submission settings (Simon-Initiative#3544)

* infra, prologue late start prevention, was_late tracking

* auto submit server impl

* beginning of client side impl

* timer and auto submit working

* add 5 min remaining timer

* add 1 minute slack time

* Auto format

* fix compile error

* really fix the compile error

* remove temporary time_limit setting code

* fix two more tests

* add page lifecycle test for was_late

* add enforcement of late start in lifecyle

* update to use finalization summary

* implementation of review_submission setting

---------

Co-authored-by: darrensiegel <darrensiegel@users.noreply.github.com>
  • Loading branch information
darrensiegel and darrensiegel committed May 18, 2023
1 parent 8463d28 commit 13c7cbc
Show file tree
Hide file tree
Showing 18 changed files with 641 additions and 65 deletions.
5 changes: 5 additions & 0 deletions assets/src/phoenix/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { retrieveCookies } from 'components/cookies/utils';
import { CreateAccountPopup } from 'components/messages/CreateAccountPopup';
import { commandButtonClicked } from '../components/editing/elements/command_button/commandButtonClicked';
import { initActivityBridge, initPreviewActivityBridge } from './activity_bridge';
import { initCountdownTimer, initEndDateTimer } from './countdownTimer';
import { finalize } from './finalize';
import { showModal } from './modal';
import { enableSubmitWhenTitleMatches } from './package_delete';
Expand Down Expand Up @@ -91,6 +92,8 @@ window.OLI = {
retrieveCookies,
onReady,
finalize,
initCountdownTimer,
initEndDateTimer,
CreateAccountPopup: (node: any, props: any) => mount(CreateAccountPopup, node, props),
};

Expand Down Expand Up @@ -166,6 +169,8 @@ declare global {
retrieveCookies: typeof retrieveCookies;
onReady: typeof onReady;
finalize: typeof finalize;
initCountdownTimer: typeof initCountdownTimer;
initEndDateTimer: typeof initEndDateTimer;
CreateAccountPopup: (node: any, props: any) => void;
};
keepAlive: () => void;
Expand Down
76 changes: 76 additions & 0 deletions assets/src/phoenix/countdownTimer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
export function initCountdownTimer(
timerId: string,
submitButtonId: string,
timeOutInMins: number,
startTimeInMs: any,
effectiveTimeInMs: any,
autoSubmit: boolean,
) {
const now = new Date().getTime();

if (effectiveTimeInMs > now) {
const timeOutInMs = timeOutInMins * 60 * 1000;

const timeLeft = effectiveTimeInMs - now;
const realDeadlineInMs = timeLeft < timeOutInMs ? now + timeLeft : timeOutInMs + startTimeInMs;

const interval = setInterval(function () {
const now = new Date().getTime();
const distance = realDeadlineInMs - now;
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
update(timerId, 'Time remaining: ' + minutes + 'm ' + seconds + 's ');

if (distance < 0) {
clearInterval(interval);
update(timerId, '');

update(timerId, 'This is a late submission');

if (autoSubmit) {
(document.getElementById(submitButtonId) as any).click();
}
}
}, 1000);
}
}

export function initEndDateTimer(
timerId: string,
submitButtonId: string,
effectiveTimeInMs: any,
autoSubmit: boolean,
) {
const now = new Date().getTime();

if (effectiveTimeInMs > now) {
const timeLeft = effectiveTimeInMs - now;
const realDeadlineInMs = now + timeLeft;

const interval = setInterval(function () {
const now = new Date().getTime();
const distance = realDeadlineInMs - now;
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((distance % (1000 * 60)) / 1000);

if (minutes < 5) {
update(timerId, 'Time remaining: ' + minutes + 'm ' + seconds + 's ');
}

if (distance < 0) {
clearInterval(interval);
update(timerId, '');

update(timerId, 'This is a late submission');

if (autoSubmit) {
(document.getElementById(submitButtonId) as any).click();
}
}
}, 1000);
}
}

function update(id: string, content: string) {
(document.getElementById(id) as any).innerHTML = content;
}
3 changes: 2 additions & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ config :oli, Oban,
snapshots: 20,
selections: 2,
updates: 10,
grades: 30
grades: 30,
auto_submit: 3
]

config :ex_money,
Expand Down
67 changes: 67 additions & 0 deletions lib/oli/delivery/attempts/auto_submit/worker.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
defmodule Oli.Delivery.Attempts.AutoSubmit.Worker do
use Oban.Worker, queue: :auto_submit, max_attempts: 5

alias Oli.Delivery.Attempts.AutoSubmit.Worker
alias Oli.Delivery.Attempts.PageLifecycle
alias Oli.Delivery.Attempts.Core.ResourceAttempt
alias Oli.Delivery.Settings

@moduledoc """
An Oban worker driven page attempts auto submission creator.
"""

@impl Oban.Worker
def perform(%Oban.Job{
args: %{"attempt_guid" => attempt_guid, "section_slug" => section_slug, "datashop_session_id" => datashop_session_id}
}) do
PageLifecycle.finalize(section_slug, attempt_guid, datashop_session_id)
end

@doc """
Possibly schedules a finalization auto submit for a resource attempt. If the resource attempt
is not eligible for auto submit, then no job is scheduled. If the resource attempt is eligible
for auto submit, then a job is scheduled for the deadline and {:ok, job} is returned. If the
resource attempt is already past the deadline or if there is no deadline or late_submit == :allow
then {:ok, :not_scheduled} is returned.
"""
def maybe_schedule_auto_submit(effective_settings, section_slug, resource_attempt, datashop_session_id) do

if needs_job?(effective_settings) do
# calculate the deadline, taking into account the grace period
deadline = Settings.determine_effective_deadline(resource_attempt, effective_settings)

# ensure that we only schedule for deadlines in the future
if DateTime.compare(deadline, DateTime.utc_now()) == :lt do
{:ok, :not_scheduled}
else

# we schedule the auto submit job 1 minute past the actual deadline to allow for a client side
# auto submit to take place and cancel this job.
deadline_with_slack = DateTime.add(deadline, 1, :minute)

{:ok, job} = %{attempt_guid: resource_attempt.attempt_guid, section_slug: section_slug, datashop_session_id: datashop_session_id}
|> Worker.new(scheduled_at: deadline_with_slack)
|> Oban.insert()

{:ok, job.id}
end

else
{:ok, :not_scheduled}
end

end

def cancel_auto_submit(%ResourceAttempt{auto_submit_job_id: id}), do: Oban.cancel_job(id)

# We only need an auto submit job when there is a deadline and the late_submit policy
# is :disallow. If the late_submit policy is :allow, then we don't need to schedule.
defp needs_job?(es) do
has_deadline?(es) and es.late_submit == :disallow
end

defp has_deadline?(effective_settings) do
effective_settings.end_date != nil or effective_settings.time_limit > 0
end

end
2 changes: 2 additions & 0 deletions lib/oli/delivery/attempts/core/resource_access.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule Oli.Delivery.Attempts.Core.ResourceAccess do
field(:score, :float)
field(:out_of, :float)
field(:progress, :float)
field(:was_late, :boolean, default: false)

# Completed LMS grade updates
field(:last_successful_grade_update_id, :integer)
Expand All @@ -30,6 +31,7 @@ defmodule Oli.Delivery.Attempts.Core.ResourceAccess do
:score,
:out_of,
:progress,
:was_late,
:last_successful_grade_update_id,
:last_grade_update_id,
:user_id,
Expand Down
4 changes: 4 additions & 0 deletions lib/oli/delivery/attempts/core/resource_attempt.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ defmodule Oli.Delivery.Attempts.Core.ResourceAttempt do
field(:state, :map, default: %{})
field(:content, :map)
field(:errors, {:array, :string}, default: [])
field(:was_late, :boolean, default: false)
field(:auto_submit_job_id, :integer)

belongs_to(:resource_access, Oli.Delivery.Attempts.Core.ResourceAccess)
belongs_to(:revision, Oli.Resources.Revision)
Expand All @@ -34,6 +36,8 @@ defmodule Oli.Delivery.Attempts.Core.ResourceAttempt do
:lifecycle_state,
:content,
:errors,
:was_late,
:auto_submit_job_id,
:state,
:date_evaluated,
:date_submitted,
Expand Down
15 changes: 12 additions & 3 deletions lib/oli/delivery/attempts/manual_grading.ex
Original file line number Diff line number Diff line change
Expand Up @@ -310,18 +310,27 @@ defmodule Oli.Delivery.Attempts.ManualGrading do
do: {:ok, resource_attempt_guid}

defp maybe_finalize_resource_attempt(section, true, resource_attempt_guid) do

resource_attempt = Oli.Delivery.Attempts.Core.get_resource_attempt(attempt_guid: resource_attempt_guid)
|> Oli.Repo.preload(:revision)

resource_access = Oli.Repo.get(ResourceAccess, resource_attempt.resource_access_id)
effective_settings = Oli.Delivery.Settings.get_combined_settings(resource_attempt.revision, section.id, resource_access.user_id)

case Oli.Delivery.Attempts.PageLifecycle.Graded.roll_up_activities_to_resource_attempt(
resource_attempt_guid
resource_attempt,
effective_settings
) do
{:ok, %ResourceAttempt{lifecycle_state: :evaluated, revision: revision, resource_access_id: resource_access_id}} ->
{:ok, %ResourceAttempt{lifecycle_state: :evaluated, revision: revision, resource_access_id: resource_access_id, was_late: was_late}} ->

resource_access = Oli.Repo.get(ResourceAccess, resource_access_id)
effective_settings = Oli.Delivery.Settings.get_combined_settings(revision, section.id, resource_access.user_id)

Oli.Delivery.Attempts.PageLifecycle.Graded.roll_up_resource_attempts_to_access(
effective_settings,
section.slug,
resource_access_id
resource_access_id,
was_late
)
|> maybe_initiate_grade_passback(section)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,23 @@ defmodule Oli.Delivery.Attempts.PageLifecycle.FinalizationSummary do
:resource_access,
:part_attempt_guids,
:lifecycle_state,
:graded
:graded,
:effective_settings
]

defstruct [
:resource_access,
:part_attempt_guids,
:lifecycle_state,
:graded
:graded,
:effective_settings
]

@type t() :: %__MODULE__{
resource_access: any(),
part_attempt_guids: list(),
lifecycle_state: atom(),
graded: boolean()
graded: boolean(),
effective_settings: struct()
}
end
Loading

0 comments on commit 13c7cbc

Please sign in to comment.