MOBILE-3636 assign: Submission review and edit pages

main
Pau Ferrer Ocaña 2021-02-19 12:41:30 +01:00
parent a4b356350b
commit 5350f715a3
19 changed files with 912 additions and 174 deletions

View File

@ -12,22 +12,61 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { conditionalRoutes } from '@/app/app-routing.module';
import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedModule } from '@/core/shared.module';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { CanLeaveGuard } from '@guards/can-leave';
import { CoreScreen } from '@services/screen';
import { AddonModAssignComponentsModule } from './components/components.module'; import { AddonModAssignComponentsModule } from './components/components.module';
import { AddonModAssignEditPage } from './pages/edit/edit';
import { AddonModAssignIndexPage } from './pages/index/index.page'; import { AddonModAssignIndexPage } from './pages/index/index.page';
import { AddonModAssignSubmissionListPage } from './pages/submission-list/submission-list.page'; import { AddonModAssignSubmissionListPage } from './pages/submission-list/submission-list.page';
import { AddonModAssignSubmissionReviewPage } from './pages/submission-review/submission-review';
const routes: Routes = [ const commonRoutes: Routes = [
{ {
path: ':courseId/:cmId', path: ':courseId/:cmId',
component: AddonModAssignIndexPage, component: AddonModAssignIndexPage,
}, },
{ {
path: ':courseId/:cmId/submission-list', path: ':courseId/:cmId/edit',
component: AddonModAssignEditPage,
canDeactivate: [CanLeaveGuard],
},
];
const mobileRoutes: Routes = [
...commonRoutes,
{
path: ':courseId/:cmId/submission',
component: AddonModAssignSubmissionListPage, component: AddonModAssignSubmissionListPage,
}, },
{
path: ':courseId/:cmId/submission/:submitId',
component: AddonModAssignSubmissionReviewPage,
canDeactivate: [CanLeaveGuard],
},
];
const tabletRoutes: Routes = [
...commonRoutes,
{
path: ':courseId/:cmId/submission',
component: AddonModAssignSubmissionListPage,
children: [
{
path: ':submitId',
component: AddonModAssignSubmissionReviewPage,
canDeactivate: [CanLeaveGuard],
},
],
},
];
const routes: Routes = [
...conditionalRoutes(mobileRoutes, () => CoreScreen.instance.isMobile),
...conditionalRoutes(tabletRoutes, () => CoreScreen.instance.isTablet),
]; ];
@NgModule({ @NgModule({
@ -39,6 +78,8 @@ const routes: Routes = [
declarations: [ declarations: [
AddonModAssignIndexPage, AddonModAssignIndexPage,
AddonModAssignSubmissionListPage, AddonModAssignSubmissionListPage,
AddonModAssignSubmissionReviewPage,
AddonModAssignEditPage,
], ],
}) })
export class AddonModAssignLazyModule {} export class AddonModAssignLazyModule {}

View File

@ -35,7 +35,7 @@
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label> <ion-label>
<core-format-text [text]="description" [component]="component" [componentId]="componentId" maxHeight="120" <core-format-text [text]="description" [component]="component" [componentId]="componentId" maxHeight="120"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId" (click)="expandDescription($event)"> contextLevel="module" [contextInstanceId]="module!.id" [courseId]="courseId" (click)="expandDescription($event)">
</core-format-text> </core-format-text>
</ion-label> </ion-label>
</ion-item> </ion-item>
@ -97,7 +97,7 @@
<!-- Summary of submissions with draft status. --> <!-- Summary of submissions with draft status. -->
<ion-item class="ion-text-wrap" *ngIf="assign.submissiondrafts && summary && summary.submissionsenabled" <ion-item class="ion-text-wrap" *ngIf="assign.submissiondrafts && summary && summary.submissionsenabled"
[detail]="!showNumbers || summary.submissiondraftscount" [detail]="!showNumbers || summary.submissiondraftscount"
(click)="goToSubmissionList(submissionStatusDraft, summary.submissiondraftscount)"> (click)="goToSubmissionList(submissionStatusDraft, !!summary.submissiondraftscount)">
<ion-label><h2>{{ 'addon.mod_assign.numberofdraftsubmissions' | translate }}</h2></ion-label> <ion-label><h2>{{ 'addon.mod_assign.numberofdraftsubmissions' | translate }}</h2></ion-label>
<ion-badge slot="end" *ngIf="showNumbers" color="primary"> <ion-badge slot="end" *ngIf="showNumbers" color="primary">
{{ summary.submissiondraftscount }} {{ summary.submissiondraftscount }}
@ -107,7 +107,7 @@
<!-- Summary of submissions with submitted status. --> <!-- Summary of submissions with submitted status. -->
<ion-item class="ion-text-wrap" *ngIf="summary && summary.submissionsenabled" <ion-item class="ion-text-wrap" *ngIf="summary && summary.submissionsenabled"
[detail]="!showNumbers || summary.submissionssubmittedcount" [detail]="!showNumbers || summary.submissionssubmittedcount"
(click)="goToSubmissionList(submissionStatusSubmitted, summary.submissionssubmittedcount)"> (click)="goToSubmissionList(submissionStatusSubmitted, !!summary.submissionssubmittedcount)">
<ion-label><h2>{{ 'addon.mod_assign.numberofsubmittedassignments' | translate }}</h2></ion-label> <ion-label><h2>{{ 'addon.mod_assign.numberofsubmittedassignments' | translate }}</h2></ion-label>
<ion-badge slot="end" *ngIf="showNumbers" color="primary"> <ion-badge slot="end" *ngIf="showNumbers" color="primary">
{{ summary.submissionssubmittedcount }} {{ summary.submissionssubmittedcount }}
@ -136,7 +136,7 @@
<!-- If it's a student, display his submission. --> <!-- If it's a student, display his submission. -->
<addon-mod-assign-submission *ngIf="loaded && !canViewAllSubmissions && canViewOwnSubmission" [courseId]="courseId" <addon-mod-assign-submission *ngIf="loaded && !canViewAllSubmissions && canViewOwnSubmission" [courseId]="courseId"
[moduleId]="module.id"> [moduleId]="module!.id">
</addon-mod-assign-submission> </addon-mod-assign-submission>
</core-loading> </core-loading>

View File

@ -34,6 +34,7 @@ import {
AddonModAssignGradedEventData, AddonModAssignGradedEventData,
AddonModAssignProvider, AddonModAssignProvider,
AddonModAssignSubmissionGradingSummary, AddonModAssignSubmissionGradingSummary,
AddonModAssignSubmissionSavedEventData,
AddonModAssignSubmittedForGradingEventData, AddonModAssignSubmittedForGradingEventData,
} from '../../services/assign'; } from '../../services/assign';
import { AddonModAssignOffline } from '../../services/assign-offline'; import { AddonModAssignOffline } from '../../services/assign-offline';
@ -106,12 +107,16 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
this.currentSite = CoreSites.instance.getCurrentSite(); this.currentSite = CoreSites.instance.getCurrentSite();
// Listen to events. // Listen to events.
this.savedObserver = CoreEvents.on<any>(AddonModAssignProvider.SUBMISSION_SAVED_EVENT, (data) => { this.savedObserver = CoreEvents.on<AddonModAssignSubmissionSavedEventData>(
AddonModAssignProvider.SUBMISSION_SAVED_EVENT,
(data) => {
if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) { if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) {
// Assignment submission saved, refresh data. // Assignment submission saved, refresh data.
this.showLoadingAndRefresh(true, false); this.showLoadingAndRefresh(true, false);
} }
}, this.siteId); },
this.siteId,
);
this.submittedObserver = CoreEvents.on<AddonModAssignSubmittedForGradingEventData>( this.submittedObserver = CoreEvents.on<AddonModAssignSubmittedForGradingEventData>(
AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT,
@ -262,7 +267,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
* @param groupId Group ID. * @param groupId Group ID.
* @return Resolved when done. * @return Resolved when done.
*/ */
async setGroup(groupId: number): Promise<void> { async setGroup(groupId = 0): Promise<void> {
this.group = groupId; this.group = groupId;
const submissionStatus = await AddonModAssign.instance.getSubmissionStatus(this.assign!.id, { const submissionStatus = await AddonModAssign.instance.getSubmissionStatus(this.assign!.id, {
@ -303,10 +308,10 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
* Go to view a list of submissions. * Go to view a list of submissions.
* *
* @param status Status to see. * @param status Status to see.
* @param count Number of submissions with the status. * @param hasSubmissions If the status has any submission.
*/ */
goToSubmissionList(status: string, count: number): void { goToSubmissionList(status?: string, hasSubmissions = false): void {
if (typeof status != 'undefined' && !count && this.showNumbers) { if (typeof status != 'undefined' && !hasSubmissions && this.showNumbers) {
return; return;
} }

View File

@ -1,10 +1,10 @@
<core-loading [hideUntil]="loaded" class="core-loading-center"> <core-loading [hideUntil]="loaded" class="core-loading-center">
<!-- 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" [title]="user.fullname"> <ion-item class="ion-text-wrap" *ngIf="!blindMarking && user" core-user-link [userId]="submitId" [courseId]="courseId" [title]="user!.fullname">
<core-user-avatar [user]="user" slot="start"></core-user-avatar> <core-user-avatar [user]="user" slot="start"></core-user-avatar>
<ion-label> <ion-label>
<h2>{{ user.fullname }}</h2> <h2>{{ user!.fullname }}</h2>
<ng-container *ngTemplateOutlet="submissionStatus"></ng-container> <ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
</ion-label> </ion-label>
<ng-container *ngTemplateOutlet="submissionStatusBadges"></ng-container> <ng-container *ngTemplateOutlet="submissionStatusBadges"></ng-container>
@ -39,10 +39,10 @@
<!-- 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="userSubmission && userSubmission.status != statusNew && userSubmission.timemodified"> *ngIf="userSubmission && userSubmission!.status != statusNew && userSubmission!.timemodified">
<ion-label> <ion-label>
<h2>{{ 'addon.mod_assign.timemodified' | translate }}</h2> <h2>{{ 'addon.mod_assign.timemodified' | translate }}</h2>
<p>{{ userSubmission.timemodified * 1000 | coreFormatDate }}</p> <p>{{ userSubmission!.timemodified * 1000 | coreFormatDate }}</p>
</ion-label> </ion-label>
</ion-item> </ion-item>
@ -55,49 +55,49 @@
<ion-item class="ion-text-wrap" *ngIf="fromDate && !isSubmittedForGrading"> <ion-item class="ion-text-wrap" *ngIf="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" <p *ngIf="!assign!.intro"
[innerHTML]="'addon.mod_assign.allowsubmissionsanddescriptionfromdatesummary' | translate: [innerHTML]="'addon.mod_assign.allowsubmissionsanddescriptionfromdatesummary' | translate:
{'$a': fromDate}"> {'$a': fromDate}">
</p> </p>
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item class="ion-text-wrap" *ngIf="assign.duedate && !isSubmittedForGrading"> <ion-item class="ion-text-wrap" *ngIf="assign!.duedate && !isSubmittedForGrading">
<ion-label> <ion-label>
<h2>{{ 'addon.mod_assign.duedate' | translate }}</h2> <h2>{{ 'addon.mod_assign.duedate' | translate }}</h2>
<p *ngIf="assign.duedate" >{{ assign.duedate * 1000 | coreFormatDate }}</p> <p *ngIf="assign!.duedate" >{{ assign!.duedate * 1000 | coreFormatDate }}</p>
<p *ngIf="!assign.duedate" >{{ 'addon.mod_assign.duedateno' | translate }}</p> <p *ngIf="!assign!.duedate" >{{ 'addon.mod_assign.duedateno' | translate }}</p>
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item class="ion-text-wrap" *ngIf="assign.duedate && assign.cutoffdate && isSubmittedForGrading"> <ion-item class="ion-text-wrap" *ngIf="assign!.duedate && assign!.cutoffdate && isSubmittedForGrading">
<ion-label> <ion-label>
<h2>{{ 'addon.mod_assign.cutoffdate' | translate }}</h2> <h2>{{ 'addon.mod_assign.cutoffdate' | translate }}</h2>
<p>{{ assign.cutoffdate * 1000 | coreFormatDate }}</p> <p>{{ assign!.cutoffdate * 1000 | coreFormatDate }}</p>
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item class="ion-text-wrap" <ion-item class="ion-text-wrap"
*ngIf="assign.duedate && lastAttempt && lastAttempt.extensionduedate && !isSubmittedForGrading"> *ngIf="assign!.duedate && lastAttempt?.extensionduedate && !isSubmittedForGrading">
<ion-label> <ion-label>
<h2>{{ 'addon.mod_assign.extensionduedate' | translate }}</h2> <h2>{{ 'addon.mod_assign.extensionduedate' | translate }}</h2>
<p>{{ lastAttempt.extensionduedate * 1000 | coreFormatDate }}</p> <p>{{ lastAttempt!.extensionduedate * 1000 | coreFormatDate }}</p>
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item class="ion-text-wrap" *ngIf="currentAttempt && !isGrading"> <ion-item class="ion-text-wrap" *ngIf="currentAttempt && !isGrading">
<ion-label> <ion-label>
<h2>{{ 'addon.mod_assign.attemptnumber' | translate }}</h2> <h2>{{ 'addon.mod_assign.attemptnumber' | translate }}</h2>
<p *ngIf="assign.maxattempts == unlimitedAttempts"> <p *ngIf="assign!.maxattempts == unlimitedAttempts">
{{ 'addon.mod_assign.outof' | translate : {{ 'addon.mod_assign.outof' | translate :
{'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }} {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }}
</p> </p>
<p *ngIf="assign.maxattempts != unlimitedAttempts"> <p *ngIf="assign!.maxattempts != unlimitedAttempts">
{{ 'addon.mod_assign.outof' | translate : {{ 'addon.mod_assign.outof' | translate :
{'$a': {'current': currentAttempt, 'total': assign.maxattempts} } }} {'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }}
</p> </p>
</ion-label> </ion-label>
</ion-item> </ion-item>
@ -114,12 +114,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" color="primary" <ion-button expand="block" class="ion-text-wrap" color="primary"
*ngIf="!hasOffline && *ngIf="!hasOffline &&
(!userSubmission || !userSubmission.status || userSubmission.status == statusNew)" (!userSubmission || !userSubmission!.status || userSubmission!.status == statusNew)"
(click)="goToEdit()"> (click)="goToEdit()">
{{ 'addon.mod_assign.addsubmission' | translate }} {{ 'addon.mod_assign.addsubmission' | translate }}
</ion-button> </ion-button>
<!-- If reopened, show addfromprevious and addnewattempt. --> <!-- If reopened, show addfromprevious and addnewattempt. -->
<ng-container *ngIf="!hasOffline && userSubmission && userSubmission.status == statusReopened"> <ng-container *ngIf="!hasOffline && userSubmission?.status == statusReopened">
<ion-button *ngIf="!isPreviousAttemptEmpty" expand="block" class="ion-text-wrap" color="primary" <ion-button *ngIf="!isPreviousAttemptEmpty" expand="block" class="ion-text-wrap" color="primary"
(click)="copyPrevious()"> (click)="copyPrevious()">
{{ 'addon.mod_assign.addnewattemptfromprevious' | translate }} {{ 'addon.mod_assign.addnewattemptfromprevious' | translate }}
@ -130,9 +130,9 @@
</ng-container> </ng-container>
<!-- Else show editsubmission. --> <!-- Else show editsubmission. -->
<ion-button expand="block" class="ion-text-wrap" color="primary" <ion-button expand="block" class="ion-text-wrap" color="primary"
*ngIf="!hasOffline && userSubmission && userSubmission.status && *ngIf="!hasOffline && userSubmission && 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>
</div> </div>
@ -176,9 +176,10 @@
</ng-container> </ng-container>
<!-- Team members that need to submit it too. --> <!-- Team members that need to submit it too. -->
<ion-item class="ion-text-wrap" *ngIf="membersToSubmit && membersToSubmit.length > 0"> <ion-item-divider class="ion-text-wrap" *ngIf="membersToSubmit && membersToSubmit.length > 0">
<ion-label><h2>{{ 'addon.mod_assign.userswhoneedtosubmit' | translate: {$a: ''} }}</h2></ion-label> <ion-label><h2>{{ 'addon.mod_assign.userswhoneedtosubmit' | translate: {$a: ''} }}</h2></ion-label>
<ng-container *ngIf="!blindMarking"> </ion-item-divider>
<ng-container *ngIf="membersToSubmit && membersToSubmit.length > 0 && !blindMarking">
<ng-container *ngFor="let user of membersToSubmit"> <ng-container *ngFor="let user of membersToSubmit">
<ion-item class="ion-text-wrap" core-user-link [userId]="user.id" <ion-item class="ion-text-wrap" core-user-link [userId]="user.id"
[courseId]="courseId" [title]="user.fullname"> [courseId]="courseId" [title]="user.fullname">
@ -187,14 +188,13 @@
</ion-item> </ion-item>
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-container *ngIf="blindMarking"> <ng-container *ngIf="membersToSubmit && membersToSubmit.length > 0 && blindMarking">
<ng-container *ngFor="let blindId of membersToSubmitBlind"> <ng-container *ngFor="let blindId of membersToSubmitBlind">
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label>{{ 'addon.mod_assign.hiddenuser' | translate }} {{ blindId }}</ion-label> <ion-label>{{ 'addon.mod_assign.hiddenuser' | translate }} {{ blindId }}</ion-label>
</ion-item> </ion-item>
</ng-container> </ng-container>
</ng-container> </ng-container>
</ion-item>
<!-- Submission is locked. --> <!-- Submission is locked. -->
<ion-item class="ion-text-wrap" *ngIf="lastAttempt?.locked"> <ion-item class="ion-text-wrap" *ngIf="lastAttempt?.locked">
@ -219,12 +219,12 @@
<ng-template> <ng-template>
<!-- Current grade if method is advanced. --> <!-- Current grade if method is advanced. -->
<ion-item class="ion-text-wrap core-grading-summary" <ion-item class="ion-text-wrap core-grading-summary"
*ngIf="feedback.gradefordisplay && (!isGrading || grade.method != 'simple')"> *ngIf="feedback?.gradefordisplay && (!isGrading || grade.method != 'simple')">
<ion-label> <ion-label>
<h2>{{ 'addon.mod_assign.currentgrade' | translate }}</h2> <h2>{{ 'addon.mod_assign.currentgrade' | translate }}</h2>
<p><core-format-text [text]="feedback.gradefordisplay" [filter]="false"></core-format-text></p> <p><core-format-text [text]="feedback!.gradefordisplay" [filter]="false"></core-format-text></p>
</ion-label> </ion-label>
<ion-button slot="end" *ngIf="feedback.advancedgrade" (click)="showAdvancedGrade()"> <ion-button slot="end" *ngIf="feedback!.advancedgrade" (click)="showAdvancedGrade()">
<ion-icon name="fas-search" slot="icon-only"></ion-icon> <ion-icon name="fas-search" slot="icon-only"></ion-icon>
</ion-button> </ion-button>
</ion-item> </ion-item>
@ -236,7 +236,7 @@
<ion-label position="stacked"> <ion-label position="stacked">
<h2>{{ 'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo!.grade} }}</h2> <h2>{{ 'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo!.grade} }}</h2>
</ion-label> </ion-label>
<ion-input *ngIf="!grade.disabled" type="text" [(ngModel)]="grade.grade" min="0" [max]="gradeInfo.grade" <ion-input *ngIf="!grade.disabled" type="text" [(ngModel)]="grade.grade" min="0" [max]="gradeInfo!.grade"
[lang]="grade.lang"> [lang]="grade.lang">
</ion-input> </ion-input>
<p item-content *ngIf="grade.disabled">{{ 'addon.mod_assign.gradelocked' | translate }}</p> <p item-content *ngIf="grade.disabled">{{ 'addon.mod_assign.gradelocked' | translate }}</p>
@ -256,7 +256,7 @@
<ion-item class="ion-text-wrap" *ngFor="let outcome of gradeInfo!.outcomes"> <ion-item class="ion-text-wrap" *ngFor="let outcome of gradeInfo!.outcomes">
<ion-label><h2>{{ outcome.name }}</h2></ion-label> <ion-label><h2>{{ outcome.name }}</h2></ion-label>
<ion-select *ngIf="canSaveGrades && outcome.itemNumber" [(ngModel)]="outcome.selectedId" <ion-select *ngIf="canSaveGrades && outcome.itemNumber" [(ngModel)]="outcome.selectedId"
interface="action-sheet" [disabled]="gradeInfo.disabled"> interface="action-sheet" [disabled]="gradeInfo!.disabled">
<ion-select-option *ngFor="let grade of outcome.options" [value]="grade.value"> <ion-select-option *ngFor="let grade of outcome.options" [value]="grade.value">
{{grade.label}} {{grade.label}}
</ion-select-option> </ion-select-option>
@ -268,18 +268,18 @@
<ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple'"> <ion-item class="ion-text-wrap" *ngIf="grade.method == 'simple'">
<ion-label> <ion-label>
<h2>{{ 'addon.mod_assign.currentgrade' | translate }}</h2> <h2>{{ 'addon.mod_assign.currentgrade' | translate }}</h2>
<p *ngIf="grade.gradebookGrade !== false && grade.gradebookGrade !== null && !grade.scale"> <p *ngIf="grade.gradebookGrade && !grade.scale">
{{ grade.gradebookGrade }} {{ grade.gradebookGrade }}
</p> </p>
<p *ngIf="grade.gradebookGrade !== false && grade.gradebookGrade !== null && grade.scale"> <p *ngIf="grade.gradebookGrade && grade.scale">
{{ grade.scale[grade.gradebookGrade].label }} {{ grade.scale[grade.gradebookGrade].label }}
</p> </p>
<p *ngIf="grade.gradebookGrade === false || grade.gradebookGrade === null">-</p> <p *ngIf="!grade.gradebookGrade">-</p>
</ion-label> </ion-label>
</ion-item> </ion-item>
</ng-container> </ng-container>
<addon-mod-assign-feedback-plugin *ngFor="let plugin of feedback.plugins" [assign]="assign" <addon-mod-assign-feedback-plugin *ngFor="let plugin of feedback!.plugins" [assign]="assign"
[submission]="userSubmission" [userId]="submitId" [plugin]="plugin" [canEdit]="canSaveGrades"> [submission]="userSubmission" [userId]="submitId" [plugin]="plugin" [canEdit]="canSaveGrades">
</addon-mod-assign-feedback-plugin> </addon-mod-assign-feedback-plugin>
@ -292,28 +292,30 @@
</ion-item> </ion-item>
<!--- Apply grade to all team members. --> <!--- Apply grade to all team members. -->
<ion-item class="ion-text-wrap" *ngIf="assign.teamsubmission && canSaveGrades"> <ion-item class="ion-text-wrap" *ngIf="assign!.teamsubmission && canSaveGrades">
<ion-label>
<h2>{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}</h2> <h2>{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}</h2>
<ion-label>{{ 'addon.mod_assign.applytoteam' | translate }}</ion-label> <p>{{ 'addon.mod_assign.applytoteam' | translate }}</p>
</ion-label>
<ion-toggle [(ngModel)]="grade.applyToAll"></ion-toggle> <ion-toggle [(ngModel)]="grade.applyToAll"></ion-toggle>
</ion-item> </ion-item>
<!-- Attempt status. --> <!-- Attempt status. -->
<ng-container *ngIf="isGrading && assign.attemptreopenmethod != attemptReopenMethodNone"> <ng-container *ngIf="isGrading && assign!.attemptreopenmethod != attemptReopenMethodNone">
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label> <ion-label>
<h2>{{ 'addon.mod_assign.attemptsettings' | translate }}</h2> <h2>{{ 'addon.mod_assign.attemptsettings' | translate }}</h2>
<p *ngIf="assign.maxattempts == unlimitedAttempts"> <p *ngIf="assign!.maxattempts == unlimitedAttempts">
{{ 'addon.mod_assign.outof' | translate : {{ 'addon.mod_assign.outof' | translate :
{'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }} {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }}
</p> </p>
<p *ngIf="assign.maxattempts != unlimitedAttempts"> <p *ngIf="assign!.maxattempts != unlimitedAttempts">
{{ 'addon.mod_assign.outof' | translate : {{ 'addon.mod_assign.outof' | translate :
{'$a': {'current': currentAttempt, 'total': assign.maxattempts} } }} {'$a': {'current': currentAttempt, 'total': assign!.maxattempts} } }}
</p> </p>
<p> <p>
{{ 'addon.mod_assign.attemptreopenmethod' | translate }}: {{ 'addon.mod_assign.attemptreopenmethod' | translate }}:
{{ 'addon.mod_assign.attemptreopenmethod_' + assign.attemptreopenmethod | translate }} {{ 'addon.mod_assign.attemptreopenmethod_' + assign!.attemptreopenmethod | translate }}
</p> </p>
</ion-label> </ion-label>
</ion-item> </ion-item>
@ -324,33 +326,37 @@
</ng-container> </ng-container>
<!-- Data about the grader (teacher who graded). --> <!-- Data about the grader (teacher who graded). -->
<ion-item class="ion-text-wrap" *ngIf="grader" core-user-link [userId]="grader.id" [courseId]="courseId" <ion-item class="ion-text-wrap" *ngIf="grader" core-user-link [userId]="grader!.id" [courseId]="courseId"
[title]="grader.fullname" detail="true"> [title]="grader!.fullname" detail="true">
<core-user-avatar [user]="grader" slot="start"></core-user-avatar> <core-user-avatar [user]="grader" slot="start"></core-user-avatar>
<ion-label> <ion-label>
<h2>{{ 'addon.mod_assign.gradedby' | translate }}</h2> <h2>{{ 'addon.mod_assign.gradedby' | translate }}</h2>
<h2>{{ grader.fullname }}</h2> <h2>{{ grader!.fullname }}</h2>
<p *ngIf="feedback.gradeddate">{{ feedback.gradeddate * 1000 | coreFormatDate }}</p> <p *ngIf="feedback!.gradeddate">{{ feedback!.gradeddate * 1000 | coreFormatDate }}</p>
</ion-label> </ion-label>
</ion-item> </ion-item>
<!-- Grader is hidden, display only the grade date. --> <!-- Grader is hidden, display only the grade date. -->
<ion-item class="ion-text-wrap" *ngIf="!grader && feedback.gradeddate"> <ion-item class="ion-text-wrap" *ngIf="!grader && feedback!.gradeddate">
<ion-label> <ion-label>
<h2>{{ 'addon.mod_assign.gradedon' | translate }}</h2> <h2>{{ 'addon.mod_assign.gradedon' | translate }}</h2>
<p>{{ feedback.gradeddate * 1000 | coreFormatDate }}</p> <p>{{ feedback!.gradeddate * 1000 | coreFormatDate }}</p>
</ion-label> </ion-label>
</ion-item> </ion-item>
<!-- Warning message if cannot save grades. --> <!-- Warning message if cannot save grades. -->
<div *ngIf="isGrading && !canSaveGrades" class="core-warning-card"> <ion-card *ngIf="isGrading && !canSaveGrades" class="core-warning-card">
<ion-item>
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon> <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
<ion-label>
<p>{{ 'addon.mod_assign.cannotgradefromapp' | translate }}</p> <p>{{ 'addon.mod_assign.cannotgradefromapp' | translate }}</p>
<ion-button expand="block" *ngIf="gradeUrl" [href]="gradeUrl" core-link > <ion-button expand="block" *ngIf="gradeUrl" [href]="gradeUrl" core-link >
{{ 'core.openinbrowser' | translate }} {{ 'core.openinbrowser' | translate }}
<ion-icon name="fas-external-link-alt" slot="end"></ion-icon> <ion-icon name="fas-external-link-alt" slot="end"></ion-icon>
</ion-button> </ion-button>
</div> </ion-label>
</ion-item>
</ion-card>
</ng-template> </ng-template>
</core-tab> </core-tab>
</core-tabs> </core-tabs>
@ -360,20 +366,20 @@
<ng-template #submissionStatus> <ng-template #submissionStatus>
<ng-container *ngIf="assign && assign!.teamsubmission && lastAttempt"> <ng-container *ngIf="assign && assign!.teamsubmission && lastAttempt">
<p *ngIf="lastAttempt!.submissiongroup && lastAttempt!.submissiongroupname">{{lastAttempt!.submissiongroupname}}</p> <p *ngIf="lastAttempt!.submissiongroup && lastAttempt!.submissiongroupname">{{lastAttempt!.submissiongroupname}}</p>
<ng-container *ngIf="assign.preventsubmissionnotingroup && <ng-container *ngIf="assign!.preventsubmissionnotingroup &&
!lastAttempt!.submissiongroup && !lastAttempt!.submissiongroup &&
(!lastAttempt!.usergroups || lastAttempt!.usergroups.length <= 0)"> (!lastAttempt!.usergroups || lastAttempt!.usergroups.length <= 0)">
<p class="text-danger"><strong>{{ 'addon.mod_assign.noteam' | translate }}</strong></p> <p class="text-danger"><strong>{{ 'addon.mod_assign.noteam' | translate }}</strong></p>
<p class="text-danger">{{ 'addon.mod_assign.noteam_desc' | translate }}</p> <p class="text-danger">{{ 'addon.mod_assign.noteam_desc' | translate }}</p>
</ng-container> </ng-container>
<ng-container *ngIf="assign.preventsubmissionnotingroup && <ng-container *ngIf="assign!.preventsubmissionnotingroup &&
!lastAttempt!.submissiongroup && !lastAttempt!.submissiongroup &&
lastAttempt!.usergroups && lastAttempt!.usergroups &&
lastAttempt!.usergroups.length > 1"> lastAttempt!.usergroups.length > 1">
<p class="text-danger"><strong>{{ 'addon.mod_assign.multipleteams' | translate }}</strong></p> <p class="text-danger"><strong>{{ 'addon.mod_assign.multipleteams' | translate }}</strong></p>
<p class="text-danger">{{ 'addon.mod_assign.multipleteams_desc' | translate }}</p> <p class="text-danger">{{ 'addon.mod_assign.multipleteams_desc' | translate }}</p>
</ng-container> </ng-container>
<p *ngIf="!assign.preventsubmissionnotingroup && !lastAttempt!.submissiongroup"> <p *ngIf="!assign!.preventsubmissionnotingroup && !lastAttempt!.submissiongroup">
{{ 'addon.mod_assign.defaultteam' | translate }} {{ 'addon.mod_assign.defaultteam' | translate }}
</p> </p>
</ng-container> </ng-container>

View File

@ -55,6 +55,7 @@ import { CoreError } from '@classes/errors/error';
import { CoreGroups } from '@services/groups'; import { CoreGroups } from '@services/groups';
import { CoreSync } from '@services/sync'; import { CoreSync } from '@services/sync';
import { AddonModAssignSubmissionPluginComponent } from '../submission-plugin/submission-plugin'; import { AddonModAssignSubmissionPluginComponent } from '../submission-plugin/submission-plugin';
import { AddonModAssignModuleHandlerService } from '../../services/handlers/module';
/** /**
* Component that displays an assignment submission. * Component that displays an assignment submission.
@ -73,7 +74,6 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
@Input() moduleId!: number; // Module ID the submission belongs to. @Input() moduleId!: number; // Module ID the submission belongs to.
@Input() submitId!: number; // User that did the submission. @Input() submitId!: number; // User that did the submission.
@Input() blindId?: number; // Blinded user ID (if it's blinded). @Input() blindId?: number; // Blinded user ID (if it's blinded).
@Input() showGrade = false; // Whether to display the grade tab at start.
loaded = false; // Whether data has been loaded. loaded = false; // Whether data has been loaded.
selectedTab = 'submission'; // Tab selected on start. selectedTab = 'submission'; // Tab selected on start.
@ -121,6 +121,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
canSaveGrades = false; // Whether the user can save the grades. canSaveGrades = false; // Whether the user can save the grades.
allowAddAttempt = false; // Allow adding a new attempt when grading. allowAddAttempt = false; // Allow adding a new attempt when grading.
gradeUrl?: string; // URL to grade in browser. gradeUrl?: string; // URL to grade in browser.
isPreviousAttemptEmpty = true; // Whether the previous attempt contains an empty submission.
// Some constants. // Some constants.
statusNew = AddonModAssignProvider.SUBMISSION_STATUS_NEW; statusNew = AddonModAssignProvider.SUBMISSION_STATUS_NEW;
@ -131,7 +132,6 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
protected siteId: string; // Current site ID. protected siteId: string; // Current site ID.
protected currentUserId: number; // Current user ID. protected currentUserId: number; // Current user ID.
protected previousAttempt?: AddonModAssignSubmissionPreviousAttempt; // The previous attempt. protected previousAttempt?: AddonModAssignSubmissionPreviousAttempt; // The previous attempt.
protected isPreviousAttemptEmpty = true; // Whether the previous attempt contains an empty submission.
protected submissionStatusAvailable = false; // Whether we were able to retrieve the submission status. protected submissionStatusAvailable = false; // Whether we were able to retrieve the submission status.
protected originalGrades: AddonModAssignSubmissionOriginalGrades = { protected originalGrades: AddonModAssignSubmissionOriginalGrades = {
addAttempt: false, addAttempt: false,
@ -180,7 +180,6 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
* Component being initialized. * Component being initialized.
*/ */
ngOnInit(): void { ngOnInit(): void {
this.selectedTab = this.showGrade ? 'grade' : 'submission';
this.isSubmittedForGrading = !!this.submitId; this.isSubmittedForGrading = !!this.submitId;
this.loadData(true); this.loadData(true);
@ -343,13 +342,14 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
* Go to the page to add or edit submission. * Go to the page to add or edit submission.
*/ */
goToEdit(): void { goToEdit(): void {
CoreNavigator.instance.navigate('AddonModAssignEditPage', { CoreNavigator.instance.navigateToSitePath(
AddonModAssignModuleHandlerService.PAGE_NAME + '/' + this.courseId + '/' + this.moduleId + '/edit',
{
params: { params: {
moduleId: this.moduleId,
courseId: this.courseId,
userId: this.submitId,
blindId: this.blindId, blindId: this.blindId,
} }); },
},
);
} }
/** /**
@ -393,7 +393,7 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
try { try {
return AddonModAssignHelper.instance.hasFeedbackDataChanged( return AddonModAssignHelper.instance.hasFeedbackDataChanged(
this.assign!, this.assign!,
this.userSubmission!, // @todo this.userSubmission,
this.feedback, this.feedback,
this.submitId, this.submitId,
); );

View File

@ -24,11 +24,10 @@
<!-- Edit --> <!-- Edit -->
<ion-item class="ion-text-wrap" *ngIf="edit && loaded"> <ion-item class="ion-text-wrap" *ngIf="edit && loaded">
<ion-label> <ion-label></ion-label>
<core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" <core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name"
name="assignfeedbackcomments_editor" [component]="component" [componentId]="assign.cmid" [autoSave]="true" name="assignfeedbackcomments_editor" [component]="component" [componentId]="assign.cmid" [autoSave]="true"
contextLevel="module" [contextInstanceId]="assign.cmid" elementId="assignfeedbackcomments_editor" contextLevel="module" [contextInstanceId]="assign.cmid" elementId="assignfeedbackcomments_editor"
[draftExtraParams]="{userid: userId, action: 'grade'}"> [draftExtraParams]="{userid: userId, action: 'grade'}">
</core-rich-text-editor> </core-rich-text-editor>
</ion-label>
</ion-item> </ion-item>

View File

@ -0,0 +1,39 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="moduleId" [courseId]="courseId">
</core-format-text>
</ion-title>
<ion-buttons slot="end">
<ion-button fill="clear" (click)="save()" [attr.aria-label]="'core.save' | translate">
{{ 'core.save' | translate }}
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<ion-list *ngIf="userSubmission && userSubmission.plugins && userSubmission.plugins.length">
<!-- @todo: plagiarism_print_disclosure -->
<form name="addon-mod_assign-edit-form" #editSubmissionForm>
<!-- Submission statement. -->
<ion-item class="ion-text-wrap" *ngIf="submissionStatement">
<ion-label>
<core-format-text [text]="submissionStatement" [filter]="false">
</core-format-text>
</ion-label>
<ion-checkbox slot="end" name="submissionstatement" [(ngModel)]="submissionStatementAccepted"></ion-checkbox>
<!-- ion-checkbox doesn't use an input. Create a hidden input to hold the value. -->
<input item-content type="hidden" [ngModel]="submissionStatementAccepted" name="submissionstatement">
</ion-item>
<addon-mod-assign-submission-plugin *ngFor="let plugin of userSubmission.plugins" [assign]="assign"
[submission]="userSubmission" [plugin]="plugin" [edit]="true" [allowOffline]="allowOffline">
</addon-mod-assign-submission-plugin>
</form>
</ion-list>
</core-loading>
</ion-content>

View File

@ -0,0 +1,408 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { CoreError } from '@classes/errors/error';
import { CoreIonLoadingElement } from '@classes/ion-loading';
import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuploader-helper';
import { CoreNavigator } from '@services/navigator';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreDomUtils } from '@services/utils/dom';
import { Translate } from '@singletons';
import { CoreEventActivityDataSentData, CoreEvents } from '@singletons/events';
import {
AddonModAssignAssign,
AddonModAssignSubmission,
AddonModAssignProvider,
AddonModAssign,
AddonModAssignSubmissionStatusOptions,
AddonModAssignGetSubmissionStatusWSResponse,
AddonModAssignSavePluginData,
AddonModAssignSubmissionSavedEventData,
AddonModAssignSubmittedForGradingEventData,
} from '../../services/assign';
import { AddonModAssignHelper } from '../../services/assign-helper';
import { AddonModAssignOffline } from '../../services/assign-offline';
import { AddonModAssignSync } from '../../services/assign-sync';
/**
* Page that allows adding or editing an assigment submission.
*/
@Component({
selector: 'page-addon-mod-assign-edit',
templateUrl: 'edit.html',
})
export class AddonModAssignEditPage implements OnInit, OnDestroy {
@ViewChild('editSubmissionForm') formElement?: ElementRef;
title: string; // Title to display.
assign?: AddonModAssignAssign; // Assignment.
courseId!: number; // Course ID the assignment belongs to.
moduleId!: number; // Module ID the submission belongs to.
userSubmission?: AddonModAssignSubmission; // The user submission.
allowOffline = false; // Whether offline is allowed.
submissionStatement?: string; // The submission statement.
submissionStatementAccepted = false; // Whether submission statement is accepted.
loaded = false; // Whether data has been loaded.
protected userId: number; // User doing the submission.
protected isBlind = false; // Whether blind is used.
protected editText: string; // "Edit submission" translated text.
protected saveOffline = false; // Whether to save data in offline.
protected hasOffline = false; // Whether the assignment has offline data.
protected isDestroyed = false; // Whether the component has been destroyed.
protected forceLeave = false; // To allow leaving the page without checking for changes.
constructor(
protected route: ActivatedRoute,
) {
this.userId = CoreSites.instance.getCurrentSiteUserId(); // Right now we can only edit current user's submissions.
this.editText = Translate.instance.instant('addon.mod_assign.editsubmission');
this.title = this.editText;
}
/**
* Component being initialized.
*/
ngOnInit(): void {
this.moduleId = CoreNavigator.instance.getRouteNumberParam('cmId')!;
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!;
this.isBlind = !!CoreNavigator.instance.getRouteNumberParam('blindId');
this.fetchAssignment().finally(() => {
this.loaded = true;
});
}
/**
* Check if we can leave the page or not.
*
* @return Resolved if we can leave it, rejected if not.
*/
async ionViewCanLeave(): Promise<void> {
if (this.forceLeave) {
return;
}
// Check if data has changed.
const changed = await this.hasDataChanged();
if (changed) {
await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmcanceledit'));
}
// Nothing has changed or user confirmed to leave. Clear temporary data from plugins.
AddonModAssignHelper.instance.clearSubmissionPluginTmpData(this.assign!, this.userSubmission, this.getInputData());
CoreDomUtils.instance.triggerFormCancelledEvent(this.formElement, CoreSites.instance.getCurrentSiteId());
}
/**
* Fetch assignment data.
*
* @return Promise resolved when done.
*/
protected async fetchAssignment(): Promise<void> {
const currentUserId = CoreSites.instance.getCurrentSiteUserId();
try {
// Get assignment data.
this.assign = await AddonModAssign.instance.getAssignment(this.courseId, this.moduleId);
this.title = this.assign.name || this.title;
if (!this.isDestroyed) {
// Block the assignment.
CoreSync.instance.blockOperation(AddonModAssignProvider.COMPONENT, this.assign.id);
}
// Wait for sync to be over (if any).
await AddonModAssignSync.instance.waitForSync(this.assign.id);
// Get submission status. Ignore cache to get the latest data.
const options: AddonModAssignSubmissionStatusOptions = {
userId: this.userId,
isBlind: this.isBlind,
cmId: this.assign.cmid,
filter: false,
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
};
let submissionStatus: AddonModAssignGetSubmissionStatusWSResponse;
try {
submissionStatus = await AddonModAssign.instance.getSubmissionStatus(this.assign.id, options);
this.userSubmission =
AddonModAssign.instance.getSubmissionObjectFromAttempt(this.assign, submissionStatus.lastattempt);
} catch (error) {
// Cannot connect. Get cached data.
options.filter = true;
options.readingStrategy = CoreSitesReadingStrategy.PreferCache;
submissionStatus = await AddonModAssign.instance.getSubmissionStatus(this.assign.id, options);
this.userSubmission =
AddonModAssign.instance.getSubmissionObjectFromAttempt(this.assign, submissionStatus.lastattempt);
// Check if the user can edit it in offline.
const canEditOffline =
await AddonModAssignHelper.instance.canEditSubmissionOffline(this.assign, this.userSubmission);
if (!canEditOffline) {
// Submission cannot be edited in offline, reject.
this.allowOffline = false;
throw error;
}
}
if (!submissionStatus.lastattempt?.canedit) {
// Can't edit. Reject.
throw new CoreError(Translate.instance.instant('core.nopermissions', { $a: this.editText }));
}
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.
if (this.assign.requiresubmissionstatement && !this.assign.submissiondrafts && this.userId == currentUserId) {
this.submissionStatement = this.assign.submissionstatement;
} else {
this.submissionStatement = undefined;
}
try {
// Check if there's any offline data for this submission.
const offlineData = await AddonModAssignOffline.instance.getSubmission(this.assign.id, this.userId);
this.hasOffline = offlineData?.plugindata && Object.keys(offlineData.plugindata).length > 0;
} catch {
// No offline data found.
this.hasOffline = false;
}
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'Error getting assigment data.');
// Leave the player.
this.leaveWithoutCheck();
}
}
/**
* Get the input data.
*
* @return Input data.
*/
protected getInputData(): Record<string, unknown> {
return CoreDomUtils.instance.getDataFromForm(document.forms['addon-mod_assign-edit-form']);
}
/**
* Check if data has changed.
*
* @return Promise resolved with boolean: whether data has changed.
*/
protected hasDataChanged(): Promise<boolean> {
// Usually the hasSubmissionDataChanged call will be resolved inmediately, causing the modal to be shown just an instant.
// We'll wait a bit before showing it to prevent this "blink".
let modal: CoreIonLoadingElement;
let showModal = true;
setTimeout(async () => {
if (showModal) {
modal = await CoreDomUtils.instance.showModalLoading();
}
}, 100);
const data = this.getInputData();
return AddonModAssignHelper.instance.hasSubmissionDataChanged(this.assign!, this.userSubmission, data).finally(() => {
if (modal) {
modal.dismiss();
} else {
showModal = false;
}
});
}
/**
* Leave the view without checking for changes.
*/
protected leaveWithoutCheck(): void {
this.forceLeave = true;
CoreNavigator.instance.back();
}
/**
* Get data to submit based on the input data.
*
* @param inputData The input data.
* @return Promise resolved with the data to submit.
*/
protected prepareSubmissionData(inputData: Record<string, unknown>): Promise<AddonModAssignSavePluginData> {
// If there's offline data, always save it in offline.
this.saveOffline = this.hasOffline;
try {
return AddonModAssignHelper.instance.prepareSubmissionPluginData(
this.assign!,
this.userSubmission,
inputData,
this.hasOffline,
);
} catch (error) {
if (this.allowOffline && !this.saveOffline) {
// Cannot submit in online, prepare for offline usage.
this.saveOffline = true;
return AddonModAssignHelper.instance.prepareSubmissionPluginData(
this.assign!,
this.userSubmission,
inputData,
true,
);
}
throw error;
}
}
/**
* Save the submission.
*/
async save(): Promise<void> {
// Check if data has changed.
const changed = await this.hasDataChanged();
if (!changed) {
// Nothing to save, just go back.
this.leaveWithoutCheck();
return;
}
try {
await this.saveSubmission();
this.leaveWithoutCheck();
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'Error saving submission.');
}
}
/**
* Save the submission.
*
* @return Promise resolved when done.
*/
protected async saveSubmission(): Promise<void> {
const inputData = this.getInputData();
if (this.submissionStatement && (!inputData.submissionstatement || inputData.submissionstatement === 'false')) {
throw Translate.instance.instant('addon.mod_assign.acceptsubmissionstatement');
}
let modal = await CoreDomUtils.instance.showModalLoading();
let size = -1;
// Get size to ask for confirmation.
try {
size = await AddonModAssignHelper.instance.getSubmissionSizeForEdit(this.assign!, this.userSubmission!, inputData);
} catch (error) {
// Error calculating size, return -1.
size = -1;
}
modal.dismiss();
try {
// Confirm action.
await CoreFileUploaderHelper.instance.confirmUploadFile(size, true, this.allowOffline);
modal = await CoreDomUtils.instance.showModalLoading('core.sending', true);
const pluginData = await this.prepareSubmissionData(inputData);
if (!Object.keys(pluginData).length) {
// Nothing to save.
return;
}
let sent: boolean;
if (this.saveOffline) {
// Save submission in offline.
sent = false;
await AddonModAssignOffline.instance.saveSubmission(
this.assign!.id,
this.courseId,
pluginData,
this.userSubmission!.timemodified,
!this.assign!.submissiondrafts,
this.userId,
);
} else {
// Try to send it to server.
sent = await AddonModAssign.instance.saveSubmission(
this.assign!.id,
this.courseId,
pluginData,
this.allowOffline,
this.userSubmission!.timemodified,
!!this.assign!.submissiondrafts,
this.userId,
);
}
// Clear temporary data from plugins.
AddonModAssignHelper.instance.clearSubmissionPluginTmpData(this.assign!, this.userSubmission, inputData);
if (sent) {
CoreEvents.trigger<CoreEventActivityDataSentData>(CoreEvents.ACTIVITY_DATA_SENT, { module: 'assign' });
}
// Submission saved, trigger events.
CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, sent, CoreSites.instance.getCurrentSiteId());
CoreEvents.trigger<AddonModAssignSubmissionSavedEventData>(
AddonModAssignProvider.SUBMISSION_SAVED_EVENT,
{
assignmentId: this.assign!.id,
submissionId: this.userSubmission!.id,
userId: this.userId,
},
CoreSites.instance.getCurrentSiteId(),
);
if (!this.assign!.submissiondrafts) {
// No drafts allowed, so it was submitted. Trigger event.
CoreEvents.trigger<AddonModAssignSubmittedForGradingEventData>(
AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT,
{
assignmentId: this.assign!.id,
submissionId: this.userSubmission!.id,
userId: this.userId,
},
CoreSites.instance.getCurrentSiteId(),
);
}
} finally {
modal.dismiss();
}
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
this.isDestroyed = true;
// Unblock the assignment.
if (this.assign) {
CoreSync.instance.unblockOperation(AddonModAssignProvider.COMPONENT, this.assign.id);
}
}
}

View File

@ -61,7 +61,6 @@
{{ 'addon.mod_assign.defaultteam' | translate }} {{ 'addon.mod_assign.defaultteam' | translate }}
</span> </span>
</p> </p>
</ion-label>
<ion-badge class="ion-text-center ion-text-wrap" [color]="submission.statusColor" <ion-badge class="ion-text-center ion-text-wrap" [color]="submission.statusColor"
*ngIf="submission.statusTranslated"> *ngIf="submission.statusTranslated">
{{ submission.statusTranslated }} {{ submission.statusTranslated }}
@ -70,6 +69,7 @@
*ngIf="submission.gradingStatusTranslationId"> *ngIf="submission.gradingStatusTranslationId">
{{ submission.gradingStatusTranslationId | translate }} {{ submission.gradingStatusTranslationId | translate }}
</ion-badge> </ion-badge>
</ion-label>
</ion-item> </ion-item>
</ng-container> </ng-container>

View File

@ -17,6 +17,7 @@ import { ActivatedRoute } from '@angular/router';
import { IonRefresher } from '@ionic/angular'; import { IonRefresher } from '@ionic/angular';
import { CoreGroupInfo, CoreGroups } from '@services/groups'; import { CoreGroupInfo, CoreGroups } from '@services/groups';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreScreen } from '@services/screen';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
@ -37,6 +38,7 @@ import {
AddonModAssignManualSyncData, AddonModAssignManualSyncData,
AddonModAssignAutoSyncData, AddonModAssignAutoSyncData,
} from '../../services/assign-sync'; } from '../../services/assign-sync';
import { AddonModAssignModuleHandlerService } from '../../services/handlers/module';
/** /**
* Page that displays a list of submissions of an assignment. * Page that displays a list of submissions of an assignment.
@ -137,10 +139,10 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
this.title = Translate.instance.instant('addon.mod_assign.numberofparticipants'); this.title = Translate.instance.instant('addon.mod_assign.numberofparticipants');
} }
this.fetchAssignment(true).finally(() => { this.fetchAssignment(true).finally(() => {
/* if (!this.selectedSubmissionId && this.splitviewCtrl.isOn() && this.submissions.length > 0) { if (!this.selectedSubmissionId && CoreScreen.instance.isTablet && this.submissions.length > 0) {
// Take first and load it. // Take first and load it.
this.loadSubmission(this.submissions[0]); this.loadSubmission(this.submissions[0]);
}*/ }
this.loaded = true; this.loaded = true;
}); });
@ -153,7 +155,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
* @param sync Whether to try to synchronize data. * @param sync Whether to try to synchronize data.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async fetchAssignment(sync?: boolean): Promise<void> { protected async fetchAssignment(sync = false): Promise<void> {
try { try {
// Get assignment data. // Get assignment data.
this.assign = await AddonModAssign.instance.getAssignment(this.courseId, this.moduleId); this.assign = await AddonModAssign.instance.getAssignment(this.courseId, this.moduleId);
@ -310,19 +312,21 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
* @param submission The submission to load. * @param submission The submission to load.
*/ */
loadSubmission(submission: AddonModAssignSubmissionForList): void { loadSubmission(submission: AddonModAssignSubmissionForList): void {
/* if (this.selectedSubmissionId === submission.submitid && this.splitviewCtrl.isOn()) { if (this.selectedSubmissionId === submission.submitid) {
// Already selected. // Already selected.
return; return;
}*/ }
this.selectedSubmissionId = submission.submitid; this.selectedSubmissionId = submission.submitid;
/* this.splitviewCtrl.push('AddonModAssignSubmissionReviewPage', { CoreNavigator.instance.navigateToSitePath(
courseId: this.courseId, AddonModAssignModuleHandlerService.PAGE_NAME+'/'+this.courseId+'/'+this.moduleId+'/submission/'+submission.submitid,
moduleId: this.moduleId, {
submitId: submission.submitid, params: {
blindId: submission.blindid, blindId: submission.blindid,
});*/ },
},
);
} }
/** /**

View File

@ -0,0 +1,29 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="moduleId" [courseId]="courseId">
</core-format-text>
</ion-title>
<ion-buttons slot="end"></ion-buttons>
</ion-toolbar>
<core-navbar-buttons slot="end">
<ion-button [hidden]="!canSaveGrades" fill="clear" (click)="submitGrade()" [attr.aria-label]="'core.done' | translate">
{{ 'core.done' | translate }}
</ion-button>
</core-navbar-buttons>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshSubmission($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<addon-mod-assign-submission [courseId]="courseId" [moduleId]="moduleId" [submitId]="submitId" [blindId]="blindId">
</addon-mod-assign-submission>
</core-loading>
</ion-content>

View File

@ -0,0 +1,184 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { CoreCourse } from '@features/course/services/course';
import { IonRefresher } from '@ionic/angular';
import { CoreNavigator } from '@services/navigator';
import { CoreScreen } from '@services/screen';
import { CoreDomUtils } from '@services/utils/dom';
import { AddonModAssignSubmissionComponent } from '../../components/submission/submission';
import { AddonModAssign, AddonModAssignAssign } from '../../services/assign';
/**
* Page that displays a submission.
*/
@Component({
selector: 'page-addon-mod-assign-submission-review',
templateUrl: 'submission-review.html',
})
export class AddonModAssignSubmissionReviewPage implements OnInit {
@ViewChild(AddonModAssignSubmissionComponent) submissionComponent?: AddonModAssignSubmissionComponent;
title = ''; // Title to display.
moduleId!: number; // Module ID the submission belongs to.
courseId!: number; // Course ID the assignment belongs to.
submitId!: number; // User that did the submission.
blindId?: number; // Blinded user ID (if it's blinded).
loaded = false; // Whether data has been loaded.
canSaveGrades = false; // Whether the user can save grades.
protected assign?: AddonModAssignAssign; // The assignment the submission belongs to.
protected blindMarking = false; // Whether it uses blind marking.
protected forceLeave = false; // To allow leaving the page without checking for changes.
constructor(
protected route: ActivatedRoute,
) { }
/**
* Component being initialized.
*/
ngOnInit(): void {
this.route.queryParams.subscribe((params) => {
this.moduleId = CoreNavigator.instance.getRouteNumberParam('cmId')!;
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId')!;
this.submitId = CoreNavigator.instance.getRouteNumberParam('submitId') || 0;
this.blindId = CoreNavigator.instance.getRouteNumberParam('blindId', params);
this.fetchSubmission().finally(() => {
this.loaded = true;
});
});
}
/**
* Check if we can leave the page or not.
*
* @return Resolved if we can leave it, rejected if not.
*/
ionViewCanLeave(): boolean | Promise<void> {
if (!this.submissionComponent || this.forceLeave) {
return true;
}
// Check if data has changed.
return this.submissionComponent.canLeave();
}
/**
* User entered the page.
*/
ionViewDidEnter(): void {
this.submissionComponent?.ionViewDidEnter();
}
/**
* User left the page.
*/
ionViewDidLeave(): void {
this.submissionComponent?.ionViewDidLeave();
}
/**
* Get the submission.
*
* @return Promise resolved when done.
*/
protected async fetchSubmission(): Promise<void> {
this.assign = await AddonModAssign.instance.getAssignment(this.courseId, this.moduleId);
this.title = this.assign.name;
this.blindMarking = !!this.assign.blindmarking && !this.assign.revealidentities;
const gradeInfo = await CoreCourse.instance.getModuleBasicGradeInfo(this.moduleId);
if (!gradeInfo) {
return;
}
// Grades can be saved if simple grading.
if (gradeInfo.advancedgrading && gradeInfo.advancedgrading[0] &&
typeof gradeInfo.advancedgrading[0].method != 'undefined') {
const method = gradeInfo.advancedgrading[0].method || 'simple';
this.canSaveGrades = method == 'simple';
} else {
this.canSaveGrades = true;
}
}
/**
* Refresh all the data.
*
* @return Promise resolved when done.
*/
protected async refreshAllData(): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(AddonModAssign.instance.invalidateAssignmentData(this.courseId));
if (this.assign) {
promises.push(AddonModAssign.instance.invalidateSubmissionData(this.assign.id));
promises.push(AddonModAssign.instance.invalidateAssignmentUserMappingsData(this.assign.id));
promises.push(AddonModAssign.instance.invalidateSubmissionStatusData(
this.assign.id,
this.submitId,
undefined,
this.blindMarking,
));
}
try {
await Promise.all(promises);
} finally {
this.submissionComponent && this.submissionComponent.invalidateAndRefresh(true);
await this.fetchSubmission();
}
}
/**
* Refresh the data.
*
* @param refresher Refresher.
*/
refreshSubmission(refresher?: CustomEvent<IonRefresher>): void {
this.refreshAllData().finally(() => {
refresher?.detail.complete();
});
}
/**
* Submit a grade and feedback.
*/
async submitGrade(): Promise<void> {
if (!this.submissionComponent) {
return;
}
try {
await this.submissionComponent.submitGrade();
// Grade submitted, leave the view if not in tablet.
if (!CoreScreen.instance.isTablet) {
this.forceLeave = true;
CoreNavigator.instance.back();
}
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.error', true);
}
}
}

View File

@ -49,7 +49,7 @@ export class AddonModAssignHelperProvider {
* @param submission Submission. * @param submission Submission.
* @return Whether it can be edited offline. * @return Whether it can be edited offline.
*/ */
async canEditSubmissionOffline(assign: AddonModAssignAssign, submission: AddonModAssignSubmission): Promise<boolean> { async canEditSubmissionOffline(assign: AddonModAssignAssign, submission?: AddonModAssignSubmission): Promise<boolean> {
if (!submission) { if (!submission) {
return false; return false;
} }
@ -85,7 +85,15 @@ export class AddonModAssignHelperProvider {
* @param submission Submission to clear the data for. * @param submission Submission to clear the data for.
* @param inputData Data entered in the submission form. * @param inputData Data entered in the submission form.
*/ */
clearSubmissionPluginTmpData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, inputData: any): void { clearSubmissionPluginTmpData(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission | undefined,
inputData: Record<string, unknown>,
): void {
if (!submission) {
return;
}
submission.plugins?.forEach((plugin) => { submission.plugins?.forEach((plugin) => {
AddonModAssignSubmissionDelegate.instance.clearTmpData(assign, submission, plugin, inputData); AddonModAssignSubmissionDelegate.instance.clearTmpData(assign, submission, plugin, inputData);
}); });
@ -356,7 +364,7 @@ export class AddonModAssignHelperProvider {
async getSubmissionSizeForEdit( async getSubmissionSizeForEdit(
assign: AddonModAssignAssign, assign: AddonModAssignAssign,
submission: AddonModAssignSubmission, submission: AddonModAssignSubmission,
inputData: any, inputData: Record<string, unknown>,
): Promise<number> { ): Promise<number> {
let totalSize = 0; let totalSize = 0;
@ -492,15 +500,17 @@ export class AddonModAssignHelperProvider {
*/ */
async hasFeedbackDataChanged( async hasFeedbackDataChanged(
assign: AddonModAssignAssign, assign: AddonModAssignAssign,
submission: AddonModAssignSubmission | AddonModAssignSubmissionFormatted, submission: AddonModAssignSubmission | AddonModAssignSubmissionFormatted | undefined,
feedback: AddonModAssignSubmissionFeedback, feedback: AddonModAssignSubmissionFeedback,
userId: number, userId: number,
): Promise<boolean> { ): Promise<boolean> {
if (!submission || !feedback.plugins) {
return false;
}
let hasChanged = false; let hasChanged = false;
const promises = feedback.plugins const promises = feedback.plugins.map((plugin) =>
? feedback.plugins.map((plugin) =>
this.prepareFeedbackPluginData(assign.id, userId, feedback).then(async (inputData) => { this.prepareFeedbackPluginData(assign.id, userId, feedback).then(async (inputData) => {
const changed = await CoreUtils.instance.ignoreErrors( const changed = await CoreUtils.instance.ignoreErrors(
AddonModAssignFeedbackDelegate.instance.hasPluginDataChanged(assign, submission, plugin, inputData, userId), AddonModAssignFeedbackDelegate.instance.hasPluginDataChanged(assign, submission, plugin, inputData, userId),
@ -511,8 +521,7 @@ export class AddonModAssignHelperProvider {
} }
return; return;
})) }));
: [];
await CoreUtils.instance.allPromises(promises); await CoreUtils.instance.allPromises(promises);
@ -529,9 +538,13 @@ export class AddonModAssignHelperProvider {
*/ */
async hasSubmissionDataChanged( async hasSubmissionDataChanged(
assign: AddonModAssignAssign, assign: AddonModAssignAssign,
submission: AddonModAssignSubmission, submission: AddonModAssignSubmission | undefined,
inputData: any, inputData: Record<string, unknown>,
): Promise<boolean> { ): Promise<boolean> {
if (!submission) {
return false;
}
let hasChanged = false; let hasChanged = false;
const promises = submission.plugins const promises = submission.plugins
@ -591,14 +604,17 @@ export class AddonModAssignHelperProvider {
*/ */
async prepareSubmissionPluginData( async prepareSubmissionPluginData(
assign: AddonModAssignAssign, assign: AddonModAssignAssign,
submission: AddonModAssignSubmission, submission: AddonModAssignSubmission | undefined,
inputData: any, inputData: Record<string, unknown>,
offline = false, offline = false,
): Promise<any> { ): Promise<AddonModAssignSavePluginData> {
const pluginData = {}; if (!submission || !submission.plugins) {
const promises = submission.plugins return {};
? submission.plugins.map((plugin) => }
const pluginData: AddonModAssignSavePluginData = {};
const promises = submission.plugins.map((plugin) =>
AddonModAssignSubmissionDelegate.instance.preparePluginSubmissionData( AddonModAssignSubmissionDelegate.instance.preparePluginSubmissionData(
assign, assign,
submission, submission,
@ -606,8 +622,7 @@ export class AddonModAssignHelperProvider {
inputData, inputData,
pluginData, pluginData,
offline, offline,
)) ));
: [];
await Promise.all(promises); await Promise.all(promises);

View File

@ -30,6 +30,7 @@ import { CoreUtils } from '@services/utils/utils';
import { AddonModAssignOffline } from './assign-offline'; import { AddonModAssignOffline } from './assign-offline';
import { AddonModAssignSubmissionDelegate } from './submission-delegate'; import { AddonModAssignSubmissionDelegate } from './submission-delegate';
import { CoreComments } from '@features/comments/services/comments'; import { CoreComments } from '@features/comments/services/comments';
import { AddonModAssignSubmissionFormatted } from './assign-helper';
const ROOT_CACHE_KEY = 'mmaModAssign:'; const ROOT_CACHE_KEY = 'mmaModAssign:';
@ -1011,7 +1012,7 @@ export class AddonModAssignProvider {
* @param assignId Assignment ID. * @param assignId Assignment ID.
* @return Promise resolved with boolean: whether it needs to be graded or not. * @return Promise resolved with boolean: whether it needs to be graded or not.
*/ */
async needsSubmissionToBeGraded(submission: any, assignId: number): Promise<boolean> { async needsSubmissionToBeGraded(submission: AddonModAssignSubmissionFormatted, assignId: number): Promise<boolean> {
if (!submission.gradingstatus) { if (!submission.gradingstatus) {
// This should not happen, but it's better to show rather than not showing any of the submissions. // This should not happen, but it's better to show rather than not showing any of the submissions.
return true; return true;
@ -1864,11 +1865,12 @@ export type AddonModAssignSubmittedForGradingEventData = {
userId: number; userId: number;
}; };
/**
* Data sent by SUBMISSION_SAVED_EVENT event.
*/
export type AddonModAssignSubmissionSavedEventData = AddonModAssignSubmittedForGradingEventData;
/** /**
* Data sent by GRADED_EVENT event. * Data sent by GRADED_EVENT event.
*/ */
export type AddonModAssignGradedEventData = { export type AddonModAssignGradedEventData = AddonModAssignSubmittedForGradingEventData;
assignmentId: number;
submissionId: number;
userId: number;
};

View File

@ -41,7 +41,7 @@ export class AddonModAssignSubmissionFileComponent extends AddonModAssignSubmiss
/** /**
* Component being initialized. * Component being initialized.
*/ */
async nOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
// Get the offline data. // Get the offline data.
const filesData = await CoreUtils.instance.ignoreErrors( const filesData = await CoreUtils.instance.ignoreErrors(
AddonModAssignOffline.instance.getSubmission(this.assign.id), AddonModAssignOffline.instance.getSubmission(this.assign.id),

View File

@ -24,12 +24,11 @@
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item class="ion-text-wrap"> <ion-item class="ion-text-wrap">
<ion-label> <ion-label></ion-label>
<core-rich-text-editor [control]="control" [placeholder]="plugin.name" <core-rich-text-editor [control]="control" [placeholder]="plugin.name"
name="onlinetext_editor_text" (contentChanged)="onChange($event)" [component]="component" name="onlinetext_editor_text" (contentChanged)="onChange($event)" [component]="component"
[componentId]="assign.cmid" [autoSave]="true" contextLevel="module" [contextInstanceId]="assign.cmid" [componentId]="assign.cmid" [autoSave]="true" contextLevel="module" [contextInstanceId]="assign.cmid"
elementId="onlinetext_editor" [draftExtraParams]="{userid: currentUserId, action: 'editsubmission'}"> elementId="onlinetext_editor" [draftExtraParams]="{userid: currentUserId, action: 'editsubmission'}">
</core-rich-text-editor> </core-rich-text-editor>
</ion-label>
</ion-item> </ion-item>
</div> </div>

View File

@ -56,7 +56,7 @@ export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignS
/** /**
* Component being initialized. * Component being initialized.
*/ */
async nOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
// Get the text. Check if we have anything offline. // Get the text. Check if we have anything offline.
const offlineData = await CoreUtils.instance.ignoreErrors( const offlineData = await CoreUtils.instance.ignoreErrors(
AddonModAssignOffline.instance.getSubmission(this.assign.id), AddonModAssignOffline.instance.getSubmission(this.assign.id),

View File

@ -267,7 +267,7 @@ export class CoreCronDelegateService {
* @return True if handler uses network or not defined, false otherwise. * @return True if handler uses network or not defined, false otherwise.
*/ */
protected handlerUsesNetwork(name: string): boolean { protected handlerUsesNetwork(name: string): boolean {
if (!this.handlers[name] || this.handlers[name].usesNetwork) { if (!this.handlers[name] || !this.handlers[name].usesNetwork) {
// Invalid, return default. // Invalid, return default.
return true; return true;
} }

View File

@ -298,3 +298,10 @@ export type CoreEventSectionStatusChangedData = CoreEventSiteData & {
courseId: number; courseId: number;
sectionId?: number; sectionId?: number;
}; };
/**
* Data passed to ACTIVITY_DATA_SENT event.
*/
export type CoreEventActivityDataSentData = CoreEventSiteData & {
module: string;
};