MOBILE-3919 assign: Support time limit in assigns
parent
f02ba784e3
commit
1da073eefa
|
@ -331,14 +331,18 @@
|
||||||
"addon.mod_assign.allowsubmissionsfromdatesummary": "assign",
|
"addon.mod_assign.allowsubmissionsfromdatesummary": "assign",
|
||||||
"addon.mod_assign.applytoteam": "assign",
|
"addon.mod_assign.applytoteam": "assign",
|
||||||
"addon.mod_assign.assignmentisdue": "assign",
|
"addon.mod_assign.assignmentisdue": "assign",
|
||||||
|
"addon.mod_assign.assigntimeleft": "assign",
|
||||||
"addon.mod_assign.attemptnumber": "assign",
|
"addon.mod_assign.attemptnumber": "assign",
|
||||||
"addon.mod_assign.attemptreopenmethod": "assign",
|
"addon.mod_assign.attemptreopenmethod": "assign",
|
||||||
"addon.mod_assign.attemptreopenmethod_manual": "assign",
|
"addon.mod_assign.attemptreopenmethod_manual": "assign",
|
||||||
"addon.mod_assign.attemptreopenmethod_untilpass": "assign",
|
"addon.mod_assign.attemptreopenmethod_untilpass": "assign",
|
||||||
"addon.mod_assign.attemptsettings": "assign",
|
"addon.mod_assign.attemptsettings": "assign",
|
||||||
|
"addon.mod_assign.beginassignment": "assign",
|
||||||
|
"addon.mod_assign.caneditsubmission": "assign",
|
||||||
"addon.mod_assign.cannoteditduetostatementsubmission": "local_moodlemobileapp",
|
"addon.mod_assign.cannoteditduetostatementsubmission": "local_moodlemobileapp",
|
||||||
"addon.mod_assign.cannotgradefromapp": "local_moodlemobileapp",
|
"addon.mod_assign.cannotgradefromapp": "local_moodlemobileapp",
|
||||||
"addon.mod_assign.cannotsubmitduetostatementsubmission": "local_moodlemobileapp",
|
"addon.mod_assign.cannotsubmitduetostatementsubmission": "local_moodlemobileapp",
|
||||||
|
"addon.mod_assign.confirmstart": "assign",
|
||||||
"addon.mod_assign.confirmsubmission": "assign",
|
"addon.mod_assign.confirmsubmission": "assign",
|
||||||
"addon.mod_assign.currentattempt": "assign",
|
"addon.mod_assign.currentattempt": "assign",
|
||||||
"addon.mod_assign.currentattemptof": "assign",
|
"addon.mod_assign.currentattemptof": "assign",
|
||||||
|
@ -416,7 +420,10 @@
|
||||||
"addon.mod_assign.submitassignment_help": "assign",
|
"addon.mod_assign.submitassignment_help": "assign",
|
||||||
"addon.mod_assign.submittedearly": "assign",
|
"addon.mod_assign.submittedearly": "assign",
|
||||||
"addon.mod_assign.submittedlate": "assign",
|
"addon.mod_assign.submittedlate": "assign",
|
||||||
|
"addon.mod_assign.submittedovertime": "assign",
|
||||||
|
"addon.mod_assign.submittedundertime": "assign",
|
||||||
"addon.mod_assign.syncblockedusercomponent": "local_moodlemobileapp",
|
"addon.mod_assign.syncblockedusercomponent": "local_moodlemobileapp",
|
||||||
|
"addon.mod_assign.timelimit": "assign",
|
||||||
"addon.mod_assign.timemodified": "assign",
|
"addon.mod_assign.timemodified": "assign",
|
||||||
"addon.mod_assign.timeremaining": "assign",
|
"addon.mod_assign.timeremaining": "assign",
|
||||||
"addon.mod_assign.ungroupedusers": "assign",
|
"addon.mod_assign.ungroupedusers": "assign",
|
||||||
|
|
|
@ -33,6 +33,14 @@
|
||||||
</ion-select>
|
</ion-select>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item class="ion-text-wrap">
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ 'core.course.hiddenfromstudents' | translate }}</h2>
|
||||||
|
<p *ngIf="module.visible">{{ 'core.no' | translate }}</p>
|
||||||
|
<p *ngIf="!module.visible">{{ 'core.yes' | translate }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
<ion-item class="ion-text-wrap" *ngIf="timeRemaining">
|
<ion-item class="ion-text-wrap" *ngIf="timeRemaining">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2>
|
<h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2>
|
||||||
|
|
|
@ -86,6 +86,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
|
||||||
protected savedObserver?: CoreEventObserver;
|
protected savedObserver?: CoreEventObserver;
|
||||||
protected submittedObserver?: CoreEventObserver;
|
protected submittedObserver?: CoreEventObserver;
|
||||||
protected gradedObserver?: CoreEventObserver;
|
protected gradedObserver?: CoreEventObserver;
|
||||||
|
protected startedObserver?: CoreEventObserver;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected content?: IonContent,
|
protected content?: IonContent,
|
||||||
|
@ -136,6 +137,13 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
|
||||||
}
|
}
|
||||||
}, this.siteId);
|
}, this.siteId);
|
||||||
|
|
||||||
|
this.startedObserver = CoreEvents.on(AddonModAssignProvider.STARTED_EVENT, (data) => {
|
||||||
|
if (this.assign && data.assignmentId == this.assign.id) {
|
||||||
|
// Assignment submission started, refresh data.
|
||||||
|
this.showLoadingAndRefresh(false, false);
|
||||||
|
}
|
||||||
|
}, this.siteId);
|
||||||
|
|
||||||
await this.loadContent(false, true);
|
await this.loadContent(false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,12 +175,17 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
|
||||||
if (submissions.canviewsubmissions) {
|
if (submissions.canviewsubmissions) {
|
||||||
|
|
||||||
// Calculate the messages to display about time remaining and late submissions.
|
// Calculate the messages to display about time remaining and late submissions.
|
||||||
|
this.timeRemaining = '';
|
||||||
|
this.lateSubmissions = '';
|
||||||
|
|
||||||
if (this.assign.duedate > 0) {
|
if (this.assign.duedate > 0) {
|
||||||
if (this.assign.duedate - time <= 0) {
|
if (this.assign.duedate - time <= 0) {
|
||||||
this.timeRemaining = Translate.instant('addon.mod_assign.assignmentisdue');
|
this.timeRemaining = Translate.instant('addon.mod_assign.assignmentisdue');
|
||||||
} else {
|
} else {
|
||||||
this.timeRemaining = CoreTimeUtils.formatDuration(this.assign.duedate - time, 3);
|
this.timeRemaining = CoreTimeUtils.formatTime(this.assign.duedate - time);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.assign.duedate < time) {
|
||||||
if (this.assign.cutoffdate) {
|
if (this.assign.cutoffdate) {
|
||||||
if (this.assign.cutoffdate > time) {
|
if (this.assign.cutoffdate > time) {
|
||||||
this.lateSubmissions = Translate.instant(
|
this.lateSubmissions = Translate.instant(
|
||||||
|
@ -182,13 +195,8 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
|
||||||
} else {
|
} else {
|
||||||
this.lateSubmissions = Translate.instant('addon.mod_assign.nomoresubmissionsaccepted');
|
this.lateSubmissions = Translate.instant('addon.mod_assign.nomoresubmissionsaccepted');
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
this.lateSubmissions = '';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
this.timeRemaining = '';
|
|
||||||
this.lateSubmissions = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if groupmode is enabled to avoid showing wrong numbers.
|
// Check if groupmode is enabled to avoid showing wrong numbers.
|
||||||
|
@ -398,6 +406,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
|
||||||
this.savedObserver?.off();
|
this.savedObserver?.off();
|
||||||
this.submittedObserver?.off();
|
this.submittedObserver?.off();
|
||||||
this.gradedObserver?.off();
|
this.gradedObserver?.off();
|
||||||
|
this.startedObserver?.off();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
<core-loading [hideUntil]="loaded">
|
<core-loading [hideUntil]="loaded">
|
||||||
<div class="list-item-limited-width">
|
<div class="list-item-limited-width">
|
||||||
|
<!-- Time limit is over. -->
|
||||||
|
<ion-card *ngIf="timeLimitFinished" class="core-danger-card">
|
||||||
|
<ion-item class="ion-text-wrap">
|
||||||
|
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
|
||||||
|
<ion-label>
|
||||||
|
<p>{{ 'addon.mod_assign.caneditsubmission' | translate }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
<!-- User and status of the submission. -->
|
<!-- User and status of the submission. -->
|
||||||
<ion-item class="ion-text-wrap" *ngIf="!blindMarking && user" core-user-link [userId]="submitId" [courseId]="courseId"
|
<ion-item class="ion-text-wrap" *ngIf="!blindMarking && user" core-user-link [userId]="submitId" [courseId]="courseId"
|
||||||
[attr.aria-label]="user!.fullname">
|
[attr.aria-label]="user!.fullname">
|
||||||
|
@ -31,33 +41,36 @@
|
||||||
<!-- View the submission tab. -->
|
<!-- View the submission tab. -->
|
||||||
<core-tab [title]="'addon.mod_assign.submission' | translate" id="submission">
|
<core-tab [title]="'addon.mod_assign.submission' | translate" id="submission">
|
||||||
<ng-template>
|
<ng-template>
|
||||||
<addon-mod-assign-submission-plugin *ngFor="let plugin of submissionPlugins" [assign]="assign"
|
|
||||||
[submission]="userSubmission" [plugin]="plugin">
|
|
||||||
</addon-mod-assign-submission-plugin>
|
|
||||||
|
|
||||||
<!-- Render some data about the submission. -->
|
<!-- Render some data about the submission. -->
|
||||||
<ion-item class="ion-text-wrap"
|
<ion-item class="ion-text-wrap" *ngIf="currentAttempt && !isGrading">
|
||||||
*ngIf="userSubmission && userSubmission!.status != statusNew && userSubmission!.timemodified">
|
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>{{ 'addon.mod_assign.timemodified' | translate }}</h2>
|
<h2>{{ 'addon.mod_assign.attemptnumber' | translate }}</h2>
|
||||||
<p>{{ userSubmission!.timemodified * 1000 | coreFormatDate }}</p>
|
<p *ngIf="assign!.maxattempts == unlimitedAttempts">
|
||||||
|
{{ 'addon.mod_assign.outof' | translate :
|
||||||
|
{'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }}
|
||||||
|
</p>
|
||||||
|
<p *ngIf="assign!.maxattempts != unlimitedAttempts">
|
||||||
|
{{ 'addon.mod_assign.outof' | translate :
|
||||||
|
{'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }}
|
||||||
|
</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item class="ion-text-wrap" *ngIf="timeRemaining" [ngClass]="[timeRemainingClass]">
|
<!-- Submission is locked. -->
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="lastAttempt?.locked">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2>
|
<h2>{{ 'addon.mod_assign.submissionslocked' | translate }}</h2>
|
||||||
<p [innerHTML]="timeRemaining"></p>
|
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
|
<!-- Dates. -->
|
||||||
<ion-item class="ion-text-wrap" *ngIf="showDates && fromDate && !isSubmittedForGrading">
|
<ion-item class="ion-text-wrap" *ngIf="showDates && fromDate && !isSubmittedForGrading">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<p *ngIf="assign!.intro"
|
<p *ngIf="assign!.intro"
|
||||||
[innerHTML]="'addon.mod_assign.allowsubmissionsfromdatesummary' | translate: {'$a': fromDate}">
|
[innerHTML]="'addon.mod_assign.allowsubmissionsfromdatesummary' | translate: {'$a': fromDate}">
|
||||||
</p>
|
</p>
|
||||||
<p *ngIf="!assign!.intro" [innerHTML]="'addon.mod_assign.allowsubmissionsanddescriptionfromdatesummary' | translate:
|
<p *ngIf="!assign!.intro" [innerHTML]="'addon.mod_assign.allowsubmissionsanddescriptionfromdatesummary' | translate:
|
||||||
{'$a': fromDate}">
|
{'$a': fromDate}">
|
||||||
</p>
|
</p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
@ -84,20 +97,48 @@
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item class="ion-text-wrap" *ngIf="currentAttempt && !isGrading">
|
<!-- Time remaining. -->
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="timeRemaining || timeLimitEndTime > 0" [ngClass]="[timeRemainingClass]">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>{{ 'addon.mod_assign.attemptnumber' | translate }}</h2>
|
<h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2>
|
||||||
<p *ngIf="assign!.maxattempts == unlimitedAttempts">
|
<p *ngIf="!timeLimitEndTime" [innerHTML]="timeRemaining"></p>
|
||||||
{{ 'addon.mod_assign.outof' | translate :
|
<core-timer *ngIf="timeLimitEndTime > 0" [endTime]="timeLimitEndTime" mode="basic" timeUpText="00:00:00"
|
||||||
{'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }}
|
[timeLeftClassThreshold]="-1" [underTimeClassThresholds]="[300, 900]" (finished)="timeUp()">
|
||||||
</p>
|
</core-timer>
|
||||||
<p *ngIf="assign!.maxattempts != unlimitedAttempts">
|
|
||||||
{{ 'addon.mod_assign.outof' | translate :
|
|
||||||
{'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }}
|
|
||||||
</p>
|
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
|
<!-- Time limit. -->
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="assign && assign.timelimit">
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ 'addon.mod_assign.timelimit' | translate }}</h2>
|
||||||
|
<p>{{ assign.timelimit | coreDuration }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<!-- Editing status. -->
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="lastAttempt && isSubmittedForGrading && lastAttempt!.caneditowner !== undefined"
|
||||||
|
[ngClass]="{submissioneditable: lastAttempt!.caneditowner, submissionnoteditable: !lastAttempt!.caneditowner}">
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ 'addon.mod_assign.editingstatus' | translate }}</h2>
|
||||||
|
<p *ngIf="lastAttempt!.caneditowner">{{ 'addon.mod_assign.submissioneditable' | translate }}</p>
|
||||||
|
<p *ngIf="!lastAttempt!.caneditowner">{{ 'addon.mod_assign.submissionnoteditable' | translate }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<!-- Last modified. -->
|
||||||
|
<ion-item class="ion-text-wrap"
|
||||||
|
*ngIf="userSubmission && userSubmission!.status != statusNew && userSubmission!.timemodified">
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ 'addon.mod_assign.timemodified' | translate }}</h2>
|
||||||
|
<p>{{ userSubmission!.timemodified * 1000 | coreFormatDate }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<addon-mod-assign-submission-plugin *ngFor="let plugin of submissionPlugins" [assign]="assign"
|
||||||
|
[submission]="userSubmission" [plugin]="plugin">
|
||||||
|
</addon-mod-assign-submission-plugin>
|
||||||
|
|
||||||
<!-- Add or edit submission. -->
|
<!-- Add or edit submission. -->
|
||||||
<ion-item class="ion-text-wrap" *ngIf="canEdit">
|
<ion-item class="ion-text-wrap" *ngIf="canEdit">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
|
@ -109,7 +150,12 @@
|
||||||
<!-- If no submission or is new, show add submission. -->
|
<!-- If no submission or is new, show add submission. -->
|
||||||
<ion-button expand="block" class="ion-text-wrap" (click)="goToEdit()" *ngIf="!hasOffline &&
|
<ion-button expand="block" class="ion-text-wrap" (click)="goToEdit()" *ngIf="!hasOffline &&
|
||||||
(!userSubmission || !userSubmission!.status || userSubmission!.status == statusNew)">
|
(!userSubmission || !userSubmission!.status || userSubmission!.status == statusNew)">
|
||||||
{{ 'addon.mod_assign.addsubmission' | translate }}
|
<ng-container *ngIf="!assign?.timelimit || userSubmission?.timestarted">
|
||||||
|
{{ 'addon.mod_assign.addsubmission' | translate }}
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngIf="assign?.timelimit && (!userSubmission || !userSubmission.timestarted)">
|
||||||
|
{{ 'addon.mod_assign.beginassignment' | translate }}
|
||||||
|
</ng-container>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
<!-- If reopened, show addfromprevious and addnewattempt. -->
|
<!-- If reopened, show addfromprevious and addnewattempt. -->
|
||||||
<ng-container *ngIf="!hasOffline && userSubmission?.status == statusReopened">
|
<ng-container *ngIf="!hasOffline && userSubmission?.status == statusReopened">
|
||||||
|
@ -122,9 +168,8 @@
|
||||||
</ion-button>
|
</ion-button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<!-- Else show editsubmission. -->
|
<!-- Else show editsubmission. -->
|
||||||
<ion-button expand="block" class="ion-text-wrap" *ngIf="!hasOffline &&
|
<ion-button expand="block" class="ion-text-wrap" *ngIf="!hasOffline && userSubmission &&
|
||||||
userSubmission && userSubmission!.status &&
|
userSubmission!.status && userSubmission!.status != statusNew &&
|
||||||
userSubmission!.status != statusNew &&
|
|
||||||
userSubmission!.status != statusReopened" (click)="goToEdit()">
|
userSubmission!.status != statusReopened" (click)="goToEdit()">
|
||||||
{{ 'addon.mod_assign.editsubmission' | translate }}
|
{{ 'addon.mod_assign.editsubmission' | translate }}
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
@ -191,23 +236,6 @@
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Submission is locked. -->
|
|
||||||
<ion-item class="ion-text-wrap" *ngIf="lastAttempt?.locked">
|
|
||||||
<ion-label>
|
|
||||||
<h2>{{ 'addon.mod_assign.submissionslocked' | translate }}</h2>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<!-- Editing status. -->
|
|
||||||
<ion-item class="ion-text-wrap" *ngIf="lastAttempt && isSubmittedForGrading && lastAttempt!.caneditowner !== undefined"
|
|
||||||
[ngClass]="{submissioneditable: lastAttempt!.caneditowner, submissionnoteditable: !lastAttempt!.caneditowner}">
|
|
||||||
<ion-label>
|
|
||||||
<h2>{{ 'addon.mod_assign.editingstatus' | translate }}</h2>
|
|
||||||
<p *ngIf="lastAttempt!.caneditowner">{{ 'addon.mod_assign.submissioneditable' | translate }}</p>
|
|
||||||
<p *ngIf="!lastAttempt!.caneditowner">{{ 'addon.mod_assign.submissionnoteditable' | translate }}</p>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</core-tab>
|
</core-tab>
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
:host ::ng-deep {
|
:host ::ng-deep {
|
||||||
div.latesubmission,
|
ion-item.latesubmission,
|
||||||
div.overdue {
|
ion-item.overdue {
|
||||||
border-bottom: 3px solid var(--danger) !important;
|
border-bottom: 3px solid var(--danger) !important;
|
||||||
ion-icon {
|
ion-icon {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div.earlysubmission {
|
ion-item.earlysubmission {
|
||||||
border-bottom: 3px solid var(--success) !important;
|
border-bottom: 3px solid var(--success) !important;
|
||||||
ion-icon {
|
ion-icon {
|
||||||
color: var(--success);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div.submissioneditable p {
|
ion-item.submissioneditable p {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,10 +26,22 @@
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
core-timer .core-timer {
|
||||||
|
&.core-timer-under-300 {
|
||||||
|
background-color: var(--danger-tint);
|
||||||
|
color: var(--danger-shade);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.core-timer-under-900 {
|
||||||
|
background-color: var(--warning-tint);
|
||||||
|
color: var(--warning-shade);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:host-context(body.dark) ::ng-deep {
|
:host-context(body.dark) ::ng-deep {
|
||||||
div.submissioneditable p {
|
ion-item.submissioneditable p {
|
||||||
color: var(--danger-tint);
|
color: var(--danger-tint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,6 +106,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
|
||||||
submissionPlugins: AddonModAssignPlugin[] = []; // List of submission plugins.
|
submissionPlugins: AddonModAssignPlugin[] = []; // List of submission plugins.
|
||||||
timeRemaining = ''; // Message about time remaining.
|
timeRemaining = ''; // Message about time remaining.
|
||||||
timeRemainingClass = ''; // Class to apply to time remaining message.
|
timeRemainingClass = ''; // Class to apply to time remaining message.
|
||||||
|
timeLimitEndTime = 0; // If time limit is enabled and submission is ongoing, the end time for the timer.
|
||||||
statusTranslated?: string; // Status.
|
statusTranslated?: string; // Status.
|
||||||
statusColor = ''; // Color to apply to the status.
|
statusColor = ''; // Color to apply to the status.
|
||||||
unsupportedEditPlugins: string[] = []; // List of submission plugins that don't support edit.
|
unsupportedEditPlugins: string[] = []; // List of submission plugins that don't support edit.
|
||||||
|
@ -126,6 +127,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
|
||||||
gradeUrl?: string; // URL to grade in browser.
|
gradeUrl?: string; // URL to grade in browser.
|
||||||
isPreviousAttemptEmpty = true; // Whether the previous attempt contains an empty submission.
|
isPreviousAttemptEmpty = true; // Whether the previous attempt contains an empty submission.
|
||||||
showDates = false; // Whether to show some dates.
|
showDates = false; // Whether to show some dates.
|
||||||
|
timeLimitFinished = false; // Whether there is a time limit and it finished, so the user will submit late.
|
||||||
|
|
||||||
// Some constants.
|
// Some constants.
|
||||||
statusNew = AddonModAssignSubmissionStatusValues.NEW;
|
statusNew = AddonModAssignSubmissionStatusValues.NEW;
|
||||||
|
@ -200,7 +202,12 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.assign.duedate <= 0) {
|
const submissionStarted = !!this.userSubmission?.timestarted;
|
||||||
|
this.timeLimitEndTime = 0;
|
||||||
|
this.timeLimitFinished = false;
|
||||||
|
|
||||||
|
if (this.assign.duedate <= 0 && !submissionStarted) {
|
||||||
|
// No due date and no countdown.
|
||||||
this.timeRemaining = '';
|
this.timeRemaining = '';
|
||||||
this.timeRemainingClass = '';
|
this.timeRemainingClass = '';
|
||||||
|
|
||||||
|
@ -208,53 +215,53 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
|
||||||
}
|
}
|
||||||
|
|
||||||
const time = CoreTimeUtils.timestamp();
|
const time = CoreTimeUtils.timestamp();
|
||||||
const dueDate = response.lastattempt?.extensionduedate
|
const timeLimitEnabled = this.assign.timelimit && submissionStarted;
|
||||||
? response.lastattempt.extensionduedate
|
const dueDateReached = this.assign.duedate > 0 && this.assign.duedate - time <= 0;
|
||||||
: this.assign.duedate;
|
const timeLimitEnabledBeforeDueDate = timeLimitEnabled && !dueDateReached;
|
||||||
const timeRemaining = dueDate - time;
|
|
||||||
|
|
||||||
if (timeRemaining > 0) {
|
if (this.userSubmission && this.userSubmission.status === AddonModAssignSubmissionStatusValues.SUBMITTED) {
|
||||||
this.timeRemaining = CoreTimeUtils.formatDuration(timeRemaining, 3);
|
// Submitted, display the relevant early/late message.
|
||||||
this.timeRemainingClass = '';
|
const lateCalculation = this.userSubmission.timemodified -
|
||||||
|
(timeLimitEnabledBeforeDueDate ? this.userSubmission.timecreated : 0);
|
||||||
|
const lateThreshold = timeLimitEnabledBeforeDueDate ? this.assign.timelimit || 0 : this.assign.duedate;
|
||||||
|
const earlyString = timeLimitEnabledBeforeDueDate ? 'submittedundertime' : 'submittedearly';
|
||||||
|
const lateString = timeLimitEnabledBeforeDueDate ? 'submittedovertime' : 'submittedlate';
|
||||||
|
const onTime = lateCalculation <= lateThreshold;
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not submitted.
|
|
||||||
if (!this.userSubmission || this.userSubmission.status != AddonModAssignSubmissionStatusValues.SUBMITTED) {
|
|
||||||
|
|
||||||
if (response.lastattempt?.submissionsenabled || response.gradingsummary?.submissionsenabled) {
|
|
||||||
this.timeRemaining = Translate.instant(
|
|
||||||
'addon.mod_assign.overdue',
|
|
||||||
{ $a: CoreTimeUtils.formatDuration(-timeRemaining, 3) },
|
|
||||||
);
|
|
||||||
this.timeRemainingClass = 'overdue';
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.timeRemaining = Translate.instant('addon.mod_assign.duedatereached');
|
|
||||||
this.timeRemainingClass = '';
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeSubmittedDiff = this.userSubmission.timemodified - dueDate;
|
|
||||||
if (timeSubmittedDiff > 0) {
|
|
||||||
this.timeRemaining = Translate.instant(
|
this.timeRemaining = Translate.instant(
|
||||||
'addon.mod_assign.submittedlate',
|
'addon.mod_assign.' + (onTime ? earlyString : lateString),
|
||||||
{ $a: CoreTimeUtils.formatDuration(timeSubmittedDiff, 2) },
|
{ $a: CoreTimeUtils.formatTime(Math.abs(lateCalculation - lateThreshold)) },
|
||||||
);
|
);
|
||||||
this.timeRemainingClass = 'latesubmission';
|
this.timeRemainingClass = onTime ? 'earlysubmission' : 'latesubmission';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.timeRemaining = Translate.instant(
|
if (dueDateReached) {
|
||||||
'addon.mod_assign.submittedearly',
|
// There is no submission, due date has passed, show assignment is overdue.
|
||||||
{ $a: CoreTimeUtils.formatDuration(-timeSubmittedDiff, 2) },
|
const submissionsEnabled = response.lastattempt?.submissionsenabled || response.gradingsummary?.submissionsenabled;
|
||||||
);
|
this.timeRemaining = Translate.instant(
|
||||||
this.timeRemainingClass = 'earlysubmission';
|
'addon.mod_assign.' + (submissionsEnabled ? 'overdue' : 'duedatereached'),
|
||||||
|
{ $a: CoreTimeUtils.formatTime(time - this.assign.duedate) },
|
||||||
|
);
|
||||||
|
this.timeRemainingClass = 'overdue';
|
||||||
|
this.timeLimitFinished = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeLimitEnabled && submissionStarted) {
|
||||||
|
// An attempt has started and there is a time limit, display the time limit.
|
||||||
|
this.timeRemaining = '';
|
||||||
|
this.timeRemainingClass = 'timeremaining';
|
||||||
|
this.timeLimitEndTime = AddonModAssignHelper.calculateEndTime(this.assign, this.userSubmission);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assignment is not overdue, and no submission has been made. Just display the due date.
|
||||||
|
this.timeRemaining = CoreTimeUtils.formatTime(this.assign.duedate - time);
|
||||||
|
this.timeRemainingClass = 'timeremaining';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -292,7 +299,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
|
||||||
|
|
||||||
if (!this.previousAttempt?.submission) {
|
if (!this.previousAttempt?.submission) {
|
||||||
// Cannot access previous attempts, just go to edit.
|
// Cannot access previous attempts, just go to edit.
|
||||||
return this.goToEdit();
|
return this.goToEdit(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousSubmission = this.previousAttempt.submission;
|
const previousSubmission = this.previousAttempt.submission;
|
||||||
|
@ -319,7 +326,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
|
||||||
try {
|
try {
|
||||||
await AddonModAssignHelper.copyPreviousAttempt(this.assign, previousSubmission);
|
await AddonModAssignHelper.copyPreviousAttempt(this.assign, previousSubmission);
|
||||||
// Now go to edit.
|
// Now go to edit.
|
||||||
this.goToEdit();
|
this.goToEdit(true);
|
||||||
|
|
||||||
if (!this.assign.submissiondrafts && this.userSubmission) {
|
if (!this.assign.submissiondrafts && this.userSubmission) {
|
||||||
// No drafts allowed, so it was submitted. Trigger event.
|
// No drafts allowed, so it was submitted. Trigger event.
|
||||||
|
@ -352,8 +359,24 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Go to the page to add or edit submission.
|
* Go to the page to add or edit submission.
|
||||||
|
*
|
||||||
|
* @param afterCopyPrevious Whether the user has just copied the previous submission.
|
||||||
*/
|
*/
|
||||||
goToEdit(): void {
|
async goToEdit(afterCopyPrevious = false): Promise<void> {
|
||||||
|
if (!afterCopyPrevious && this.assign?.timelimit && (!this.userSubmission || !this.userSubmission.timestarted)) {
|
||||||
|
try {
|
||||||
|
await CoreDomUtils.showConfirm(
|
||||||
|
Translate.instant('addon.mod_assign.confirmstart', {
|
||||||
|
$a: CoreTimeUtils.formatTime(this.assign.timelimit),
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
Translate.instant('addon.mod_assign.beginassignment'),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return; // User canceled.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CoreNavigator.navigateToSitePath(
|
CoreNavigator.navigateToSitePath(
|
||||||
AddonModAssignModuleHandlerService.PAGE_NAME + '/' + this.courseId + '/' + this.moduleId + '/edit',
|
AddonModAssignModuleHandlerService.PAGE_NAME + '/' + this.courseId + '/' + this.moduleId + '/edit',
|
||||||
{
|
{
|
||||||
|
@ -1175,6 +1198,13 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy, Can
|
||||||
this.setGradeSyncBlocked(tab.id === 'grade');
|
this.setGradeSyncBlocked(tab.id === 'grade');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called when the time is up.
|
||||||
|
*/
|
||||||
|
timeUp(): void {
|
||||||
|
this.timeLimitFinished = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being destroyed.
|
* Component being destroyed.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -9,14 +9,18 @@
|
||||||
"allowsubmissionsfromdatesummary": "This assignment will accept submissions from <strong>{{$a}}</strong>",
|
"allowsubmissionsfromdatesummary": "This assignment will accept submissions from <strong>{{$a}}</strong>",
|
||||||
"applytoteam": "Apply grades and feedback to entire group",
|
"applytoteam": "Apply grades and feedback to entire group",
|
||||||
"assignmentisdue": "Assignment is due",
|
"assignmentisdue": "Assignment is due",
|
||||||
|
"assigntimeleft": "Time left",
|
||||||
"attemptnumber": "Attempt number",
|
"attemptnumber": "Attempt number",
|
||||||
"attemptreopenmethod": "Additional attempts",
|
"attemptreopenmethod": "Additional attempts",
|
||||||
"attemptreopenmethod_manual": "Manually",
|
"attemptreopenmethod_manual": "Manually",
|
||||||
"attemptreopenmethod_untilpass": "Automatically until pass",
|
"attemptreopenmethod_untilpass": "Automatically until pass",
|
||||||
"attemptsettings": "Attempt settings",
|
"attemptsettings": "Attempt settings",
|
||||||
|
"beginassignment": "Begin assignment",
|
||||||
|
"caneditsubmission": "You can submit/edit submission after time limit passed, but it will be marked as late.",
|
||||||
"cannoteditduetostatementsubmission": "You can't add or edit a submission in the app because the submission statement could not be retrieved from the site.",
|
"cannoteditduetostatementsubmission": "You can't add or edit a submission in the app because the submission statement could not be retrieved from the site.",
|
||||||
"cannotgradefromapp": "Certain grading methods are not yet supported by the app and cannot be modified.",
|
"cannotgradefromapp": "Certain grading methods are not yet supported by the app and cannot be modified.",
|
||||||
"cannotsubmitduetostatementsubmission": "You can't make a submission in the app because the submission statement could not be retrieved from the site.",
|
"cannotsubmitduetostatementsubmission": "You can't make a submission in the app because the submission statement could not be retrieved from the site.",
|
||||||
|
"confirmstart": "Your submission will have a time limit of {{$a}}. When you start, the timer will begin to count down and cannot be paused. You must finish your submission before it expires. Are you sure you wish to start now?",
|
||||||
"confirmsubmission": "Are you sure you want to submit your work for grading? You will not be able to make any more changes.",
|
"confirmsubmission": "Are you sure you want to submit your work for grading? You will not be able to make any more changes.",
|
||||||
"currentattempt": "This is attempt {{$a}}.",
|
"currentattempt": "This is attempt {{$a}}.",
|
||||||
"currentattemptof": "This is attempt {{$a.attemptnumber}} ( {{$a.maxattempts}} attempts allowed ).",
|
"currentattemptof": "This is attempt {{$a.attemptnumber}} ( {{$a.maxattempts}} attempts allowed ).",
|
||||||
|
@ -94,7 +98,10 @@
|
||||||
"submitassignment_help": "Once this assignment is submitted you will not be able to make any more changes.",
|
"submitassignment_help": "Once this assignment is submitted you will not be able to make any more changes.",
|
||||||
"submittedearly": "Assignment was submitted {{$a}} early",
|
"submittedearly": "Assignment was submitted {{$a}} early",
|
||||||
"submittedlate": "Assignment was submitted {{$a}} late",
|
"submittedlate": "Assignment was submitted {{$a}} late",
|
||||||
|
"submittedovertime": "Assignment was submitted {{$a}} over the time limit",
|
||||||
|
"submittedundertime": "Assignment was submitted {{$a}} under the time limit",
|
||||||
"syncblockedusercomponent": "user grade",
|
"syncblockedusercomponent": "user grade",
|
||||||
|
"timelimit": "Time limit",
|
||||||
"timemodified": "Last modified",
|
"timemodified": "Last modified",
|
||||||
"timeremaining": "Time remaining",
|
"timeremaining": "Time remaining",
|
||||||
"ungroupedusers": "The setting 'Require group to make submission' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions.",
|
"ungroupedusers": "The setting 'Require group to make submission' is enabled and some users are either not a member of any group, or are a member of more than one group, so are unable to make submissions.",
|
||||||
|
|
|
@ -20,6 +20,11 @@
|
||||||
<core-loading [hideUntil]="loaded">
|
<core-loading [hideUntil]="loaded">
|
||||||
<ion-list *ngIf="userSubmission && userSubmission.plugins && userSubmission.plugins.length">
|
<ion-list *ngIf="userSubmission && userSubmission.plugins && userSubmission.plugins.length">
|
||||||
<!-- @todo: plagiarism_print_disclosure -->
|
<!-- @todo: plagiarism_print_disclosure -->
|
||||||
|
<core-timer *ngIf="timeLimitEndTime > 0" [endTime]="timeLimitEndTime" (finished)="timeUp()" timeUpText="00:00:00"
|
||||||
|
[timerText]="'addon.mod_assign.assigntimeleft' | translate" [align]="'center'" [timeLeftClassThreshold]="-1"
|
||||||
|
[underTimeClassThresholds]="[300, 900]" class="ion-margin-horizontal">
|
||||||
|
</core-timer>
|
||||||
|
|
||||||
<form name="addon-mod_assign-edit-form" #editSubmissionForm>
|
<form name="addon-mod_assign-edit-form" #editSubmissionForm>
|
||||||
<!-- Submission statement. -->
|
<!-- Submission statement. -->
|
||||||
<ion-item class="ion-text-wrap" *ngIf="submissionStatement">
|
<ion-item class="ion-text-wrap" *ngIf="submissionStatement">
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
:host ::ng-deep {
|
||||||
|
core-timer {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.core-timer {
|
||||||
|
&.core-timer-under-300 {
|
||||||
|
background-color: var(--danger-tint);
|
||||||
|
color: var(--danger-shade);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.core-timer-under-900 {
|
||||||
|
background-color: var(--warning-tint);
|
||||||
|
color: var(--warning-shade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,6 +32,7 @@ import {
|
||||||
AddonModAssignSubmissionStatusOptions,
|
AddonModAssignSubmissionStatusOptions,
|
||||||
AddonModAssignGetSubmissionStatusWSResponse,
|
AddonModAssignGetSubmissionStatusWSResponse,
|
||||||
AddonModAssignSavePluginData,
|
AddonModAssignSavePluginData,
|
||||||
|
AddonModAssignSubmissionStatusValues,
|
||||||
} from '../../services/assign';
|
} from '../../services/assign';
|
||||||
import { AddonModAssignHelper } from '../../services/assign-helper';
|
import { AddonModAssignHelper } from '../../services/assign-helper';
|
||||||
import { AddonModAssignOffline } from '../../services/assign-offline';
|
import { AddonModAssignOffline } from '../../services/assign-offline';
|
||||||
|
@ -44,6 +45,7 @@ import { CoreUtils } from '@services/utils/utils';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'page-addon-mod-assign-edit',
|
selector: 'page-addon-mod-assign-edit',
|
||||||
templateUrl: 'edit.html',
|
templateUrl: 'edit.html',
|
||||||
|
styleUrls: ['edit.scss'],
|
||||||
})
|
})
|
||||||
export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
|
export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
|
||||||
|
|
||||||
|
@ -58,6 +60,7 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
|
||||||
submissionStatement?: string; // The submission statement.
|
submissionStatement?: string; // The submission statement.
|
||||||
submissionStatementAccepted = false; // Whether submission statement is accepted.
|
submissionStatementAccepted = false; // Whether submission statement is accepted.
|
||||||
loaded = false; // Whether data has been loaded.
|
loaded = false; // Whether data has been loaded.
|
||||||
|
timeLimitEndTime = 0; // If time limit is enabled, the end time for the timer.
|
||||||
|
|
||||||
protected userId: number; // User doing the submission.
|
protected userId: number; // User doing the submission.
|
||||||
protected isBlind = false; // Whether blind is used.
|
protected isBlind = false; // Whether blind is used.
|
||||||
|
@ -179,6 +182,8 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
|
||||||
throw new CoreError(Translate.instant('core.nopermissions', { $a: this.editText }));
|
throw new CoreError(Translate.instant('core.nopermissions', { $a: this.editText }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
submissionStatus = await this.startSubmissionIfNeeded(submissionStatus, options);
|
||||||
|
|
||||||
this.allowOffline = true; // If offline isn't allowed we shouldn't have reached this point.
|
this.allowOffline = true; // If offline isn't allowed we shouldn't have reached this point.
|
||||||
// Only show submission statement if we are editing our own submission.
|
// Only show submission statement if we are editing our own submission.
|
||||||
if (this.assign.requiresubmissionstatement && !this.assign.submissiondrafts && this.userId == currentUserId) {
|
if (this.assign.requiresubmissionstatement && !this.assign.submissiondrafts && this.userId == currentUserId) {
|
||||||
|
@ -187,6 +192,12 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
|
||||||
this.submissionStatement = undefined;
|
this.submissionStatement = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.assign.timelimit && this.userSubmission?.timestarted) {
|
||||||
|
this.timeLimitEndTime = AddonModAssignHelper.calculateEndTime(this.assign, this.userSubmission);
|
||||||
|
} else {
|
||||||
|
this.timeLimitEndTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if there's any offline data for this submission.
|
// Check if there's any offline data for this submission.
|
||||||
const offlineData = await AddonModAssignOffline.getSubmission(this.assign.id, this.userId);
|
const offlineData = await AddonModAssignOffline.getSubmission(this.assign.id, this.userId);
|
||||||
|
@ -204,6 +215,45 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the submission if needed.
|
||||||
|
*
|
||||||
|
* @param submissionStatus Current submission status.
|
||||||
|
* @param options Options.
|
||||||
|
* @return Promise resolved with the new submission status if it changed, original submission status otherwise.
|
||||||
|
*/
|
||||||
|
protected async startSubmissionIfNeeded(
|
||||||
|
submissionStatus: AddonModAssignGetSubmissionStatusWSResponse,
|
||||||
|
options: AddonModAssignSubmissionStatusOptions,
|
||||||
|
): Promise<AddonModAssignGetSubmissionStatusWSResponse> {
|
||||||
|
if (!this.assign || !this.assign.timelimit) {
|
||||||
|
// Submission only needs to be started if there's a timelimit.
|
||||||
|
return submissionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.userSubmission && this.userSubmission.status !== AddonModAssignSubmissionStatusValues.NEW &&
|
||||||
|
this.userSubmission.status !== AddonModAssignSubmissionStatusValues.REOPENED) {
|
||||||
|
// There is an ongoing submission, no need to start it.
|
||||||
|
return submissionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
await AddonModAssign.startSubmission(this.assign.id);
|
||||||
|
|
||||||
|
CoreEvents.trigger(AddonModAssignProvider.STARTED_EVENT, {
|
||||||
|
assignmentId: this.assign.id,
|
||||||
|
}, CoreSites.getCurrentSiteId());
|
||||||
|
|
||||||
|
// Submission started, update the submission status.
|
||||||
|
const newSubmissionStatus = await AddonModAssign.getSubmissionStatus(this.assign.id, {
|
||||||
|
...options,
|
||||||
|
readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK, // Make sure not to use cache.
|
||||||
|
});
|
||||||
|
|
||||||
|
this.userSubmission = AddonModAssign.getSubmissionObjectFromAttempt(this.assign, newSubmissionStatus.lastattempt);
|
||||||
|
|
||||||
|
return newSubmissionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the input data.
|
* Get the input data.
|
||||||
*
|
*
|
||||||
|
@ -392,6 +442,18 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy, CanLeave {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called when the time is up.
|
||||||
|
*/
|
||||||
|
timeUp(): void {
|
||||||
|
CoreDomUtils.showToastWithOptions({
|
||||||
|
message: Translate.instant('addon.mod_assign.caneditsubmission'),
|
||||||
|
duration: 0,
|
||||||
|
buttons: [Translate.instant('core.dismiss')],
|
||||||
|
cssClass: 'core-danger-toast',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being destroyed.
|
* Component being destroyed.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -44,6 +44,25 @@ import { CoreFileEntry } from '@services/file-helper';
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AddonModAssignHelperProvider {
|
export class AddonModAssignHelperProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the end time (timestamp) for an assign and submission.
|
||||||
|
*
|
||||||
|
* @param assign Assign instance.
|
||||||
|
* @param submission Submission.
|
||||||
|
* @return End time.
|
||||||
|
*/
|
||||||
|
calculateEndTime(assign: AddonModAssignAssign, submission?: AddonModAssignSubmissionFormatted): number {
|
||||||
|
const timeDue = (submission?.timestarted || 0) + (assign.timelimit || 0);
|
||||||
|
|
||||||
|
if (assign.duedate) {
|
||||||
|
return Math.min(timeDue, assign.duedate);
|
||||||
|
} else if (assign.cutoffdate) {
|
||||||
|
return Math.min(timeDue, assign.cutoffdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeDue;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a submission can be edited in offline.
|
* Check if a submission can be edited in offline.
|
||||||
*
|
*
|
||||||
|
|
|
@ -49,6 +49,7 @@ declare module '@singletons/events' {
|
||||||
[AddonModAssignProvider.SUBMISSION_SAVED_EVENT]: AddonModAssignSubmissionSavedEventData;
|
[AddonModAssignProvider.SUBMISSION_SAVED_EVENT]: AddonModAssignSubmissionSavedEventData;
|
||||||
[AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT]: AddonModAssignSubmittedForGradingEventData;
|
[AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT]: AddonModAssignSubmittedForGradingEventData;
|
||||||
[AddonModAssignProvider.GRADED_EVENT]: AddonModAssignGradedEventData;
|
[AddonModAssignProvider.GRADED_EVENT]: AddonModAssignGradedEventData;
|
||||||
|
[AddonModAssignProvider.STARTED_EVENT]: AddonModAssignStartedEventData;
|
||||||
[AddonModAssignSyncProvider.MANUAL_SYNCED]: AddonModAssignManualSyncData;
|
[AddonModAssignSyncProvider.MANUAL_SYNCED]: AddonModAssignManualSyncData;
|
||||||
[AddonModAssignSyncProvider.AUTO_SYNCED]: AddonModAssignAutoSyncData;
|
[AddonModAssignSyncProvider.AUTO_SYNCED]: AddonModAssignAutoSyncData;
|
||||||
}
|
}
|
||||||
|
@ -73,6 +74,7 @@ export class AddonModAssignProvider {
|
||||||
static readonly SUBMISSION_SAVED_EVENT = 'addon_mod_assign_submission_saved';
|
static readonly SUBMISSION_SAVED_EVENT = 'addon_mod_assign_submission_saved';
|
||||||
static readonly SUBMITTED_FOR_GRADING_EVENT = 'addon_mod_assign_submitted_for_grading';
|
static readonly SUBMITTED_FOR_GRADING_EVENT = 'addon_mod_assign_submitted_for_grading';
|
||||||
static readonly GRADED_EVENT = 'addon_mod_assign_graded';
|
static readonly GRADED_EVENT = 'addon_mod_assign_graded';
|
||||||
|
static readonly STARTED_EVENT = 'addon_mod_assign_started';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the user can submit in offline. This should only be used if submissionStatus.lastattempt.cansubmit cannot
|
* Check if the user can submit in offline. This should only be used if submissionStatus.lastattempt.cansubmit cannot
|
||||||
|
@ -1069,6 +1071,35 @@ export class AddonModAssignProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a submission.
|
||||||
|
*
|
||||||
|
* @param assignId Assign ID.
|
||||||
|
* @param siteId Site ID. If not defined, use current site.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async startSubmission(assignId: number, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
const params: AddonModAssignStartSubmissionWSParams = {
|
||||||
|
assignid: assignId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await site.write<AddonModAssignStartSubmissionWSResponse>('mod_assign_start_submission', params);
|
||||||
|
|
||||||
|
if (!result.warnings?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore some warnings.
|
||||||
|
const warning = result.warnings.find(warning =>
|
||||||
|
warning.warningcode !== 'timelimitnotenabled' && warning.warningcode !== 'opensubmissionexists');
|
||||||
|
|
||||||
|
if (warning) {
|
||||||
|
throw new CoreWSError(warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit the current user assignment for grading.
|
* Submit the current user assignment for grading.
|
||||||
*
|
*
|
||||||
|
@ -1351,6 +1382,11 @@ export type AddonModAssignAssign = {
|
||||||
introformat?: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
introformat?: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
||||||
introfiles?: CoreWSExternalFile[];
|
introfiles?: CoreWSExternalFile[];
|
||||||
introattachments?: CoreWSExternalFile[];
|
introattachments?: CoreWSExternalFile[];
|
||||||
|
activity?: string; // @since 4.0. Description of activity.
|
||||||
|
activityformat?: number; // @since 4.0. Format of activity.
|
||||||
|
activityattachments?: CoreWSExternalFile[]; // @since 4.0. Files from activity field.
|
||||||
|
timelimit?: number; // @since 4.0. Time limit to complete assigment.
|
||||||
|
submissionattachments?: number; // @since 4.0. Flag to only show files during submission.
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1395,6 +1431,7 @@ export type AddonModAssignSubmission = {
|
||||||
latest?: number; // Latest attempt.
|
latest?: number; // Latest attempt.
|
||||||
plugins?: AddonModAssignPlugin[]; // Plugins.
|
plugins?: AddonModAssignPlugin[]; // Plugins.
|
||||||
gradingstatus?: AddonModAssignGradingStates; // Grading status.
|
gradingstatus?: AddonModAssignGradingStates; // Grading status.
|
||||||
|
timestarted?: number; // @since 4.0. Submission start time.
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1445,6 +1482,7 @@ export type AddonModAssignSubmissionAttempt = {
|
||||||
blindmarking: boolean; // Whether blind marking is enabled.
|
blindmarking: boolean; // Whether blind marking is enabled.
|
||||||
gradingstatus: AddonModAssignGradingStates; // Grading status.
|
gradingstatus: AddonModAssignGradingStates; // Grading status.
|
||||||
usergroups: number[]; // User groups in the course.
|
usergroups: number[]; // User groups in the course.
|
||||||
|
timelimit?: number; // @since 4.0. Time limit for submission.
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1604,6 +1642,14 @@ export type AddonModAssignGetSubmissionStatusWSResponse = {
|
||||||
lastattempt?: AddonModAssignSubmissionAttempt; // Last attempt information.
|
lastattempt?: AddonModAssignSubmissionAttempt; // Last attempt information.
|
||||||
feedback?: AddonModAssignSubmissionFeedback; // Feedback for the last attempt.
|
feedback?: AddonModAssignSubmissionFeedback; // Feedback for the last attempt.
|
||||||
previousattempts?: AddonModAssignSubmissionPreviousAttempt[]; // List all the previous attempts did by the user.
|
previousattempts?: AddonModAssignSubmissionPreviousAttempt[]; // List all the previous attempts did by the user.
|
||||||
|
assignmentdata?: { // @since 4.0. Extra information about assignment.
|
||||||
|
attachments?: { // Intro and activity attachments.
|
||||||
|
intro?: CoreWSExternalFile[]; // Intro attachments files.
|
||||||
|
activity?: CoreWSExternalFile[]; // Activity attachments files.
|
||||||
|
};
|
||||||
|
activity?: string; // Text of activity.
|
||||||
|
activityformat?: number; // Format of activity.
|
||||||
|
};
|
||||||
warnings?: CoreWSExternalWarning[];
|
warnings?: CoreWSExternalWarning[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1715,6 +1761,25 @@ type AddonModAssignSubmitGradingFormWSParams = {
|
||||||
jsonformdata: string; // The data from the grading form, encoded as a json array.
|
jsonformdata: string; // The data from the grading form, encoded as a json array.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of mod_assign_start_submission WS.
|
||||||
|
*
|
||||||
|
* @since 4.0
|
||||||
|
*/
|
||||||
|
type AddonModAssignStartSubmissionWSParams = {
|
||||||
|
assignid: number; // Assignment instance id.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned by mod_assign_start_submission WS.
|
||||||
|
*
|
||||||
|
* @since 4.0
|
||||||
|
*/
|
||||||
|
export type AddonModAssignStartSubmissionWSResponse = {
|
||||||
|
submissionid: number; // New submission ID.
|
||||||
|
warnings?: CoreWSExternalWarning[];
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assignment grade outcomes.
|
* Assignment grade outcomes.
|
||||||
*/
|
*/
|
||||||
|
@ -1739,6 +1804,13 @@ export type AddonModAssignSubmissionSavedEventData = AddonModAssignSubmittedForG
|
||||||
*/
|
*/
|
||||||
export type AddonModAssignGradedEventData = AddonModAssignSubmittedForGradingEventData;
|
export type AddonModAssignGradedEventData = AddonModAssignSubmittedForGradingEventData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data sent by STARTED_EVENT event.
|
||||||
|
*/
|
||||||
|
export type AddonModAssignStartedEventData = {
|
||||||
|
assignmentId: number;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submission status.
|
* Submission status.
|
||||||
* Constants on LMS starting with ASSIGN_SUBMISSION_STATUS_
|
* Constants on LMS starting with ASSIGN_SUBMISSION_STATUS_
|
||||||
|
|
|
@ -87,9 +87,10 @@ export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPref
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const assign = await AddonModAssign.getAssignment(courseId, module.id, { siteId });
|
const assign = await AddonModAssign.getAssignment(courseId, module.id, { siteId });
|
||||||
// Get intro files and attachments.
|
// Get intro files, attachments and activity files.
|
||||||
let files: CoreWSFile[] = assign.introattachments || [];
|
let files: CoreWSFile[] = assign.introattachments || [];
|
||||||
files = files.concat(this.getIntroFilesFromInstance(module, assign));
|
files = files.concat(this.getIntroFilesFromInstance(module, assign));
|
||||||
|
files = files.concat(assign.activityattachments || []);
|
||||||
|
|
||||||
// Now get the files in the submissions.
|
// Now get the files in the submissions.
|
||||||
const submissionData = await AddonModAssign.getSubmissions(assign.id, { cmId: module.id, siteId });
|
const submissionData = await AddonModAssign.getSubmissions(assign.id, { cmId: module.id, siteId });
|
||||||
|
|
|
@ -90,8 +90,8 @@ export class AddonModH5PActivityProvider {
|
||||||
formattedAttempt.durationReadable = '-';
|
formattedAttempt.durationReadable = '-';
|
||||||
formattedAttempt.durationCompact = '-';
|
formattedAttempt.durationCompact = '-';
|
||||||
} else {
|
} else {
|
||||||
formattedAttempt.durationReadable = CoreTimeUtils.formatTime(attempt.duration);
|
formattedAttempt.durationReadable = CoreTimeUtils.formatTime(attempt.duration, 3);
|
||||||
formattedAttempt.durationCompact = CoreTimeUtils.formatDurationShort(attempt.duration);
|
formattedAttempt.durationCompact = CoreTimeUtils.formatTimeShort(attempt.duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
return formattedAttempt;
|
return formattedAttempt;
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
<div *ngIf="lesson" [ngClass]='{"addon-mod_lesson-slideshow": lesson.slideshow}'
|
<div *ngIf="lesson" [ngClass]='{"addon-mod_lesson-slideshow": lesson.slideshow}'
|
||||||
[ngStyle]="{'width': lessonWidth, 'height': lessonHeight}">
|
[ngStyle]="{'width': lessonWidth, 'height': lessonHeight}">
|
||||||
|
|
||||||
<core-timer *ngIf="endTime" [endTime]="endTime" (finished)="timeUp()"
|
<core-timer *ngIf="endTime" [endTime]="endTime" (finished)="timeUp()" [timeLeftClassThreshold]="-1"
|
||||||
[timerText]="'addon.mod_lesson.timeremaining' | translate">
|
[timerText]="'addon.mod_lesson.timeremaining' | translate">
|
||||||
</core-timer>
|
</core-timer>
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
@import "~theme/globals";
|
||||||
|
|
||||||
|
$quiz-timer-warn-color: $red !default;
|
||||||
|
$quiz-timer-iterations: 15 !default;
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
.addon-mod_quiz-question-note p {
|
.addon-mod_quiz-question-note p {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
@ -7,4 +12,19 @@
|
||||||
ion-content ion-toolbar {
|
ion-content ion-toolbar {
|
||||||
border-bottom: 1px solid var(--stroke);
|
border-bottom: 1px solid var(--stroke);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
core-timer .core-timer {
|
||||||
|
// Make the timer go red when it's reaching 0.
|
||||||
|
@for $i from 0 through $quiz-timer-iterations {
|
||||||
|
&.core-timer-timeleft-#{$i} {
|
||||||
|
background-color: rgba($quiz-timer-warn-color, 1 - ($i / $quiz-timer-iterations)) !important;
|
||||||
|
|
||||||
|
@if $i <= $quiz-timer-iterations / 2 {
|
||||||
|
label, span, ion-icon {
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,21 @@
|
||||||
<ion-item class="core-timer" role="timer" [ngClass]="{'ion-text-center': align == 'center', 'ion-text-end': align == 'right'}">
|
<ion-item *ngIf="mode !== modeBasic" class="core-timer" role="timer"
|
||||||
|
[ngClass]="{'ion-text-center': align == 'center', 'ion-text-end': align == 'right'}">
|
||||||
<ion-icon name="fas-clock" slot="start" aria-hidden="true"></ion-icon>
|
<ion-icon name="fas-clock" slot="start" aria-hidden="true"></ion-icon>
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<span *ngIf="timeLeft && timeLeft > 0 && timerText" class="core-timer-text">{{ timerText }}</span>
|
<ng-container *ngTemplateOutlet="timerTemplate"></ng-container>
|
||||||
<span *ngIf="timeLeft && timeLeft > 0" class="core-timer-time-left">{{ timeLeft | coreSecondsToHMS }}</span>
|
|
||||||
<span class="core-timesup" *ngIf="timeLeft !== undefined && timeLeft <= 0">
|
|
||||||
{{ 'core.timesup' | translate }}
|
|
||||||
</span>
|
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
|
<div *ngIf="mode === modeBasic" class="core-timer ion-padding" role="timer"
|
||||||
|
[ngClass]="{'ion-text-center': align == 'center', 'ion-text-end': align == 'right'}">
|
||||||
|
<ng-container *ngTemplateOutlet="timerTemplate"></ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template #timerTemplate>
|
||||||
|
<span *ngIf="timerText" class="core-timer-text">{{ timerText }}</span>
|
||||||
|
<span *ngIf="timeLeft && timeLeft > 0" class="core-timer-time-left">{{ timeLeft | coreSecondsToHMS }}</span>
|
||||||
|
<span class="core-timesup" *ngIf="timeLeft !== undefined && timeLeft <= 0">
|
||||||
|
<ng-container *ngIf="timeUpText">{{ timeUpText }}</ng-container>
|
||||||
|
<ng-container *ngIf="!timeUpText">{{ 'core.timesup' | translate }}</ng-container>
|
||||||
|
</span>
|
||||||
|
</ng-template>
|
||||||
|
|
|
@ -1,8 +1,3 @@
|
||||||
@import "~theme/globals";
|
|
||||||
|
|
||||||
$core-timer-warn-color: $red !default;
|
|
||||||
$core-timer-iterations: 15 !default;
|
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
.core-timer {
|
.core-timer {
|
||||||
--background: transparent !important;
|
--background: transparent !important;
|
||||||
|
@ -15,18 +10,5 @@ $core-timer-iterations: 15 !default;
|
||||||
span {
|
span {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the timer warning colors.
|
|
||||||
@for $i from 0 through $core-timer-iterations {
|
|
||||||
&.core-timer-timeleft-#{$i} {
|
|
||||||
background-color: rgba($core-timer-warn-color, 1 - ($i / $core-timer-iterations)) !important;
|
|
||||||
|
|
||||||
@if $i <= $core-timer-iterations / 2 {
|
|
||||||
label, span, ion-icon {
|
|
||||||
color: var(--white);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,10 +32,15 @@ export class CoreTimerComponent implements OnInit, OnDestroy {
|
||||||
@Input() endTime?: string | number; // Timestamp (in seconds) when the timer should end.
|
@Input() endTime?: string | number; // Timestamp (in seconds) when the timer should end.
|
||||||
@Input() timerText?: string; // Text to show next to the timer. If not defined, no text shown.
|
@Input() timerText?: string; // Text to show next to the timer. If not defined, no text shown.
|
||||||
@Input() timeLeftClass?: string; // Name of the class to apply with each second. By default, 'core-timer-timeleft-'.
|
@Input() timeLeftClass?: string; // Name of the class to apply with each second. By default, 'core-timer-timeleft-'.
|
||||||
|
@Input() timeLeftClassThreshold = 100; // Number of seconds to start adding the timeLeftClass. Set it to -1 to not add it.
|
||||||
@Input() align?: string; // Where to align the time and text. Defaults to 'left'. Other values: 'center', 'right'.
|
@Input() align?: string; // Where to align the time and text. Defaults to 'left'. Other values: 'center', 'right'.
|
||||||
|
@Input() timeUpText?: string; // Text to show when the timer reaches 0. If not defined, 'core.timesup'.
|
||||||
|
@Input() mode: CoreTimerMode = CoreTimerMode.ITEM; // How to display data.
|
||||||
|
@Input() underTimeClassThresholds = []; // Number of seconds to add the class 'core-timer-under-'.
|
||||||
@Output() finished = new EventEmitter<void>(); // Will emit an event when the timer reaches 0.
|
@Output() finished = new EventEmitter<void>(); // Will emit an event when the timer reaches 0.
|
||||||
|
|
||||||
timeLeft?: number; // Seconds left to end.
|
timeLeft?: number; // Seconds left to end.
|
||||||
|
modeBasic = CoreTimerMode.BASIC;
|
||||||
|
|
||||||
protected timeInterval?: number;
|
protected timeInterval?: number;
|
||||||
protected element?: HTMLElement;
|
protected element?: HTMLElement;
|
||||||
|
@ -50,31 +55,51 @@ export class CoreTimerComponent implements OnInit, OnDestroy {
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
const timeLeftClass = this.timeLeftClass || 'core-timer-timeleft-';
|
const timeLeftClass = this.timeLeftClass || 'core-timer-timeleft-';
|
||||||
const endTime = Math.round(Number(this.endTime));
|
const endTime = Math.round(Number(this.endTime));
|
||||||
const container: HTMLElement | undefined = this.elementRef.nativeElement.querySelector('.core-timer');
|
this.underTimeClassThresholds.sort((a, b) => a - b); // Sort by increase order.
|
||||||
|
|
||||||
if (!endTime) {
|
if (!endTime) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let container: HTMLElement | undefined;
|
||||||
|
|
||||||
// Check time left every 200ms.
|
// Check time left every 200ms.
|
||||||
this.timeInterval = window.setInterval(() => {
|
this.timeInterval = window.setInterval(() => {
|
||||||
this.timeLeft = endTime - CoreTimeUtils.timestamp();
|
container = container || this.elementRef.nativeElement.querySelector('.core-timer');
|
||||||
|
this.timeLeft = Math.max(endTime - CoreTimeUtils.timestamp(), 0);
|
||||||
|
|
||||||
if (this.timeLeft < 0) {
|
if (container) {
|
||||||
|
// Add class if timer is below timeLeftClassThreshold.
|
||||||
|
if (this.timeLeft < this.timeLeftClassThreshold && !container.classList.contains(timeLeftClass + this.timeLeft)) {
|
||||||
|
// Time left has changed. Remove previous classes and add the new one.
|
||||||
|
container.classList.remove(timeLeftClass + (this.timeLeft + 1));
|
||||||
|
container.classList.remove(timeLeftClass + (this.timeLeft + 2));
|
||||||
|
container.classList.add(timeLeftClass + this.timeLeft);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add classes for underTimeClassThresholds.
|
||||||
|
for (let i = 0; i < this.underTimeClassThresholds.length; i++) {
|
||||||
|
const threshold = this.underTimeClassThresholds[i];
|
||||||
|
if (this.timeLeft <= threshold) {
|
||||||
|
if (!container.classList.contains('core-timer-under-' + this.timeLeft)) {
|
||||||
|
// Add new class and remove the previous one.
|
||||||
|
const nextTreshold = this.underTimeClassThresholds[i + 1];
|
||||||
|
container.classList.add('core-timer-under-' + threshold);
|
||||||
|
nextTreshold && container.classList.remove('core-timer-under-' + nextTreshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.timeLeft === 0) {
|
||||||
// Time is up! Stop the timer and call the finish function.
|
// Time is up! Stop the timer and call the finish function.
|
||||||
clearInterval(this.timeInterval);
|
clearInterval(this.timeInterval);
|
||||||
this.finished.emit();
|
this.finished.emit();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the time has nearly expired, change the color.
|
|
||||||
if (this.timeLeft < 100 && container && !container.classList.contains(timeLeftClass + this.timeLeft)) {
|
|
||||||
// Time left has changed. Remove previous classes and add the new one.
|
|
||||||
container.classList.remove(timeLeftClass + (this.timeLeft + 1));
|
|
||||||
container.classList.remove(timeLeftClass + (this.timeLeft + 2));
|
|
||||||
container.classList.add(timeLeftClass + this.timeLeft);
|
|
||||||
}
|
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,3 +111,8 @@ export class CoreTimerComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CoreTimerMode {
|
||||||
|
ITEM = 'item',
|
||||||
|
BASIC = 'basic',
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { CoreLang } from '@services/lang';
|
||||||
import { CoreLocalNotifications } from '@services/local-notifications';
|
import { CoreLocalNotifications } from '@services/local-notifications';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreUpdateManager } from '@services/update-manager';
|
import { CoreUpdateManager } from '@services/update-manager';
|
||||||
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
|
|
||||||
export default async function(): Promise<void> {
|
export default async function(): Promise<void> {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
@ -27,5 +28,6 @@ export default async function(): Promise<void> {
|
||||||
CoreLang.initialize(),
|
CoreLang.initialize(),
|
||||||
CoreLocalNotifications.initialize(),
|
CoreLocalNotifications.initialize(),
|
||||||
CoreUpdateManager.initialize(),
|
CoreUpdateManager.initialize(),
|
||||||
|
CoreTimeUtils.initialize(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
import { Injectable, SimpleChange, ElementRef, KeyValueChanges } from '@angular/core';
|
import { Injectable, SimpleChange, ElementRef, KeyValueChanges } from '@angular/core';
|
||||||
import { IonContent } from '@ionic/angular';
|
import { IonContent } from '@ionic/angular';
|
||||||
import { ModalOptions, PopoverOptions, AlertOptions, AlertButton, TextFieldTypes, getMode } from '@ionic/core';
|
import { ModalOptions, PopoverOptions, AlertOptions, AlertButton, TextFieldTypes, getMode, ToastOptions } from '@ionic/core';
|
||||||
import { Md5 } from 'ts-md5';
|
import { Md5 } from 'ts-md5';
|
||||||
|
|
||||||
import { CoreApp } from '@services/app';
|
import { CoreApp } from '@services/app';
|
||||||
|
@ -1633,6 +1633,24 @@ export class CoreDomUtilsProvider {
|
||||||
return loader;
|
return loader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show toast with some options.
|
||||||
|
*
|
||||||
|
* @param options Options.
|
||||||
|
* @return Promise resolved with Toast instance.
|
||||||
|
*/
|
||||||
|
async showToastWithOptions(options: ToastOptions): Promise<HTMLIonToastElement> {
|
||||||
|
// Set some default values.
|
||||||
|
options.duration = options.duration ?? 2000;
|
||||||
|
options.position = options.position ?? 'bottom';
|
||||||
|
|
||||||
|
const loader = await ToastController.create(options);
|
||||||
|
|
||||||
|
await loader.present();
|
||||||
|
|
||||||
|
return loader;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores a component/directive instance.
|
* Stores a component/directive instance.
|
||||||
*
|
*
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import moment, { LongDateFormatKey } from 'moment';
|
import moment, { LongDateFormatKey } from 'moment';
|
||||||
import { CoreConstants } from '@/core/constants';
|
|
||||||
import { makeSingleton, Translate } from '@singletons';
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -68,6 +67,20 @@ export class CoreTimeUtilsProvider {
|
||||||
'%%': '%',
|
'%%': '%',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize.
|
||||||
|
*/
|
||||||
|
initialize(): void {
|
||||||
|
// Set relative time thresholds for humanize(), otherwise for example 47 minutes were converted to 'an hour'.
|
||||||
|
moment.relativeTimeThreshold('s', 60);
|
||||||
|
moment.relativeTimeThreshold('m', 60);
|
||||||
|
moment.relativeTimeThreshold('h', 24);
|
||||||
|
moment.relativeTimeThreshold('d', 31);
|
||||||
|
moment.relativeTimeThreshold('M', 12);
|
||||||
|
moment.relativeTimeThreshold('y', 365);
|
||||||
|
moment.relativeTimeThreshold('ss', 0); // To display exact number of seconds instead of just "a few seconds".
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a PHP format to a Moment format.
|
* Convert a PHP format to a Moment format.
|
||||||
*
|
*
|
||||||
|
@ -142,82 +155,16 @@ export class CoreTimeUtilsProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns hours, minutes and seconds in a human readable format
|
* Returns years, months, days, hours, minutes and seconds in a human readable format.
|
||||||
*
|
*
|
||||||
* @param seconds A number of seconds
|
* @param seconds A number of seconds
|
||||||
|
* @param precision Number of elements to have in precision.
|
||||||
* @return Seconds in a human readable format.
|
* @return Seconds in a human readable format.
|
||||||
*/
|
*/
|
||||||
formatTime(seconds: number): string {
|
formatTime(seconds: number, precision = 2): string {
|
||||||
const totalSecs = Math.abs(seconds);
|
precision = precision || 6; // Use max precision if 0 is passed.
|
||||||
const years = Math.floor(totalSecs / CoreConstants.SECONDS_YEAR);
|
|
||||||
let remainder = totalSecs - (years * CoreConstants.SECONDS_YEAR);
|
|
||||||
const days = Math.floor(remainder / CoreConstants.SECONDS_DAY);
|
|
||||||
|
|
||||||
remainder = totalSecs - (days * CoreConstants.SECONDS_DAY);
|
const eventDuration = moment.duration(Math.abs(seconds), 'seconds');
|
||||||
|
|
||||||
const hours = Math.floor(remainder / CoreConstants.SECONDS_HOUR);
|
|
||||||
remainder = remainder - (hours * CoreConstants.SECONDS_HOUR);
|
|
||||||
|
|
||||||
const mins = Math.floor(remainder / CoreConstants.SECONDS_MINUTE);
|
|
||||||
const secs = remainder - (mins * CoreConstants.SECONDS_MINUTE);
|
|
||||||
|
|
||||||
const ss = Translate.instant('core.' + (secs == 1 ? 'sec' : 'secs'));
|
|
||||||
const sm = Translate.instant('core.' + (mins == 1 ? 'min' : 'mins'));
|
|
||||||
const sh = Translate.instant('core.' + (hours == 1 ? 'hour' : 'hours'));
|
|
||||||
const sd = Translate.instant('core.' + (days == 1 ? 'day' : 'days'));
|
|
||||||
const sy = Translate.instant('core.' + (years == 1 ? 'year' : 'years'));
|
|
||||||
let oyears = '';
|
|
||||||
let odays = '';
|
|
||||||
let ohours = '';
|
|
||||||
let omins = '';
|
|
||||||
let osecs = '';
|
|
||||||
|
|
||||||
if (years) {
|
|
||||||
oyears = years + ' ' + sy;
|
|
||||||
}
|
|
||||||
if (days) {
|
|
||||||
odays = days + ' ' + sd;
|
|
||||||
}
|
|
||||||
if (hours) {
|
|
||||||
ohours = hours + ' ' + sh;
|
|
||||||
}
|
|
||||||
if (mins) {
|
|
||||||
omins = mins + ' ' + sm;
|
|
||||||
}
|
|
||||||
if (secs) {
|
|
||||||
osecs = secs + ' ' + ss;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (years) {
|
|
||||||
return oyears + ' ' + odays;
|
|
||||||
}
|
|
||||||
if (days) {
|
|
||||||
return odays + ' ' + ohours;
|
|
||||||
}
|
|
||||||
if (hours) {
|
|
||||||
return ohours + ' ' + omins;
|
|
||||||
}
|
|
||||||
if (mins) {
|
|
||||||
return omins + ' ' + osecs;
|
|
||||||
}
|
|
||||||
if (secs) {
|
|
||||||
return osecs;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Translate.instant('core.now');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns hours, minutes and seconds in a human readable format.
|
|
||||||
*
|
|
||||||
* @param duration Duration in seconds
|
|
||||||
* @param precision Number of elements to have in precision. 0 or undefined to full precission.
|
|
||||||
* @return Duration in a human readable format.
|
|
||||||
*/
|
|
||||||
formatDuration(duration: number, precision?: number): string {
|
|
||||||
precision = precision || 5;
|
|
||||||
|
|
||||||
const eventDuration = moment.duration(duration, 'seconds');
|
|
||||||
let durationString = '';
|
let durationString = '';
|
||||||
|
|
||||||
if (precision && eventDuration.years() > 0) {
|
if (precision && eventDuration.years() > 0) {
|
||||||
|
@ -240,17 +187,21 @@ export class CoreTimeUtilsProvider {
|
||||||
durationString += ' ' + moment.duration(eventDuration.minutes(), 'minutes').humanize();
|
durationString += ' ' + moment.duration(eventDuration.minutes(), 'minutes').humanize();
|
||||||
precision--;
|
precision--;
|
||||||
}
|
}
|
||||||
|
if (precision && (eventDuration.seconds() > 0 || !durationString)) {
|
||||||
|
durationString += ' ' + moment.duration(eventDuration.seconds(), 'seconds').humanize();
|
||||||
|
precision--;
|
||||||
|
}
|
||||||
|
|
||||||
return durationString.trim();
|
return durationString.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns duration in a short human readable format: minutes and seconds, in fromat: 3' 27''.
|
* Converts a number of seconds into a short human readable format: minutes and seconds, in fromat: 3' 27''.
|
||||||
*
|
*
|
||||||
* @param duration Duration in seconds
|
* @param seconds Seconds
|
||||||
* @return Duration in a short human readable format.
|
* @return Short human readable text.
|
||||||
*/
|
*/
|
||||||
formatDurationShort(duration: number): string {
|
formatTimeShort(duration: number): string {
|
||||||
const minutes = Math.floor(duration / 60);
|
const minutes = Math.floor(duration / 60);
|
||||||
const seconds = duration - minutes * 60;
|
const seconds = duration - minutes * 60;
|
||||||
const durations = <string[]>[];
|
const durations = <string[]>[];
|
||||||
|
@ -266,6 +217,29 @@ export class CoreTimeUtilsProvider {
|
||||||
return durations.join(' ');
|
return durations.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns hours, minutes and seconds in a human readable format.
|
||||||
|
*
|
||||||
|
* @param duration Duration in seconds
|
||||||
|
* @param precision Number of elements to have in precision. 0 or undefined to full precission.
|
||||||
|
* @return Duration in a human readable format.
|
||||||
|
* @deprecated since 4.0. Use formatTime instead.
|
||||||
|
*/
|
||||||
|
formatDuration(duration: number, precision?: number): string {
|
||||||
|
return this.formatTime(duration, precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns duration in a short human readable format: minutes and seconds, in fromat: 3' 27''.
|
||||||
|
*
|
||||||
|
* @param duration Duration in seconds
|
||||||
|
* @return Duration in a short human readable format.
|
||||||
|
* @deprecated since 4.0. Use formatTime instead.
|
||||||
|
*/
|
||||||
|
formatDurationShort(duration: number): string {
|
||||||
|
return this.formatTimeShort(duration);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the current timestamp in a "readable" format: YYYYMMDDHHmmSS.
|
* Return the current timestamp in a "readable" format: YYYYMMDDHHmmSS.
|
||||||
*
|
*
|
||||||
|
|
|
@ -504,10 +504,19 @@ ion-toast {
|
||||||
@include media-breakpoint-down(sm) {
|
@include media-breakpoint-down(sm) {
|
||||||
&::part(container) {
|
&::part(container) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@each $color-name, $unused in $colors {
|
||||||
|
ion-toast.core-#{$color-name}-toast {
|
||||||
|
--background: var(--ion-color-#{$color-name}-tint);
|
||||||
|
--color: var(--ion-color-#{$color-name}-shade);
|
||||||
|
--button-color: var(--ion-color-#{$color-name}-shade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ionic list.
|
// Ionic list.
|
||||||
ion-list {
|
ion-list {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
|
|
Loading…
Reference in New Issue