MOBILE-3636 assign: Implement assignment base

main
Pau Ferrer Ocaña 2021-02-09 15:06:40 +01:00
parent b0cf681ab6
commit a4eefeb25a
26 changed files with 6777 additions and 2 deletions

View File

@ -0,0 +1,42 @@
// (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 { CoreSharedModule } from '@/core/shared.module';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AddonModAssignComponentsModule } from './components/components.module';
import { AddonModAssignIndexPage } from './pages/index/index.page';
const routes: Routes = [
{
path: ':courseId/:cmId',
component: AddonModAssignIndexPage,
},
{
path: ':courseId/:cmId/submission-list',
component: AddonModAssignSubmissionListPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CoreSharedModule,
AddonModAssignComponentsModule,
],
declarations: [
AddonModAssignIndexPage,
],
})
export class AddonModAssignLazyModule {}

View File

@ -0,0 +1,66 @@
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
import { Routes } from '@angular/router';
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate';
import { CoreCronDelegate } from '@services/cron';
import { CORE_SITE_SCHEMAS } from '@services/sites';
import { AddonModAssignComponentsModule } from './components/components.module';
import { OFFLINE_SITE_SCHEMA } from './services/database/assign';
import { AddonModAssignIndexLinkHandler } from './services/handlers/index-link';
import { AddonModAssignListLinkHandler } from './services/handlers/list-link';
import { AddonModAssignModuleHandler, AddonModAssignModuleHandlerService } from './services/handlers/module';
import { AddonModAssignPrefetchHandler } from './services/handlers/prefetch';
import { AddonModAssignPushClickHandler } from './services/handlers/push-click';
import { AddonModAssignSyncCronHandler } from './services/handlers/sync-cron';
const routes: Routes = [
{
path: AddonModAssignModuleHandlerService.PAGE_NAME,
loadChildren: () => import('./assign-lazy.module').then(m => m.AddonModAssignLazyModule),
},
];
@NgModule({
imports: [
CoreMainMenuTabRoutingModule.forChild(routes),
AddonModAssignComponentsModule,
],
providers: [
{
provide: CORE_SITE_SCHEMAS,
useValue: [OFFLINE_SITE_SCHEMA],
multi: true,
},
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
CoreCourseModuleDelegate.instance.registerHandler(AddonModAssignModuleHandler.instance);
CoreContentLinksDelegate.instance.registerHandler(AddonModAssignIndexLinkHandler.instance);
CoreContentLinksDelegate.instance.registerHandler(AddonModAssignListLinkHandler.instance);
CoreCourseModulePrefetchDelegate.instance.registerHandler(AddonModAssignPrefetchHandler.instance);
CoreCronDelegate.instance.register(AddonModAssignSyncCronHandler.instance);
CorePushNotificationsDelegate.instance.registerClickHandler(AddonModAssignPushClickHandler.instance);
},
},
],
})
export class AddonModAssignModule {}

View File

@ -0,0 +1,47 @@
// (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 { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
import { AddonModAssignIndexComponent } from './index/index';
@NgModule({
declarations: [
AddonModAssignIndexComponent,
/* AddonModAssignSubmissionComponent,
AddonModAssignSubmissionPluginComponent,
AddonModAssignFeedbackPluginComponent*/
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
FormsModule,
CoreSharedModule,
CoreCourseComponentsModule,
],
exports: [
AddonModAssignIndexComponent,
/* AddonModAssignSubmissionComponent,
AddonModAssignSubmissionPluginComponent,
AddonModAssignFeedbackPluginComponent */
],
})
export class AddonModAssignComponentsModule {}

View File

@ -0,0 +1,142 @@
<!-- Buttons to add to the header. -->
<core-navbar-buttons slot="end">
<core-context-menu>
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate"
[href]="externalUrl" iconAction="fas-external-link-alt">
</core-context-menu-item>
<core-context-menu-item *ngIf="assign && (description || (assign.introattachments && assign.introattachments.length))"
[priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()"
iconAction="fas-arrow-right">
</core-context-menu-item>
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}"
iconAction="far-newspaper" (action)="gotoBlog()">
</core-context-menu-item>
<core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate"
(action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600"
[content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon"
[closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)"
[iconAction]="prefetchStatusIcon" [closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}"
iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false">
</core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center">
<!-- Description and intro attachments. -->
<ion-card *ngIf="description" (click)="expandDescription($event)" class="core-clickable">
<ion-item class="ion-text-wrap">
<ion-label>
<core-format-text [text]="description" [component]="component" [componentId]="componentId" maxHeight="120"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId" (click)="expandDescription($event)">
</core-format-text>
</ion-label>
</ion-item>
</ion-card>
<ion-card *ngIf="assign && assign.introattachments && assign.introattachments.length">
<core-file *ngFor="let file of assign.introattachments" [file]="file" [component]="component" [componentId]="componentId">
</core-file>
</ion-card>
<!-- Assign has something offline. -->
<ion-card class="core-warning-card" *ngIf="hasOffline">
<ion-item>
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
<ion-label>{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}</ion-label>
</ion-item>
</ion-card>
<!-- User can view all submissions (teacher). -->
<ng-container *ngIf="assign && canViewAllSubmissions">
<ion-list class="core-list-align-detail-right with-borders">
<ion-item class="ion-text-wrap" *ngIf="(groupInfo.separateGroups || groupInfo.visibleGroups)">
<ion-label id="addon-assign-groupslabel">
<ng-container *ngIf="groupInfo.separateGroups">{{'core.groupsseparate' | translate }}</ng-container>
<ng-container *ngIf="groupInfo.visibleGroups">{{'core.groupsvisible' | translate }}</ng-container>
</ion-label>
<ion-select [(ngModel)]="group" (ionChange)="setGroup(group)" aria-labelledby="addon-assign-groupslabel"
interface="action-sheet">
<ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
{{groupOpt.name}}
</ion-select-option>
</ion-select>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="timeRemaining">
<ion-label>
<h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2>
<p>{{ timeRemaining }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="lateSubmissions">
<ion-label>
<h2>{{ 'addon.mod_assign.latesubmissions' | translate }}</h2>
<p>{{ lateSubmissions }}</p>
</ion-label>
</ion-item>
<!-- Summary of all submissions. -->
<ion-item class="ion-text-wrap" *ngIf="summary && summary.participantcount" (click)="goToSubmissionList()" detail>
<ion-label>
<h2 *ngIf="assign.teamsubmission">{{ 'addon.mod_assign.numberofteams' | translate }}</h2>
<h2 *ngIf="!assign.teamsubmission">{{ 'addon.mod_assign.numberofparticipants' | translate }}</h2>
</ion-label>
<ion-badge slot="end" *ngIf="showNumbers" color="primary">
{{ summary.participantcount }}
</ion-badge>
</ion-item>
<!-- Summary of submissions with draft status. -->
<ion-item class="ion-text-wrap" *ngIf="assign.submissiondrafts && summary && summary.submissionsenabled"
[detail]="!showNumbers || summary.submissiondraftscount"
(click)="goToSubmissionList(submissionStatusDraft, summary.submissiondraftscount)">
<ion-label><h2>{{ 'addon.mod_assign.numberofdraftsubmissions' | translate }}</h2></ion-label>
<ion-badge slot="end" *ngIf="showNumbers" color="primary">
{{ summary.submissiondraftscount }}
</ion-badge>
</ion-item>
<!-- Summary of submissions with submitted status. -->
<ion-item class="ion-text-wrap" *ngIf="summary && summary.submissionsenabled"
[detail]="!showNumbers || summary.submissionssubmittedcount"
(click)="goToSubmissionList(submissionStatusSubmitted, summary.submissionssubmittedcount)">
<ion-label><h2>{{ 'addon.mod_assign.numberofsubmittedassignments' | translate }}</h2></ion-label>
<ion-badge slot="end" *ngIf="showNumbers" color="primary">
{{ summary.submissionssubmittedcount }}
</ion-badge>
</ion-item>
<!-- Summary of submissions that need grading. -->
<ion-item class="ion-text-wrap" *ngIf="summary && summary.submissionsenabled && !assign.teamsubmission && showNumbers"
[detail]="needsGradingAvalaible"
(click)="goToSubmissionList(needGrading, needsGradingAvalaible)">
<ion-label><h2>{{ 'addon.mod_assign.numberofsubmissionsneedgrading' | translate }}</h2></ion-label>
<ion-badge slot="end" color="primary">
{{ summary.submissionsneedgradingcount }}
</ion-badge>
</ion-item>
</ion-list>
<!-- Ungrouped users. -->
<ion-card *ngIf="assign.teamsubmission && summary && summary.warnofungroupedusers" class="core-info-card">
<ion-item>
<ion-icon name="fas-question-circle" slot="start"></ion-icon>
<ion-label>{{ 'addon.mod_assign.'+summary.warnofungroupedusers | translate }}</ion-label>
</ion-item>
</ion-card>
</ng-container>
<!-- If it's a student, display his submission. -->
<!-- @todo <addon-mod-assign-submission *ngIf="loaded && !canViewAllSubmissions && canViewOwnSubmission" [courseId]="courseId"
[moduleId]="module.id">
</addon-mod-assign-submission>-->
</core-loading>

View File

@ -0,0 +1,414 @@
// (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, Optional, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Params } from '@angular/router';
import { CoreSite } from '@classes/site';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
import { IonContent } from '@ionic/angular';
import { CoreGroupInfo, CoreGroups } from '@services/groups';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import {
AddonModAssign,
AddonModAssignAssign,
AddonModAssignGradedEventData,
AddonModAssignProvider,
AddonModAssignSubmissionGradingSummary,
} from '../../services/assign';
import { AddonModAssignOffline } from '../../services/assign-offline';
import {
AddonModAssignAutoSyncData,
AddonModAssignSync,
AddonModAssignSyncProvider,
AddonModAssignSyncResult,
} from '../../services/assign-sync';
/**
* Component that displays an assignment.
*/
@Component({
selector: 'addon-mod-assign-index',
templateUrl: 'addon-mod-assign-index.html',
})
export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy {
// @todo @ViewChild(AddonModAssignSubmissionComponent) submissionComponent?: AddonModAssignSubmissionComponent;
submissionComponent?: any;
component = AddonModAssignProvider.COMPONENT;
moduleName = 'assign';
assign?: AddonModAssignAssign; // The assign object.
canViewAllSubmissions = false; // Whether the user can view all submissions.
canViewOwnSubmission = false; // Whether the user can view their own submission.
timeRemaining?: string; // Message about time remaining to submit.
lateSubmissions?: string; // Message about late submissions.
showNumbers = true; // Whether to show number of submissions with each status.
summary?: AddonModAssignSubmissionGradingSummary; // The grading summary.
needsGradingAvalaible = false; // Whether we can see the submissions that need grading.
groupInfo: CoreGroupInfo = {
groups: [],
separateGroups: false,
visibleGroups: false,
defaultGroupId: 0,
};
// Status.
submissionStatusSubmitted = AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED;
submissionStatusDraft = AddonModAssignProvider.SUBMISSION_STATUS_DRAFT;
needGrading = AddonModAssignProvider.NEED_GRADING;
protected currentUserId?: number; // Current user ID.
protected currentSite?: CoreSite; // Current user ID.
protected syncEventName = AddonModAssignSyncProvider.AUTO_SYNCED;
// Observers.
protected savedObserver?: CoreEventObserver;
protected submittedObserver?: CoreEventObserver;
protected gradedObserver?: CoreEventObserver;
constructor(
protected content?: IonContent,
@Optional() courseContentsPage?: CoreCourseContentsPage,
) {
super('AddonModLessonIndexComponent', content, courseContentsPage);
}
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
super.ngOnInit();
this.currentUserId = CoreSites.instance.getCurrentSiteUserId();
this.currentSite = CoreSites.instance.getCurrentSite();
// Listen to events.
this.savedObserver = CoreEvents.on<any>(AddonModAssignProvider.SUBMISSION_SAVED_EVENT, (data) => {
if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) {
// Assignment submission saved, refresh data.
this.showLoadingAndRefresh(true, false);
}
}, this.siteId);
this.submittedObserver = CoreEvents.on<any>(AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, (data) => {
if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) {
// Assignment submitted, check completion.
CoreCourse.instance.checkModuleCompletion(this.courseId!, this.module!.completiondata);
// Reload data since it can have offline data now.
this.showLoadingAndRefresh(true, false);
}
}, this.siteId);
this.gradedObserver = CoreEvents.on<AddonModAssignGradedEventData>(AddonModAssignProvider.GRADED_EVENT, (data) => {
if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) {
// Assignment graded, refresh data.
this.showLoadingAndRefresh(true, false);
}
}, this.siteId);
await this.loadContent(false, true);
try {
await AddonModAssign.instance.logView(this.assign!.id, this.assign!.name);
CoreCourse.instance.checkModuleCompletion(this.courseId!, this.module!.completiondata);
} catch {
// Ignore errors. Just don't check Module completion.
}
if (this.canViewAllSubmissions) {
// User can see all submissions, log grading view.
CoreUtils.instance.ignoreErrors(AddonModAssign.instance.logGradingView(this.assign!.id, this.assign!.name));
} else if (this.canViewOwnSubmission) {
// User can only see their own submission, log view the user submission.
CoreUtils.instance.ignoreErrors(AddonModAssign.instance.logSubmissionView(this.assign!.id, this.assign!.name));
}
}
/**
* Expand the description.
*/
expandDescription(ev?: Event): void {
ev?.preventDefault();
ev?.stopPropagation();
if (this.assign && (this.description || this.assign.introattachments)) {
CoreTextUtils.instance.viewText(Translate.instance.instant('core.description'), this.description || '', {
component: this.component,
componentId: this.module!.id,
files: this.assign.introattachments,
filter: true,
contextLevel: 'module',
instanceId: this.module!.id,
courseId: this.courseId,
});
}
}
/**
* Get assignment data.
*
* @param refresh If it's refreshing content.
* @param sync If it should try to sync.
* @param showErrors If show errors to the user of hide them.
* @return Promise resolved when done.
*/
protected async fetchContent(refresh = false, sync = false, showErrors = false): Promise<void> {
// Get assignment data.
try {
this.assign = await AddonModAssign.instance.getAssignment(this.courseId!, this.module!.id);
this.dataRetrieved.emit(this.assign);
this.description = this.assign.intro;
if (sync) {
// Try to synchronize the assign.
await CoreUtils.instance.ignoreErrors(this.syncActivity(showErrors));
}
// Check if there's any offline data for this assign.
this.hasOffline = await AddonModAssignOffline.instance.hasAssignOfflineData(this.assign.id);
// Get assignment submissions.
const submissions = await AddonModAssign.instance.getSubmissions(this.assign.id, { cmId: this.module!.id });
const time = CoreTimeUtils.instance.timestamp();
this.canViewAllSubmissions = submissions.canviewsubmissions;
if (submissions.canviewsubmissions) {
// Calculate the messages to display about time remaining and late submissions.
if (this.assign.duedate > 0) {
if (this.assign.duedate - time <= 0) {
this.timeRemaining = Translate.instance.instant('addon.mod_assign.assignmentisdue');
} else {
this.timeRemaining = CoreTimeUtils.instance.formatDuration(this.assign.duedate - time, 3);
if (this.assign.cutoffdate) {
if (this.assign.cutoffdate > time) {
this.lateSubmissions = Translate.instance.instant(
'addon.mod_assign.latesubmissionsaccepted',
{ $a: CoreTimeUtils.instance.userDate(this.assign.cutoffdate * 1000) },
);
} else {
this.lateSubmissions = Translate.instance.instant('addon.mod_assign.nomoresubmissionsaccepted');
}
} else {
this.lateSubmissions = '';
}
}
} else {
this.timeRemaining = '';
this.lateSubmissions = '';
}
// Check if groupmode is enabled to avoid showing wrong numbers.
this.groupInfo = await CoreGroups.instance.getActivityGroupInfo(this.assign.cmid, false);
this.showNumbers = (this.groupInfo.groups && this.groupInfo.groups.length == 0) ||
this.currentSite!.isVersionGreaterEqualThan('3.5');
await this.setGroup(CoreGroups.instance.validateGroupId(this.group, this.groupInfo));
return;
}
try {
// Check if the user can view their own submission.
await AddonModAssign.instance.getSubmissionStatus(this.assign.id, { cmId: this.module!.id });
this.canViewOwnSubmission = true;
} catch (error) {
this.canViewOwnSubmission = false;
if (error.errorcode !== 'nopermission') {
throw error;
}
}
} finally {
this.fillContextMenu(refresh);
}
}
/**
* Set group to see the summary.
*
* @param groupId Group ID.
* @return Resolved when done.
*/
async setGroup(groupId: number): Promise<void> {
this.group = groupId;
const submissionStatus = await AddonModAssign.instance.getSubmissionStatus(this.assign!.id, {
groupId: this.group,
cmId: this.module!.id,
});
this.summary = submissionStatus.gradingsummary;
if (!this.summary) {
this.needsGradingAvalaible = false;
return;
}
if (this.summary?.warnofungroupedusers === true) {
this.summary.warnofungroupedusers = 'ungroupedusers';
} else {
switch (this.summary?.warnofungroupedusers) {
case AddonModAssignProvider.WARN_GROUPS_REQUIRED:
this.summary.warnofungroupedusers = 'ungroupedusers';
break;
case AddonModAssignProvider.WARN_GROUPS_OPTIONAL:
this.summary.warnofungroupedusers = 'ungroupedusersoptional';
break;
default:
this.summary.warnofungroupedusers = '';
break;
}
}
this.needsGradingAvalaible =
(submissionStatus.gradingsummary?.submissionsneedgradingcount || 0) > 0 &&
this.currentSite!.isVersionGreaterEqualThan('3.2');
}
/**
* Go to view a list of submissions.
*
* @param status Status to see.
* @param count Number of submissions with the status.
*/
goToSubmissionList(status: string, count: number): void {
if (typeof status != 'undefined' && !count && this.showNumbers) {
return;
}
const params: Params = {
groupId: this.group || 0,
moduleName: this.moduleName,
};
if (typeof status != 'undefined') {
params.status = status;
}
CoreNavigator.instance.navigate('submission-list', {
params,
});
}
/**
* Checks if sync has succeed from result sync data.
*
* @param result Data returned by the sync function.
* @return If succeed or not.
*/
protected hasSyncSucceed(result: AddonModAssignSyncResult): boolean {
if (result.updated) {
this.submissionComponent?.invalidateAndRefresh(false);
}
return result.updated;
}
/**
* Perform the invalidate content function.
*
* @return Resolved when done.
*/
protected async invalidateContent(): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(AddonModAssign.instance.invalidateAssignmentData(this.courseId!));
if (this.assign) {
promises.push(AddonModAssign.instance.invalidateAllSubmissionData(this.assign.id));
if (this.canViewAllSubmissions) {
promises.push(AddonModAssign.instance.invalidateSubmissionStatusData(this.assign.id, undefined, this.group));
}
}
await Promise.all(promises).finally(() => {
this.submissionComponent?.invalidateAndRefresh(true);
});
}
/**
* User entered the page that contains the component.
*/
ionViewDidEnter(): void {
super.ionViewDidEnter();
this.submissionComponent?.ionViewDidEnter();
}
/**
* User left the page that contains the component.
*/
ionViewDidLeave(): void {
super.ionViewDidLeave();
this.submissionComponent?.ionViewDidLeave();
}
/**
* Compares sync event data with current data to check if refresh content is needed.
*
* @param syncEventData Data receiven on sync observer.
* @return True if refresh is needed, false otherwise.
*/
protected isRefreshSyncNeeded(syncEventData: AddonModAssignAutoSyncData): boolean {
if (this.assign && syncEventData.assignId == this.assign.id) {
if (syncEventData.warnings && syncEventData.warnings.length) {
// Show warnings.
CoreDomUtils.instance.showErrorModal(syncEventData.warnings[0]);
}
return true;
}
return false;
}
/**
* Performs the sync of the activity.
*
* @return Promise resolved when done.
*/
protected async sync(): Promise<void> {
await AddonModAssignSync.instance.syncAssign(this.assign!.id);
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
super.ngOnDestroy();
this.savedObserver?.off();
this.submittedObserver?.off();
this.gradedObserver?.off();
}
}

View File

@ -0,0 +1,104 @@
{
"acceptsubmissionstatement": "Please accept the submission statement.",
"addattempt": "Allow another attempt",
"addnewattempt": "Add a new attempt",
"addnewattemptfromprevious": "Add a new attempt based on previous submission",
"addsubmission": "Add submission",
"allowsubmissionsfromdate": "Allow submissions from",
"allowsubmissionsfromdatesummary": "This assignment will accept submissions from <strong>{{$a}}</strong>",
"allowsubmissionsanddescriptionfromdatesummary": "The assignment details and submission form will be available from <strong>{{$a}}</strong>",
"applytoteam": "Apply grades and feedback to entire group",
"assignmentisdue": "Assignment is due",
"attemptnumber": "Attempt number",
"attemptreopenmethod": "Attempts reopened",
"attemptreopenmethod_manual": "Manually",
"attemptreopenmethod_untilpass": "Automatically until pass",
"attemptsettings": "Attempt settings",
"cannotgradefromapp": "Certain grading methods are not yet supported by the app and cannot be modified.",
"cannoteditduetostatementsubmission": "You can't add or edit 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.",
"confirmsubmission": "Are you sure you want to submit your work for grading? You will not be able to make any more changes.",
"currentgrade": "Current grade in gradebook",
"cutoffdate": "Cut-off date",
"currentattempt": "This is attempt {{$a}}.",
"currentattemptof": "This is attempt {{$a.attemptnumber}} ( {{$a.maxattempts}} attempts allowed ).",
"defaultteam": "Default group",
"duedate": "Due date",
"duedateno": "No due date",
"duedatereached": "The due date for this assignment has now passed",
"editingstatus": "Editing status",
"editsubmission": "Edit submission",
"erroreditpluginsnotsupported": "You can't add or edit a submission in the app because certain plugins are not yet supported for editing.",
"errorshowinginformation": "Submission information cannot be displayed.",
"extensionduedate": "Extension due date",
"feedbacknotsupported": "This feedback is not supported by the app and may not contain all the information.",
"grade": "Grade",
"graded": "Graded",
"gradedby": "Graded by",
"gradedfollowupsubmit": "Graded - follow up submission received",
"gradenotsynced": "Grade not synced",
"gradedon": "Graded on",
"gradelocked": "This grade is locked or overridden in the gradebook.",
"gradeoutof": "Grade out of {{$a}}",
"gradingstatus": "Grading status",
"groupsubmissionsettings": "Group submission settings",
"hiddenuser": "Participant",
"latesubmissions": "Late submissions",
"latesubmissionsaccepted": "Allowed until {{$a}}",
"markingworkflowstate": "Marking workflow state",
"markingworkflowstateinmarking": "In marking",
"markingworkflowstateinreview": "In review",
"markingworkflowstatenotmarked": "Not marked",
"markingworkflowstatereadyforreview": "Marking completed",
"markingworkflowstatereadyforrelease": "Ready for release",
"markingworkflowstatereleased": "Released",
"modulenameplural": "Assignments",
"multipleteams": "Member of more than one group",
"multipleteams_desc": "The assignment requires submission in groups. You are a member of more than one group. To be able to submit you must be a member of only one group. Please contact your teacher to change your group membership.",
"noattempt": "No attempt",
"nomoresubmissionsaccepted": "Only allowed for participants who have been granted an extension",
"noonlinesubmissions": "This assignment does not require you to submit anything online",
"nosubmission": "Nothing has been submitted for this assignment",
"notallparticipantsareshown": "Participants who have not made a submission are not shown.",
"noteam": "Not a member of any group",
"noteam_desc": "This assignment requires submission in groups. You are not a member of any group, so you cannot create a submission. Please contact your teacher to be added to a group.",
"notgraded": "Not graded",
"numberofdraftsubmissions": "Drafts",
"numberofparticipants": "Participants",
"numberofsubmittedassignments": "Submitted",
"numberofsubmissionsneedgrading": "Needs grading",
"numberofteams": "Groups",
"numwords": "{{$a}} words",
"outof": "{{$a.current}} out of {{$a.total}}",
"overdue": "<font color=\"red\">Assignment is overdue by: {{$a}}</font>",
"submissioneditable": "Student can edit this submission",
"submissionnoteditable": "Student cannot edit this submission",
"submissionnotsupported": "This submission is not supported by the app and may not contain all the information.",
"submission": "Submission",
"submissionslocked": "This assignment is not accepting submissions",
"submissionstatus_draft": "Draft (not submitted)",
"submissionstatusheading": "Submission status",
"submissionstatus_marked": "Graded",
"submissionstatus_new": "No submission",
"submissionstatus_reopened": "Reopened",
"submissionstatus_submitted": "Submitted for grading",
"submissionstatus_": "No submission",
"submissionstatus": "Submission status",
"submissionteam": "Group",
"submitassignment_help": "Once this assignment is submitted you will not be able to make any more changes.",
"submitassignment": "Submit assignment",
"submittedearly": "Assignment was submitted {{$a}} early",
"submittedlate": "Assignment was submitted {{$a}} late",
"syncblockedusercomponent": "user grade",
"timemodified": "Last modified",
"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.",
"ungroupedusersoptional": "The setting 'Students submit in groups' is enabled and some users are either not a member of any group, or are a member of more than one group. Please be aware that these students will submit as members of the 'Default group'.",
"unlimitedattempts": "Unlimited",
"userwithid": "User with ID {{id}}",
"userswhoneedtosubmit": "Users who need to submit: {{$a}}",
"viewsubmission": "View submission",
"warningsubmissionmodified": "The user submission was modified on the site.",
"warningsubmissiongrademodified": "The submission grade was modified on the site.",
"wordlimit": "Word limit"
}

View File

@ -0,0 +1,22 @@
<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]="module?.id" [courseId]="courseId">
</core-format-text>
</ion-title>
<ion-buttons slot="end">
<!-- The buttons defined by the component will be added in here. -->
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!assignComponent?.loaded" (ionRefresh)="assignComponent?.doRefresh($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<addon-mod-assign-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)"></addon-mod-assign-index>
</ion-content>

View File

@ -0,0 +1,68 @@
// (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 { CoreCourseWSModule } from '@features/course/services/course';
import { CoreNavigator } from '@services/navigator';
import { AddonModAssignIndexComponent } from '../../components/index/index';
import { AddonModAssignAssign } from '../../services/assign';
/**
* Page that displays an assign.
*/
@Component({
selector: 'page-addon-mod-assign-index',
templateUrl: 'index.html',
})
export class AddonModAssignIndexPage implements OnInit {
@ViewChild(AddonModAssignIndexComponent) assignComponent?: AddonModAssignIndexComponent;
title?: string;
module?: CoreCourseWSModule;
courseId?: number;
/**
* Component being initialized.
*/
ngOnInit(): void {
this.module = CoreNavigator.instance.getRouteParam('module');
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId');
this.title = this.module?.name;
}
/**
* Update some data based on the assign instance.
*
* @param assign Assign instance.
*/
updateData(assign: AddonModAssignAssign): void {
this.title = assign.name || this.title;
}
/**
* User entered the page.
*/
ionViewDidEnter(): void {
this.assignComponent?.ionViewDidEnter();
}
/**
* User left the page.
*/
ionViewDidLeave(): void {
this.assignComponent?.ionViewDidLeave();
}
}

View File

@ -0,0 +1,727 @@
// (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 { Injectable } from '@angular/core';
import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader';
import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites';
import { CoreWSExternalFile } from '@services/ws';
import { FileEntry } from '@ionic-native/file/ngx';
import {
AddonModAssignProvider,
AddonModAssignAssign,
AddonModAssignSubmission,
AddonModAssignParticipant,
AddonModAssignSubmissionFeedback,
AddonModAssign,
} from './assign';
import { AddonModAssignOffline } from './assign-offline';
import { CoreUtils } from '@services/utils/utils';
import { CoreFile } from '@services/file';
import { CoreCourseCommonModWSOptions } from '@features/course/services/course';
import { CoreGroups } from '@services/groups';
import { AddonModAssignSubmissionDelegate } from './submission-delegate';
import { AddonModAssignFeedbackDelegate } from './feedback-delegate';
import { makeSingleton } from '@singletons';
/**
* Service that provides some helper functions for assign.
*/
@Injectable({ providedIn: 'root' })
export class AddonModAssignHelperProvider {
/**
* Check if a submission can be edited in offline.
*
* @param assign Assignment.
* @param submission Submission.
* @return Whether it can be edited offline.
*/
async canEditSubmissionOffline(assign: AddonModAssignAssign, submission: AddonModAssignSubmission): Promise<boolean> {
if (!submission) {
return false;
}
if (submission.status == AddonModAssignProvider.SUBMISSION_STATUS_NEW ||
submission.status == AddonModAssignProvider.SUBMISSION_STATUS_REOPENED) {
// It's a new submission, allow creating it in offline.
return true;
}
let canEdit = true;
const promises = submission.plugins
? submission.plugins.map((plugin) =>
AddonModAssignSubmissionDelegate.instance.canPluginEditOffline(assign, submission, plugin).then((canEditPlugin) => {
if (!canEditPlugin) {
canEdit = false;
}
return;
}))
: [];
await Promise.all(promises);
return canEdit;
}
/**
* Clear plugins temporary data because a submission was cancelled.
*
* @param assign Assignment.
* @param submission Submission to clear the data for.
* @param inputData Data entered in the submission form.
*/
clearSubmissionPluginTmpData(assign: AddonModAssignAssign, submission: AddonModAssignSubmission, inputData: any): void {
submission.plugins?.forEach((plugin) => {
AddonModAssignSubmissionDelegate.instance.clearTmpData(assign, submission, plugin, inputData);
});
}
/**
* Copy the data from last submitted attempt to the current submission.
* Since we don't have any WS for that we'll have to re-submit everything manually.
*
* @param assign Assignment.
* @param previousSubmission Submission to copy.
* @return Promise resolved when done.
*/
async copyPreviousAttempt(assign: AddonModAssignAssign, previousSubmission: AddonModAssignSubmission): Promise<void> {
const pluginData: any = {};
const promises = previousSubmission.plugins
? previousSubmission.plugins.map((plugin) =>
AddonModAssignSubmissionDelegate.instance.copyPluginSubmissionData(assign, plugin, pluginData))
: [];
await Promise.all(promises);
// We got the plugin data. Now we need to submit it.
if (Object.keys(pluginData).length) {
// There's something to save.
return AddonModAssign.instance.saveSubmissionOnline(assign.id, pluginData);
}
}
/**
* Create an empty feedback object.
*
* @return Feedback.
*/
createEmptyFeedback(): AddonModAssignSubmissionFeedback {
return {
grade: undefined,
gradefordisplay: undefined,
gradeddate: undefined,
};
}
/**
* Create an empty submission object.
*
* @return Submission.
*/
createEmptySubmission(): AddonModAssignSubmissionFormatted {
return {
id: undefined,
userid: undefined,
attemptnumber: undefined,
timecreated: undefined,
timemodified: undefined,
status: undefined,
groupid: undefined,
};
}
/**
* Delete stored submission files for a plugin. See storeSubmissionFiles.
*
* @param assignId Assignment ID.
* @param folderName Name of the plugin folder. Must be unique (both in submission and feedback plugins).
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteStoredSubmissionFiles(assignId: number, folderName: string, userId?: number, siteId?: string): Promise<void> {
const folderPath = await AddonModAssignOffline.instance.getSubmissionPluginFolder(assignId, folderName, userId, siteId);
await CoreFile.instance.removeDir(folderPath);
}
/**
* Delete all drafts of the feedback plugin data.
*
* @param assignId Assignment Id.
* @param userId User Id.
* @param feedback Feedback data.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async discardFeedbackPluginData(
assignId: number,
userId: number,
feedback: AddonModAssignSubmissionFeedback,
siteId?: string,
): Promise<void> {
const promises = feedback.plugins
? feedback.plugins.map((plugin) =>
AddonModAssignFeedbackDelegate.instance.discardPluginFeedbackData(assignId, userId, plugin, siteId))
: [];
await Promise.all(promises);
}
/**
* Check if a submission has no content.
*
* @param assign Assignment object.
* @param submission Submission to inspect.
* @return Whether the submission is empty.
*/
isSubmissionEmpty(assign: AddonModAssignAssign, submission?: AddonModAssignSubmission): boolean {
if (!submission) {
return true;
}
const anyNotEmpty = submission.plugins?.some((plugin) =>
!AddonModAssignSubmissionDelegate.instance.isPluginEmpty(assign, plugin));
// If any plugin is not empty, we consider that the submission is not empty either.
if (anyNotEmpty) {
return false;
}
// If all the plugins were empty (or there were no plugins), we consider the submission to be empty.
return true;
}
/**
* List the participants for a single assignment, with some summary info about their submissions.
*
* @param assign Assignment object.
* @param groupId Group Id.
* @param options Other options.
* @return Promise resolved with the list of participants and summary of submissions.
*/
async getParticipants(
assign: AddonModAssignAssign,
groupId?: number,
options: CoreSitesCommonWSOptions = {},
): Promise<AddonModAssignParticipant[]> {
groupId = groupId || 0;
options.siteId = options.siteId || CoreSites.instance.getCurrentSiteId();
// Create new options including all existing ones.
const modOptions: CoreCourseCommonModWSOptions = { cmId: assign.cmid, ...options };
const participants = await AddonModAssign.instance.listParticipants(assign.id, groupId, modOptions);
if (groupId || participants && participants.length > 0) {
return participants;
}
// If no participants returned and all groups specified, get participants by groups.
const groupsInfo = await CoreGroups.instance.getActivityGroupInfo(assign.cmid, false, undefined, modOptions.siteId);
[];
const participantsIndexed: {[id: number]: AddonModAssignParticipant} = {};
const promises = groupsInfo.groups
? groupsInfo.groups.map((userGroup) =>
AddonModAssign.instance.listParticipants(assign.id, userGroup.id, modOptions).then((participantsFromList) => {
// Do not get repeated users.
participantsFromList.forEach((participant) => {
participantsIndexed[participant.id] = participant;
});
return;
}))
:[];
await Promise.all(promises);
return CoreUtils.instance.objectToArray(participantsIndexed);
}
/**
* Get plugin config from assignment config.
*
* @param assign Assignment object including all config.
* @param subtype Subtype name (assignsubmission or assignfeedback)
* @param type Name of the subplugin.
* @return Object containing all configurations of the subplugin selected.
*/
getPluginConfig(assign: AddonModAssignAssign, subtype: string, type: string): AddonModAssignPluginConfig {
const configs: AddonModAssignPluginConfig = {};
assign.configs.forEach((config) => {
if (config.subtype == subtype && config.plugin == type) {
configs[config.name] = config.value;
}
});
return configs;
}
/**
* Get enabled subplugins.
*
* @param assign Assignment object including all config.
* @param subtype Subtype name (assignsubmission or assignfeedback)
* @return List of enabled plugins for the assign.
*/
getPluginsEnabled(assign: AddonModAssignAssign, subtype: string): AddonModAssignPluginsEnabled {
const enabled: AddonModAssignPluginsEnabled = [];
assign.configs.forEach((config) => {
if (config.subtype == subtype && config.name == 'enabled' && parseInt(config.value, 10) === 1) {
// Format the plugin objects.
enabled.push({
type: config.plugin,
});
}
});
return enabled;
}
/**
* Get a list of stored submission files. See storeSubmissionFiles.
*
* @param assignId Assignment ID.
* @param folderName Name of the plugin folder. Must be unique (both in submission and feedback plugins).
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the files.
*/
async getStoredSubmissionFiles(
assignId: number,
folderName: string,
userId?: number,
siteId?: string,
): Promise<(FileEntry | DirectoryEntry)[]> {
const folderPath = await AddonModAssignOffline.instance.getSubmissionPluginFolder(assignId, folderName, userId, siteId);
return CoreFile.instance.getDirectoryContents(folderPath);
}
/**
* Get the size that will be uploaded to perform an attempt copy.
*
* @param assign Assignment.
* @param previousSubmission Submission to copy.
* @return Promise resolved with the size.
*/
async getSubmissionSizeForCopy(assign: AddonModAssignAssign, previousSubmission: AddonModAssignSubmission): Promise<number> {
let totalSize = 0;
const promises = previousSubmission.plugins
? previousSubmission.plugins.map((plugin) =>
AddonModAssignSubmissionDelegate.instance.getPluginSizeForCopy(assign, plugin).then((size) => {
totalSize += (size || 0);
return;
}))
: [];
await Promise.all(promises);
return totalSize;
}
/**
* Get the size that will be uploaded to save a submission.
*
* @param assign Assignment.
* @param submission Submission to check data.
* @param inputData Data entered in the submission form.
* @return Promise resolved with the size.
*/
async getSubmissionSizeForEdit(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
inputData: any,
): Promise<number> {
let totalSize = 0;
const promises = submission.plugins
? submission.plugins.map((plugin) =>
AddonModAssignSubmissionDelegate.instance.getPluginSizeForEdit(assign, submission, plugin, inputData)
.then((size) => {
totalSize += (size || 0);
return;
}))
: [];
await Promise.all(promises);
return totalSize;
}
/**
* Get user data for submissions since they only have userid.
*
* @param assign Assignment object.
* @param submissions Submissions to get the data for.
* @param groupId Group Id.
* @param options Other options.
* @return Promise always resolved. Resolve param is the formatted submissions.
*/
async getSubmissionsUserData(
assign: AddonModAssignAssign,
submissions: AddonModAssignSubmissionFormatted[] = [],
groupId?: number,
options: CoreSitesCommonWSOptions = {},
): Promise<AddonModAssignSubmissionFormatted[]> {
// Create new options including all existing ones.
const modOptions: CoreCourseCommonModWSOptions = { cmId: assign.cmid, ...options };
const parts = await this.getParticipants(assign, groupId, options);
const blind = assign.blindmarking && !assign.revealidentities;
const promises: Promise<void>[] = [];
const result: AddonModAssignSubmissionFormatted[] = [];
const participants: {[id: number]: AddonModAssignParticipant} = CoreUtils.instance.arrayToObject(parts, 'id');
submissions.forEach((submission) => {
submission.submitid = submission.userid && submission.userid > 0 ? submission.userid : submission.blindid;
if (typeof submission.submitid == 'undefined' || submission.submitid <= 0) {
return;
}
const participant = participants[submission.submitid];
if (!participant) {
// Avoid permission denied error. Participant not found on list.
return;
}
delete participants[submission.submitid];
if (!blind) {
submission.userfullname = participant.fullname;
submission.userprofileimageurl = participant.profileimageurl;
}
submission.manyGroups = !!participant.groups && participant.groups.length > 1;
submission.noGroups = !!participant.groups && participant.groups.length == 0;
if (participant.groupname) {
submission.groupid = participant.groupid!;
submission.groupname = participant.groupname;
}
let promise = Promise.resolve();
if (submission.userid && submission.userid > 0 && blind) {
// Blind but not blinded! (Moodle < 3.1.1, 3.2).
delete submission.userid;
promise = AddonModAssign.instance.getAssignmentUserMappings(assign.id, submission.submitid, modOptions)
.then((blindId) => {
submission.blindid = blindId;
return;
});
}
promises.push(promise.then(() => {
// Add to the list.
if (submission.userfullname || submission.blindid) {
result.push(submission);
}
return;
}));
});
await Promise.all(promises);
// Create a submission for each participant left in the list (the participants already treated were removed).
CoreUtils.instance.objectToArray(participants).forEach((participant: AddonModAssignParticipant) => {
const submission = this.createEmptySubmission();
submission.submitid = participant.id;
if (!blind) {
submission.userid = participant.id;
submission.userfullname = participant.fullname;
submission.userprofileimageurl = participant.profileimageurl;
} else {
submission.blindid = participant.id;
}
submission.manyGroups = !!participant.groups && participant.groups.length > 1;
submission.noGroups = !!participant.groups && participant.groups.length == 0;
if (participant.groupname) {
submission.groupid = participant.groupid!;
submission.groupname = participant.groupname;
}
submission.status = participant.submitted ? AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED :
AddonModAssignProvider.SUBMISSION_STATUS_NEW;
result.push(submission);
});
return result;
}
/**
* Check if the feedback data has changed for a certain submission and assign.
*
* @param assign Assignment.
* @param submission The submission.
* @param feedback Feedback data.
* @param userId The user ID.
* @return Promise resolved with true if data has changed, resolved with false otherwise.
*/
async hasFeedbackDataChanged(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
feedback: AddonModAssignSubmissionFeedback,
userId: number,
): Promise<boolean> {
let hasChanged = false;
const promises = feedback.plugins
? feedback.plugins.map((plugin) =>
this.prepareFeedbackPluginData(assign.id, userId, feedback).then(async (inputData) => {
const changed = await CoreUtils.instance.ignoreErrors(
AddonModAssignFeedbackDelegate.instance.hasPluginDataChanged(assign, submission, plugin, inputData, userId),
false,
);
if (changed) {
hasChanged = true;
}
return;
}))
: [];
await CoreUtils.instance.allPromises(promises);
return hasChanged;
}
/**
* Check if the submission data has changed for a certain submission and assign.
*
* @param assign Assignment.
* @param submission Submission to check data.
* @param inputData Data entered in the submission form.
* @return Promise resolved with true if data has changed, resolved with false otherwise.
*/
async hasSubmissionDataChanged(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
inputData: any,
): Promise<boolean> {
let hasChanged = false;
const promises = submission.plugins
? submission.plugins.map((plugin) =>
AddonModAssignSubmissionDelegate.instance.hasPluginDataChanged(assign, submission, plugin, inputData)
.then((changed) => {
if (changed) {
hasChanged = true;
}
return;
}).catch(() => {
// Ignore errors.
}))
: [];
await CoreUtils.instance.allPromises(promises);
return hasChanged;
}
/**
* Prepare and return the plugin data to send for a certain feedback and assign.
*
* @param assignId Assignment Id.
* @param userId User Id.
* @param feedback Feedback data.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with plugin data to send to server.
*/
async prepareFeedbackPluginData(
assignId: number,
userId: number,
feedback: AddonModAssignSubmissionFeedback,
siteId?: string,
): Promise<any> {
const pluginData = {};
const promises = feedback.plugins
? feedback.plugins.map((plugin) =>
AddonModAssignFeedbackDelegate.instance.preparePluginFeedbackData(assignId, userId, plugin, pluginData, siteId))
: [];
await Promise.all(promises);
return pluginData;
}
/**
* Prepare and return the plugin data to send for a certain submission and assign.
*
* @param assign Assignment.
* @param submission Submission to check data.
* @param inputData Data entered in the submission form.
* @param offline True to prepare the data for an offline submission, false otherwise.
* @return Promise resolved with plugin data to send to server.
*/
async prepareSubmissionPluginData(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
inputData: any,
offline = false,
): Promise<any> {
const pluginData = {};
const promises = submission.plugins
? submission.plugins.map((plugin) =>
AddonModAssignSubmissionDelegate.instance.preparePluginSubmissionData(
assign,
submission,
plugin,
inputData,
pluginData,
offline,
))
: [];
await Promise.all(promises);
return pluginData;
}
/**
* Given a list of files (either online files or local files), store the local files in a local folder
* to be submitted later.
*
* @param assignId Assignment ID.
* @param folderName Name of the plugin folder. Must be unique (both in submission and feedback plugins).
* @param files List of files.
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if success, rejected otherwise.
*/
async storeSubmissionFiles(
assignId: number,
folderName: string,
files: (CoreWSExternalFile | FileEntry)[],
userId?: number,
siteId?: string,
): Promise<CoreFileUploaderStoreFilesResult> {
// Get the folder where to store the files.
const folderPath = await AddonModAssignOffline.instance.getSubmissionPluginFolder(assignId, folderName, userId, siteId);
return CoreFileUploader.instance.storeFilesToUpload(folderPath, files);
}
/**
* Upload a file to a draft area. If the file is an online file it will be downloaded and then re-uploaded.
*
* @param assignId Assignment ID.
* @param file Online file or local FileEntry.
* @param itemId Draft ID to use. Undefined or 0 to create a new draft ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the itemId.
*/
uploadFile(assignId: number, file: CoreWSExternalFile | FileEntry, itemId?: number, siteId?: string): Promise<number> {
return CoreFileUploader.instance.uploadOrReuploadFile(file, itemId, AddonModAssignProvider.COMPONENT, assignId, siteId);
}
/**
* Given a list of files (either online files or local files), upload them to a draft area and return the draft ID.
* Online files will be downloaded and then re-uploaded.
* If there are no files to upload it will return a fake draft ID (1).
*
* @param assignId Assignment ID.
* @param files List of files.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the itemId.
*/
uploadFiles(assignId: number, files: (CoreWSExternalFile | FileEntry)[], siteId?: string): Promise<number> {
return CoreFileUploader.instance.uploadOrReuploadFiles(files, AddonModAssignProvider.COMPONENT, assignId, siteId);
}
/**
* Upload or store some files, depending if the user is offline or not.
*
* @param assignId Assignment ID.
* @param folderName Name of the plugin folder. Must be unique (both in submission and feedback plugins).
* @param files List of files.
* @param offline True if files sould be stored for offline, false to upload them.
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
uploadOrStoreFiles(
assignId: number,
folderName: string,
files: (CoreWSExternalFile | FileEntry)[],
offline = false,
userId?: number,
siteId?: string,
): Promise<any> {
if (offline) {
return this.storeSubmissionFiles(assignId, folderName, files, userId, siteId);
}
return this.uploadFiles(assignId, files, siteId);
}
}
export const AddonModAssignHelper = makeSingleton(AddonModAssignHelperProvider);
/**
* Assign submission with some calculated data.
*/
export type AddonModAssignSubmissionFormatted =
Omit<AddonModAssignSubmission, 'id' | 'userid' | 'attemptnumber' | 'timecreated' | 'timemodified' | 'status' | 'groupid'> & {
id?: number; // Submission id.
userid?: number; // Student id.
attemptnumber?: number; // Attempt number.
timecreated?: number; // Submission creation time.
timemodified?: number; // Submission last modified time.
status?: string; // Submission status.
groupid?: number; // Group id.
blindid?: number; // Calculated in the app. Blindid of the user that did the submission.
submitid?: number; // Calculated in the app. Userid or blindid of the user that did the submission.
userfullname?: string; // Calculated in the app. Full name of the user that did the submission.
userprofileimageurl?: string; // Calculated in the app. Avatar of the user that did the submission.
manyGroups?: boolean; // Calculated in the app. Whether the user belongs to more than 1 group.
noGroups?: boolean; // Calculated in the app. Whether the user doesn't belong to any group.
groupname?: string; // Calculated in the app. Name of the group the submission belongs to.
};
/**
* Assingment subplugins type enabled.
*/
export type AddonModAssignPluginsEnabled = {
type: string; // Plugin type.
}[];
/**
* Assingment plugin config.
*/
export type AddonModAssignPluginConfig = {[name: string]: string};

View File

@ -0,0 +1,459 @@
// (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 { Injectable } from '@angular/core';
import { CoreError } from '@classes/errors/error';
import { SQLiteDBRecordValues } from '@classes/sqlitedb';
import { CoreFile } from '@services/file';
import { CoreSites } from '@services/sites';
import { CoreTextUtils } from '@services/utils/text';
import { CoreTimeUtils } from '@services/utils/time';
import { makeSingleton } from '@singletons';
import { AddonModAssignOutcomes, AddonModAssignSavePluginData } from './assign';
import {
AddonModAssignSubmissionsDBRecord,
AddonModAssignSubmissionsGradingDBRecord,
SUBMISSIONS_GRADES_TABLE,
SUBMISSIONS_TABLE,
} from './database/assign';
/**
* Service to handle offline assign.
*/
@Injectable({ providedIn: 'root' })
export class AddonModAssignOfflineProvider {
/**
* Delete a submission.
*
* @param assignId Assignment ID.
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if deleted, rejected if failure.
*/
async deleteSubmission(assignId: number, userId?: number, siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
userId = userId || site.getUserId();
await site.getDb().deleteRecords(
SUBMISSIONS_TABLE,
{ assignid: assignId, userid: userId },
);
}
/**
* Delete a submission grade.
*
* @param assignId Assignment ID.
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if deleted, rejected if failure.
*/
async deleteSubmissionGrade(assignId: number, userId?: number, siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
userId = userId || site.getUserId();
await site.getDb().deleteRecords(
SUBMISSIONS_GRADES_TABLE,
{ assignid: assignId, userid: userId },
);
}
/**
* Get all the assignments ids that have something to be synced.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with assignments id that have something to be synced.
*/
async getAllAssigns(siteId?: string): Promise<number[]> {
const promises:
Promise<AddonModAssignSubmissionsDBRecordFormatted[] | AddonModAssignSubmissionsGradingDBRecordFormatted[]>[] = [];
promises.push(this.getAllSubmissions(siteId));
promises.push(this.getAllSubmissionsGrade(siteId));
const results = await Promise.all(promises);
// Flatten array.
const flatten: (AddonModAssignSubmissionsDBRecord | AddonModAssignSubmissionsGradingDBRecord)[] =
[].concat.apply([], results);
// Get assign id.
let assignIds: number[] = flatten.map((assign) => assign.assignid);
// Get unique values.
assignIds = assignIds.filter((id, pos) => assignIds.indexOf(id) == pos);
return assignIds;
}
/**
* Get all the stored submissions from all the assignments.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with submissions.
*/
protected async getAllSubmissions(siteId?: string): Promise<AddonModAssignSubmissionsDBRecordFormatted[]> {
return this.getAssignSubmissionsFormatted(undefined, siteId);
}
/**
* Get all the stored submissions for a certain assignment.
*
* @param assignId Assignment ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with submissions.
*/
async getAssignSubmissions(assignId: number, siteId?: string): Promise<AddonModAssignSubmissionsDBRecordFormatted[]> {
return this.getAssignSubmissionsFormatted({ assingid: assignId }, siteId);
}
/**
* Convenience helper function to get stored submissions formatted.
*
* @param conditions Query conditions.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with submissions.
*/
protected async getAssignSubmissionsFormatted(
conditions: SQLiteDBRecordValues = {},
siteId?: string,
): Promise<AddonModAssignSubmissionsDBRecordFormatted[]> {
const db = await CoreSites.instance.getSiteDb(siteId);
const submissions: AddonModAssignSubmissionsDBRecord[] = await db.getRecords(SUBMISSIONS_TABLE, conditions);
// Parse the plugin data.
return submissions.map((submission) => ({
assignid: submission.assignid,
userid: submission.userid,
courseid: submission.courseid,
plugindata: CoreTextUtils.instance.parseJSON<AddonModAssignSavePluginData>(submission.plugindata, {}),
onlinetimemodified: submission.onlinetimemodified,
timecreated: submission.timecreated,
timemodified: submission.timemodified,
submitted: submission.submitted,
submissionstatement: submission.submissionstatement,
}));
}
/**
* Get all the stored submissions grades from all the assignments.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with submissions grades.
*/
protected async getAllSubmissionsGrade(siteId?: string): Promise<AddonModAssignSubmissionsGradingDBRecordFormatted[]> {
return this.getAssignSubmissionsGradeFormatted(undefined, siteId);
}
/**
* Get all the stored submissions grades for a certain assignment.
*
* @param assignId Assignment ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with submissions grades.
*/
async getAssignSubmissionsGrade(
assignId: number,
siteId?: string,
): Promise<AddonModAssignSubmissionsGradingDBRecordFormatted[]> {
return this.getAssignSubmissionsGradeFormatted({ assingid: assignId }, siteId);
}
/**
* Convenience helper function to get stored submissions grading formatted.
*
* @param conditions Query conditions.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with submissions grades.
*/
protected async getAssignSubmissionsGradeFormatted(
conditions: SQLiteDBRecordValues = {},
siteId?: string,
): Promise<AddonModAssignSubmissionsGradingDBRecordFormatted[]> {
const db = await CoreSites.instance.getSiteDb(siteId);
const submissions: AddonModAssignSubmissionsGradingDBRecord[] = await db.getRecords(SUBMISSIONS_GRADES_TABLE, conditions);
// Parse the plugin data and outcomes.
return submissions.map((submission) => ({
assignid: submission.assignid,
userid: submission.userid,
courseid: submission.courseid,
grade: submission.grade,
attemptnumber: submission.attemptnumber,
addattempt: submission.addattempt,
workflowstate: submission.workflowstate,
applytoall: submission.applytoall,
outcomes: CoreTextUtils.instance.parseJSON<AddonModAssignOutcomes>(submission.outcomes, {}),
plugindata: CoreTextUtils.instance.parseJSON<AddonModAssignSavePluginData>(submission.plugindata, {}),
timemodified: submission.timemodified,
}));
}
/**
* Get a stored submission.
*
* @param assignId Assignment ID.
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with submission.
*/
async getSubmission(assignId: number, userId?: number, siteId?: string): Promise<AddonModAssignSubmissionsDBRecordFormatted> {
userId = userId || CoreSites.instance.getCurrentSiteUserId();
const submissions = await this.getAssignSubmissionsFormatted({ assignid: assignId, userid: userId }, siteId);
if (submissions.length) {
return submissions[0];
}
throw new CoreError('No records found.');
}
/**
* Get the path to the folder where to store files for an offline submission.
*
* @param assignId Assignment ID.
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the path.
*/
async getSubmissionFolder(assignId: number, userId?: number, siteId?: string): Promise<string> {
const site = await CoreSites.instance.getSite(siteId);
userId = userId || site.getUserId();
const siteFolderPath = CoreFile.instance.getSiteFolder(site.getId());
const submissionFolderPath = 'offlineassign/' + assignId + '/' + userId;
return CoreTextUtils.instance.concatenatePaths(siteFolderPath, submissionFolderPath);
}
/**
* Get a stored submission grade.
* Submission grades are not identified using attempt number so it can retrieve the feedback for a previous attempt.
*
* @param assignId Assignment ID.
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with submission grade.
*/
async getSubmissionGrade(
assignId: number,
userId?: number,
siteId?: string,
): Promise<AddonModAssignSubmissionsGradingDBRecordFormatted> {
userId = userId || CoreSites.instance.getCurrentSiteUserId();
const submissions = await this.getAssignSubmissionsGradeFormatted({ assignid: assignId, userid: userId }, siteId);
if (submissions.length) {
return submissions[0];
}
throw new CoreError('No records found.');
}
/**
* Get the path to the folder where to store files for a certain plugin in an offline submission.
*
* @param assignId Assignment ID.
* @param pluginName Name of the plugin. Must be unique (both in submission and feedback plugins).
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the path.
*/
async getSubmissionPluginFolder(assignId: number, pluginName: string, userId?: number, siteId?: string): Promise<string> {
const folderPath = await this.getSubmissionFolder(assignId, userId, siteId);
return CoreTextUtils.instance.concatenatePaths(folderPath, pluginName);
}
/**
* Check if the assignment has something to be synced.
*
* @param assignId Assignment ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with boolean: whether the assignment has something to be synced.
*/
async hasAssignOfflineData(assignId: number, siteId?: string): Promise<boolean> {
const promises:
Promise<AddonModAssignSubmissionsDBRecordFormatted[] | AddonModAssignSubmissionsGradingDBRecordFormatted[]>[] = [];
promises.push(this.getAssignSubmissions(assignId, siteId));
promises.push(this.getAssignSubmissionsGrade(assignId, siteId));
try {
const results = await Promise.all(promises);
return results.some((result) => result.length);
} catch {
// No offline data found.
return false;
}
}
/**
* Mark/Unmark a submission as being submitted.
*
* @param assignId Assignment ID.
* @param courseId Course ID the assign belongs to.
* @param submitted True to mark as submitted, false to mark as not submitted.
* @param acceptStatement True to accept the submission statement, false otherwise.
* @param timemodified The time the submission was last modified in online.
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if marked, rejected if failure.
*/
async markSubmitted(
assignId: number,
courseId: number,
submitted: boolean,
acceptStatement: boolean,
timemodified: number,
userId?: number,
siteId?: string,
): Promise<number> {
const site = await CoreSites.instance.getSite(siteId);
userId = userId || site.getUserId();
let submission: AddonModAssignSubmissionsDBRecord;
try {
const savedSubmission: AddonModAssignSubmissionsDBRecordFormatted =
await this.getSubmission(assignId, userId, site.getId());
submission = Object.assign(savedSubmission, {
plugindata: savedSubmission.plugindata ? JSON.stringify(savedSubmission.plugindata) : '{}',
submitted: submitted ? 1 : 0, // Mark the submission.
submissionstatement: acceptStatement ? 1 : 0, // Mark the submission.
});
} catch {
// No submission, create an empty one.
const now = CoreTimeUtils.instance.timestamp();
submission = {
assignid: assignId,
courseid: courseId,
userid: userId,
onlinetimemodified: timemodified,
timecreated: now,
timemodified: now,
plugindata: '{}',
submitted: submitted ? 1 : 0, // Mark the submission.
submissionstatement: acceptStatement ? 1 : 0, // Mark the submission.
};
}
return await site.getDb().insertRecord(SUBMISSIONS_TABLE, submission);
}
/**
* Save a submission to be sent later.
*
* @param assignId Assignment ID.
* @param courseId Course ID the assign belongs to.
* @param pluginData Data to save.
* @param timemodified The time the submission was last modified in online.
* @param submitted True if submission has been submitted, false otherwise.
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if stored, rejected if failure.
*/
async saveSubmission(
assignId: number,
courseId: number,
pluginData: AddonModAssignSavePluginData,
timemodified: number,
submitted: boolean,
userId?: number,
siteId?: string,
): Promise<number> {
const site = await CoreSites.instance.getSite(siteId);
userId = userId || site.getUserId();
const now = CoreTimeUtils.instance.timestamp();
const entry: AddonModAssignSubmissionsDBRecord = {
assignid: assignId,
courseid: courseId,
plugindata: pluginData ? JSON.stringify(pluginData) : '{}',
userid: userId,
submitted: submitted ? 1 : 0,
timecreated: now,
timemodified: now,
onlinetimemodified: timemodified,
};
return await site.getDb().insertRecord(SUBMISSIONS_TABLE, entry);
}
/**
* Save a grading to be sent later.
*
* @param assignId Assign ID.
* @param userId User ID.
* @param courseId Course ID the assign belongs to.
* @param grade Grade to submit.
* @param attemptNumber Number of the attempt being graded.
* @param addAttempt Admit the user to attempt again.
* @param workflowState Next workflow State.
* @param applyToAll If it's a team submission, whether the grade applies to all group members.
* @param outcomes Object including all outcomes values. If empty, any of them will be sent.
* @param pluginData Plugin data to save.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if stored, rejected if failure.
*/
async submitGradingForm(
assignId: number,
userId: number,
courseId: number,
grade: number,
attemptNumber: number,
addAttempt: boolean,
workflowState: string,
applyToAll: boolean,
outcomes: AddonModAssignOutcomes,
pluginData: AddonModAssignSavePluginData,
siteId?: string,
): Promise<number> {
const site = await CoreSites.instance.getSite(siteId);
const now = CoreTimeUtils.instance.timestamp();
const entry: AddonModAssignSubmissionsGradingDBRecord = {
assignid: assignId,
userid: userId,
courseid: courseId,
grade: grade,
attemptnumber: attemptNumber,
addattempt: addAttempt ? 1 : 0,
workflowstate: workflowState,
applytoall: applyToAll ? 1 : 0,
outcomes: outcomes ? JSON.stringify(outcomes) : '{}',
plugindata: pluginData ? JSON.stringify(pluginData) : '{}',
timemodified: now,
};
return await site.getDb().insertRecord(SUBMISSIONS_GRADES_TABLE, entry);
}
}
export const AddonModAssignOffline = makeSingleton(AddonModAssignOfflineProvider);
export type AddonModAssignSubmissionsDBRecordFormatted = Omit<AddonModAssignSubmissionsDBRecord, 'plugindata'> & {
plugindata: AddonModAssignSavePluginData;
};
export type AddonModAssignSubmissionsGradingDBRecordFormatted =
Omit<AddonModAssignSubmissionsGradingDBRecord, 'plugindata'|'outcomes'> & {
plugindata: AddonModAssignSavePluginData;
outcomes: AddonModAssignOutcomes;
};

View File

@ -0,0 +1,572 @@
// (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 { Injectable } from '@angular/core';
import { CoreEvents, CoreEventSiteData } from '@singletons/events';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreSyncBlockedError } from '@classes/base-sync';
import {
AddonModAssignProvider,
AddonModAssignAssign,
AddonModAssignSubmission,
AddonModAssign,
AddonModAssignGetSubmissionStatusWSResponse,
AddonModAssignSubmissionStatusOptions,
} from './assign';
import { makeSingleton, Translate } from '@singletons';
import { CoreCourse } from '@features/course/services/course';
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
import {
AddonModAssignOffline,
AddonModAssignSubmissionsDBRecordFormatted,
AddonModAssignSubmissionsGradingDBRecordFormatted,
} from './assign-offline';
import { CoreSync } from '@services/sync';
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreUtils } from '@services/utils/utils';
import { CoreApp } from '@services/app';
import { CoreTextUtils } from '@services/utils/text';
import { CoreNetworkError } from '@classes/errors/network-error';
import { CoreGradesFormattedItem, CoreGradesFormattedRow, CoreGradesHelper } from '@features/grades/services/grades-helper';
import { AddonModAssignSubmissionDelegate } from './submission-delegate';
import { AddonModAssignFeedbackDelegate } from './feedback-delegate';
/**
* Service to sync assigns.
*/
@Injectable({ providedIn: 'root' })
export class AddonModAssignSyncProvider extends CoreCourseActivitySyncBaseProvider<AddonModAssignSyncResult> {
static readonly AUTO_SYNCED = 'addon_mod_assign_autom_synced';
static readonly MANUAL_SYNCED = 'addon_mod_assign_manual_synced';
protected componentTranslate: string;
constructor() {
super('AddonModLessonSyncProvider');
this.componentTranslate = CoreCourse.instance.translateModuleName('assign');
}
/**
* Get the sync ID for a certain user grade.
*
* @param assignId Assign ID.
* @param userId User the grade belongs to.
* @return Sync ID.
*/
getGradeSyncId(assignId: number, userId: number): string {
return 'assignGrade#' + assignId + '#' + userId;
}
/**
* Convenience function to get scale selected option.
*
* @param options Possible options.
* @param selected Selected option to search.
* @return Index of the selected option.
*/
protected getSelectedScaleId(options: string, selected: string): number {
let optionsList = options.split(',');
optionsList = optionsList.map((value) => value.trim());
optionsList.unshift('');
const index = options.indexOf(selected) || 0;
if (index < 0) {
return 0;
}
return index;
}
/**
* Check if an assignment has data to synchronize.
*
* @param assignId Assign ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with boolean: whether it has data to sync.
*/
hasDataToSync(assignId: number, siteId?: string): Promise<boolean> {
return AddonModAssignOffline.instance.hasAssignOfflineData(assignId, siteId);
}
/**
* Try to synchronize all the assignments in a certain site or in all sites.
*
* @param siteId Site ID to sync. If not defined, sync all sites.
* @param force Wether to force sync not depending on last execution.
* @return Promise resolved if sync is successful, rejected if sync fails.
*/
syncAllAssignments(siteId?: string, force?: boolean): Promise<void> {
return this.syncOnSites('all assignments', this.syncAllAssignmentsFunc.bind(this, !!force), siteId);
}
/**
* Sync all assignments on a site.
*
* @param force Wether to force sync not depending on last execution.
* @param siteId Site ID to sync. If not defined, sync all sites.
* @param Promise resolved if sync is successful, rejected if sync fails.
*/
protected async syncAllAssignmentsFunc(force: boolean, siteId: string): Promise<void> {
// Get all assignments that have offline data.
const assignIds = await AddonModAssignOffline.instance.getAllAssigns(siteId);
// Try to sync all assignments.
await Promise.all(assignIds.map(async (assignId) => {
const result = force
? await this.syncAssign(assignId, siteId)
: await this.syncAssignIfNeeded(assignId, siteId);
if (result?.updated) {
CoreEvents.trigger<AddonModAssignAutoSyncData>(AddonModAssignSyncProvider.AUTO_SYNCED, {
assignId: assignId,
warnings: result.warnings,
gradesBlocked: result.gradesBlocked,
}, siteId);
}
}));
}
/**
* Sync an assignment only if a certain time has passed since the last time.
*
* @param assignId Assign ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the assign is synced or it doesn't need to be synced.
*/
async syncAssignIfNeeded(assignId: number, siteId?: string): Promise<AddonModAssignSyncResult | undefined> {
const needed = await this.isSyncNeeded(assignId, siteId);
if (needed) {
return this.syncAssign(assignId, siteId);
}
}
/**
* Try to synchronize an assign.
*
* @param assignId Assign ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved in success.
*/
async syncAssign(assignId: number, siteId?: string): Promise<AddonModAssignSyncResult> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
this.componentTranslate = this.componentTranslate || CoreCourse.instance.translateModuleName('assign');
if (this.isSyncing(assignId, siteId)) {
// There's already a sync ongoing for this assign, return the promise.
return this.getOngoingSync(assignId, siteId)!;
}
// Verify that assign isn't blocked.
if (CoreSync.instance.isBlocked(AddonModAssignProvider.COMPONENT, assignId, siteId)) {
this.logger.debug('Cannot sync assign ' + assignId + ' because it is blocked.');
throw new CoreSyncBlockedError(Translate.instance.instant('core.errorsyncblocked', { $a: this.componentTranslate }));
}
this.logger.debug('Try to sync assign ' + assignId + ' in site ' + siteId);
const syncPromise = this.performSyncAssign(assignId, siteId);
return this.addOngoingSync(assignId, syncPromise, siteId);
}
/**
* Perform the assign submission.
*
* @param assignId Assign ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved in success.
*/
protected async performSyncAssign(assignId: number, siteId: string): Promise<AddonModAssignSyncResult> {
// Sync offline logs.
await CoreUtils.instance.ignoreErrors(
CoreCourseLogHelper.instance.syncActivity(AddonModAssignProvider.COMPONENT, assignId, siteId),
);
const result: AddonModAssignSyncResult = {
warnings: [],
updated: false,
gradesBlocked: [],
};
// Load offline data and sync offline logs.
const [submissions, grades] = await Promise.all([
this.getOfflineSubmissions(assignId, siteId),
this.getOfflineGrades(assignId, siteId),
]);
if (!submissions.length && !grades.length) {
// Nothing to sync.
await CoreUtils.instance.ignoreErrors(this.setSyncTime(assignId, siteId));
return result;
}
if (!CoreApp.instance.isOnline()) {
// Cannot sync in offline.
throw new CoreNetworkError();
}
const courseId = submissions.length > 0 ? submissions[0].courseid : grades[0].courseid;
const assign = await AddonModAssign.instance.getAssignmentById(courseId, assignId, { siteId });
let promises: Promise<void>[] = [];
promises = promises.concat(submissions.map(async (submission) => {
await this.syncSubmission(assign, submission, result.warnings, siteId);
result.updated = true;
return;
}));
promises = promises.concat(grades.map(async (grade) => {
try {
await this.syncSubmissionGrade(assign, grade, result.warnings, courseId, siteId);
result.updated = true;
} catch (error) {
if (error instanceof CoreSyncBlockedError) {
// Grade blocked, but allow finish the sync.
result.gradesBlocked.push(grade.userid);
} else {
throw error;
}
}
}));
await CoreUtils.instance.allPromises(promises);
if (result.updated) {
// Data has been sent to server. Now invalidate the WS calls.
await CoreUtils.instance.ignoreErrors(AddonModAssign.instance.invalidateContent(assign.cmid, courseId, siteId));
}
// Sync finished, set sync time.
await CoreUtils.instance.ignoreErrors(this.setSyncTime(assignId, siteId));
// All done, return the result.
return result;
}
/**
* Get offline grades to be sent.
*
* @param assignId Assign ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise with grades.
*/
protected async getOfflineGrades(
assignId: number,
siteId: string,
): Promise<AddonModAssignSubmissionsGradingDBRecordFormatted[]> {
// If no offline data found, return empty array.
return CoreUtils.instance.ignoreErrors(AddonModAssignOffline.instance.getAssignSubmissionsGrade(assignId, siteId), []);
}
/**
* Get offline submissions to be sent.
*
* @param assignId Assign ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise with submissions.
*/
protected async getOfflineSubmissions(
assignId: number,
siteId: string,
): Promise<AddonModAssignSubmissionsDBRecordFormatted[]> {
// If no offline data found, return empty array.
return CoreUtils.instance.ignoreErrors(AddonModAssignOffline.instance.getAssignSubmissions(assignId, siteId), []);
}
/**
* Synchronize a submission.
*
* @param assign Assignment.
* @param offlineData Submission offline data.
* @param warnings List of warnings.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if success, rejected otherwise.
*/
protected async syncSubmission(
assign: AddonModAssignAssign,
offlineData: AddonModAssignSubmissionsDBRecordFormatted,
warnings: string[],
siteId: string,
): Promise<void> {
const userId = offlineData.userid;
const pluginData = {};
const options: AddonModAssignSubmissionStatusOptions = {
userId,
cmId: assign.cmid,
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
siteId,
};
const status = await AddonModAssign.instance.getSubmissionStatus(assign.id, options);
const submission = AddonModAssign.instance.getSubmissionObjectFromAttempt(assign, status.lastattempt);
if (submission && submission.timemodified != offlineData.onlinetimemodified) {
// The submission was modified in Moodle, discard the submission.
this.addOfflineDataDeletedWarning(
warnings,
this.componentTranslate,
assign.name,
Translate.instance.instant('addon.mod_assign.warningsubmissionmodified'),
);
return this.deleteSubmissionData(assign, offlineData, submission, siteId);
}
try {
if (submission?.plugins) {
// Prepare plugins data.
await Promise.all(submission.plugins.map((plugin) =>
AddonModAssignSubmissionDelegate.instance.preparePluginSyncData(
assign,
submission,
plugin,
offlineData,
pluginData,
siteId,
)));
}
// Now save the submission.
if (Object.keys(pluginData).length > 0) {
await AddonModAssign.instance.saveSubmissionOnline(assign.id, pluginData, siteId);
}
if (assign.submissiondrafts && offlineData.submitted) {
// The user submitted the assign manually. Submit it for grading.
await AddonModAssign.instance.submitForGradingOnline(assign.id, !!offlineData.submissionstatement, siteId);
}
// Submission data sent, update cached data. No need to block the user for this.
AddonModAssign.instance.getSubmissionStatus(assign.id, options);
} catch (error) {
if (!error || !CoreUtils.instance.isWebServiceError(error)) {
// Local error, reject.
throw error;
}
// A WebService has thrown an error, this means it cannot be submitted. Discard the submission.
this.addOfflineDataDeletedWarning(
warnings,
this.componentTranslate,
assign.name,
CoreTextUtils.instance.getErrorMessageFromError(error) || '',
);
}
// Delete the offline data.
await this.deleteSubmissionData(assign, offlineData, submission, siteId);
}
/**
* Delete the submission offline data (not grades).
*
* @param assign Assign.
* @param submission Submission.
* @param offlineData Offline data.
* @param siteId Site ID.
* @return Promise resolved when done.
*/
protected async deleteSubmissionData(
assign: AddonModAssignAssign,
offlineData: AddonModAssignSubmissionsDBRecordFormatted,
submission?: AddonModAssignSubmission,
siteId?: string,
): Promise<void> {
// Delete the offline data.
await AddonModAssignOffline.instance.deleteSubmission(assign.id, offlineData.userid, siteId);
if (submission?.plugins){
// Delete plugins data.
await Promise.all(submission.plugins.map((plugin) =>
AddonModAssignSubmissionDelegate.instance.deletePluginOfflineData(
assign,
submission,
plugin,
offlineData,
siteId,
)));
}
}
/**
* Synchronize a submission grade.
*
* @param assign Assignment.
* @param offlineData Submission grade offline data.
* @param warnings List of warnings.
* @param courseId Course Id.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved if success, rejected otherwise.
*/
protected async syncSubmissionGrade(
assign: AddonModAssignAssign,
offlineData: AddonModAssignSubmissionsGradingDBRecordFormatted,
warnings: string[],
courseId: number,
siteId: string,
): Promise<void> {
const userId = offlineData.userid;
const syncId = this.getGradeSyncId(assign.id, userId);
const options: AddonModAssignSubmissionStatusOptions = {
userId,
cmId: assign.cmid,
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
siteId,
};
// Check if this grade sync is blocked.
if (CoreSync.instance.isBlocked(AddonModAssignProvider.COMPONENT, syncId, siteId)) {
this.logger.error(`Cannot sync grade for assign ${assign.id} and user ${userId} because it is blocked.!!!!`);
throw new CoreSyncBlockedError(Translate.instance.instant(
'core.errorsyncblocked',
{ $a: Translate.instance.instant('addon.mod_assign.syncblockedusercomponent') },
));
}
const status = await AddonModAssign.instance.getSubmissionStatus(assign.id, options);
const timemodified = (status.feedback && (status.feedback.gradeddate || status.feedback.grade.timemodified)) || 0;
if (timemodified > offlineData.timemodified) {
// The submission grade was modified in Moodle, discard it.
this.addOfflineDataDeletedWarning(
warnings,
this.componentTranslate,
assign.name,
Translate.instance.instant('addon.mod_assign.warningsubmissiongrademodified'),
);
return AddonModAssignOffline.instance.deleteSubmissionGrade(assign.id, userId, siteId);
}
// If grade has been modified from gradebook, do not use offline.
const grades: CoreGradesFormattedItem[] | CoreGradesFormattedRow[] =
await CoreGradesHelper.instance.getGradeModuleItems(courseId, assign.cmid, userId, undefined, siteId, true);
const gradeInfo = await CoreCourse.instance.getModuleBasicGradeInfo(assign.cmid, siteId);
// Override offline grade and outcomes based on the gradebook data.
grades.forEach((grade: CoreGradesFormattedItem | CoreGradesFormattedRow) => {
if ('gradedategraded' in grade && (grade.gradedategraded || 0) >= offlineData.timemodified) {
if (!grade.outcomeid && !grade.scaleid) {
if (gradeInfo && gradeInfo.scale) {
offlineData.grade = this.getSelectedScaleId(gradeInfo.scale, grade.grade || '');
} else {
offlineData.grade = parseFloat(grade.grade || '') || undefined;
}
} else if (gradeInfo && grade.outcomeid && AddonModAssign.instance.isOutcomesEditEnabled() && gradeInfo.outcomes) {
gradeInfo.outcomes.forEach((outcome, index) => {
if (outcome.scale && grade.itemnumber == index) {
offlineData.outcomes[grade.itemnumber] = this.getSelectedScaleId(
outcome.scale,
grade.grade || '',
);
}
});
}
}
});
try {
// Now submit the grade.
await AddonModAssign.instance.submitGradingFormOnline(
assign.id,
userId,
offlineData.grade,
offlineData.attemptnumber,
!!offlineData.addattempt,
offlineData.workflowstate,
!!offlineData.applytoall,
offlineData.outcomes,
offlineData.plugindata,
siteId,
);
// Grades sent. Discard grades drafts.
let promises: Promise<void | AddonModAssignGetSubmissionStatusWSResponse>[] = [];
if (status.feedback && status.feedback.plugins) {
promises = status.feedback.plugins.map((plugin) =>
AddonModAssignFeedbackDelegate.instance.discardPluginFeedbackData(assign.id, userId, plugin, siteId));
}
// Update cached data.
promises.push(AddonModAssign.instance.getSubmissionStatus(assign.id, options));
await CoreUtils.instance.allPromises(promises);
} catch (error) {
if (!error || !CoreUtils.instance.isWebServiceError(error)) {
// Local error, reject.
throw error;
}
// A WebService has thrown an error, this means it cannot be submitted. Discard the submission.
this.addOfflineDataDeletedWarning(
warnings,
this.componentTranslate,
assign.name,
CoreTextUtils.instance.getErrorMessageFromError(error) || '',
);
}
// Delete the offline data.
await AddonModAssignOffline.instance.deleteSubmissionGrade(assign.id, userId, siteId);
}
}
export const AddonModAssignSync = makeSingleton(AddonModAssignSyncProvider);
/**
* Data returned by a assign sync.
*/
export type AddonModAssignSyncResult = {
warnings: string[]; // List of warnings.
updated: boolean; // Whether some data was sent to the server or offline data was updated.
courseId?: number; // Course the assign belongs to (if known).
gradesBlocked: number[]; // Whether some grade couldn't be synced because it was blocked. UserId fields of the blocked grade.
};
/**
* Data passed to AUTO_SYNCED event.
*/
export type AddonModAssignAutoSyncData = CoreEventSiteData & {
assignId: number;
warnings: string[];
gradesBlocked: number[]; // Whether some grade couldn't be synced because it was blocked. UserId fields of the blocked grade.
};
/**
* Data passed to MANUAL_SYNCED event.
*/
export type AddonModAssignManualSyncData = AddonModAssignAutoSyncData & {
context: string;
submitId?: number;
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,150 @@
// (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 { CoreSiteSchema } from '@services/sites';
/**
* Database variables for AddonModAssignOfflineProvider.
*/export const SUBMISSIONS_TABLE = 'addon_mod_assign_submissions';
export const SUBMISSIONS_GRADES_TABLE = 'addon_mod_assign_submissions_grading';
export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
name: 'AddonModAssignOfflineProvider',
version: 1,
tables: [
{
name: SUBMISSIONS_TABLE,
columns: [
{
name: 'assignid',
type: 'INTEGER',
},
{
name: 'courseid',
type: 'INTEGER',
},
{
name: 'userid',
type: 'INTEGER',
},
{
name: 'plugindata',
type: 'TEXT',
},
{
name: 'onlinetimemodified',
type: 'INTEGER',
},
{
name: 'timecreated',
type: 'INTEGER',
},
{
name: 'timemodified',
type: 'INTEGER',
},
{
name: 'submitted',
type: 'INTEGER',
},
{
name: 'submissionstatement',
type: 'INTEGER',
},
],
primaryKeys: ['assignid', 'userid'],
},
{
name: SUBMISSIONS_GRADES_TABLE,
columns: [
{
name: 'assignid',
type: 'INTEGER',
},
{
name: 'courseid',
type: 'INTEGER',
},
{
name: 'userid',
type: 'INTEGER',
},
{
name: 'grade',
type: 'REAL',
},
{
name: 'attemptnumber',
type: 'INTEGER',
},
{
name: 'addattempt',
type: 'INTEGER',
},
{
name: 'workflowstate',
type: 'TEXT',
},
{
name: 'applytoall',
type: 'INTEGER',
},
{
name: 'outcomes',
type: 'TEXT',
},
{
name: 'plugindata',
type: 'TEXT',
},
{
name: 'timemodified',
type: 'INTEGER',
},
],
primaryKeys: ['assignid', 'userid'],
},
],
};
/**
* Data about assign submissions to sync.
*/
export type AddonModAssignSubmissionsDBRecord = {
assignid: number; // Primary key.
userid: number; // Primary key.
courseid: number;
plugindata: string;
onlinetimemodified: number;
timecreated: number;
timemodified: number;
submitted: number;
submissionstatement?: number;
};
/**
* Data about assign submission grades to sync.
*/
export type AddonModAssignSubmissionsGradingDBRecord = {
assignid: number; // Primary key.
userid: number; // Primary key.
courseid: number;
grade?: number; // Real.
attemptnumber: number;
addattempt: number;
workflowstate: string;
applytoall: number;
outcomes: string;
plugindata: string;
timemodified: number;
};

View File

@ -0,0 +1,374 @@
// (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 { Injectable } from '@angular/core';
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
import { AddonModAssignDefaultFeedbackHandler } from './handlers/default-feedback';
import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin } from './assign';
import { makeSingleton } from '@singletons';
import { CoreWSExternalFile } from '@services/ws';
/**
* Interface that all feedback handlers must implement.
*/
export interface AddonModAssignFeedbackHandler extends CoreDelegateHandler {
/**
* Name of the type of feedback the handler supports. E.g. 'file'.
*/
type: string;
/**
* Discard the draft data of the feedback plugin.
*
* @param assignId The assignment ID.
* @param userId User ID.
* @param siteId Site ID. If not defined, current site.
* @return If the function is async, it should return a Promise resolved when done.
*/
discardDraft?(assignId: number, userId: number, siteId?: string): void | Promise<any>;
/**
* Return the Component to use to display the plugin data.
* It's recommended to return the class of the component, but you can also return an instance of the component.
*
* @param plugin The plugin object.
* @return The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent?(plugin: AddonModAssignPlugin): any | Promise<any>;
/**
* Return the draft saved data of the feedback plugin.
*
* @param assignId The assignment ID.
* @param userId User ID.
* @param siteId Site ID. If not defined, current site.
* @return Data (or promise resolved with the data).
*/
getDraft?(assignId: number, userId: number, siteId?: string): any | Promise<any>;
/**
* Get files used by this plugin.
* The files returned by this function will be prefetched when the user prefetches the assign.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param siteId Site ID. If not defined, current site.
* @return The files (or promise resolved with the files).
*/
getPluginFiles?(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
siteId?: string,
): CoreWSExternalFile[] | Promise<CoreWSExternalFile[]>;
/**
* Get a readable name to use for the plugin.
*
* @param plugin The plugin object.
* @return The plugin name.
*/
getPluginName?(plugin: AddonModAssignPlugin): string;
/**
* Check if the feedback data has changed for this plugin.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param inputData Data entered by the user for the feedback.
* @param userId User ID of the submission.
* @return Boolean (or promise resolved with boolean): whether the data has changed.
*/
hasDataChanged?(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: any,
userId: number,
): boolean | Promise<boolean>;
/**
* Check whether the plugin has draft data stored.
*
* @param assignId The assignment ID.
* @param userId User ID.
* @param siteId Site ID. If not defined, current site.
* @return Boolean or promise resolved with boolean: whether the plugin has draft data.
*/
hasDraftData?(assignId: number, userId: number, siteId?: string): boolean | Promise<boolean>;
/**
* Prefetch any required data for the plugin.
* This should NOT prefetch files. Files to be prefetched should be returned by the getPluginFiles function.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
prefetch?(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
siteId?: string,
): Promise<any>;
/**
* Prepare and add to pluginData the data to send to the server based on the draft data saved.
*
* @param assignId The assignment ID.
* @param userId User ID.
* @param plugin The plugin object.
* @param pluginData Object where to store the data to send.
* @param siteId Site ID. If not defined, current site.
* @return If the function is async, it should return a Promise resolved when done.
*/
prepareFeedbackData?(
assignId: number,
userId: number,
plugin: AddonModAssignPlugin,
pluginData: any,
siteId?: string,
): void | Promise<any>;
/**
* Save draft data of the feedback plugin.
*
* @param assignId The assignment ID.
* @param userId User ID.
* @param plugin The plugin object.
* @param data The data to save.
* @param siteId Site ID. If not defined, current site.
* @return If the function is async, it should return a Promise resolved when done.
*/
saveDraft?(assignId: number, userId: number, plugin: AddonModAssignPlugin, data: any, siteId?: string): void | Promise<any>;
}
/**
* Delegate to register plugins for assign feedback.
*/
@Injectable({ providedIn: 'root' })
export class AddonModAssignFeedbackDelegateService extends CoreDelegate<AddonModAssignFeedbackHandler> {
protected handlerNameProperty = 'type';
constructor(
protected defaultHandler: AddonModAssignDefaultFeedbackHandler,
) {
super('AddonModAssignFeedbackDelegate', true);
}
/**
* Discard the draft data of the feedback plugin.
*
* @param assignId The assignment ID.
* @param userId User ID.
* @param plugin The plugin object.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async discardPluginFeedbackData(
assignId: number,
userId: number,
plugin: AddonModAssignPlugin,
siteId?: string,
): Promise<any | undefined> {
return await this.executeFunctionOnEnabled(plugin.type, 'discardDraft', [assignId, userId, siteId]);
}
/**
* Get the component to use for a certain feedback plugin.
*
* @param plugin The plugin object.
* @return Promise resolved with the component to use, undefined if not found.
*/
async getComponentForPlugin(plugin: AddonModAssignPlugin): Promise<any | undefined> {
return await this.executeFunctionOnEnabled(plugin.type, 'getComponent', [plugin]);
}
/**
* Return the draft saved data of the feedback plugin.
*
* @param assignId The assignment ID.
* @param userId User ID.
* @param plugin The plugin object.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the draft data.
*/
async getPluginDraftData(
assignId: number,
userId: number,
plugin: AddonModAssignPlugin,
siteId?: string,
): Promise<any | undefined> {
return await this.executeFunctionOnEnabled(plugin.type, 'getDraft', [assignId, userId, siteId]);
}
/**
* Get files used by this plugin.
* The files returned by this function will be prefetched when the user prefetches the assign.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the files.
*/
async getPluginFiles(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
siteId?: string,
): Promise<CoreWSExternalFile[]> {
const files: CoreWSExternalFile[] | undefined =
await this.executeFunctionOnEnabled(plugin.type, 'getPluginFiles', [assign, submission, plugin, siteId]);
return files || [];
}
/**
* Get a readable name to use for a certain feedback plugin.
*
* @param plugin Plugin to get the name for.
* @return Human readable name.
*/
getPluginName(plugin: AddonModAssignPlugin): string | undefined {
return this.executeFunctionOnEnabled(plugin.type, 'getPluginName', [plugin]);
}
/**
* Check if the feedback data has changed for a certain plugin.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param inputData Data entered by the user for the feedback.
* @param userId User ID of the submission.
* @return Promise resolved with true if data has changed, resolved with false otherwise.
*/
async hasPluginDataChanged(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: any,
userId: number,
): Promise<boolean | undefined> {
return await this.executeFunctionOnEnabled(
plugin.type,
'hasDataChanged',
[assign, submission, plugin, inputData, userId],
);
}
/**
* Check whether the plugin has draft data stored.
*
* @param assignId The assignment ID.
* @param userId User ID.
* @param plugin The plugin object.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with true if it has draft data.
*/
async hasPluginDraftData(
assignId: number,
userId: number,
plugin: AddonModAssignPlugin,
siteId?: string,
): Promise<boolean | undefined> {
return await this.executeFunctionOnEnabled(plugin.type, 'hasDraftData', [assignId, userId, siteId]);
}
/**
* Check if a feedback plugin is supported.
*
* @param pluginType Type of the plugin.
* @return Whether it's supported.
*/
isPluginSupported(pluginType: string): boolean {
return this.hasHandler(pluginType, true);
}
/**
* Prefetch any required data for a feedback plugin.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async prefetch(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
siteId?: string,
): Promise<any> {
return await this.executeFunctionOnEnabled(plugin.type, 'prefetch', [assign, submission, plugin, siteId]);
}
/**
* Prepare and add to pluginData the data to submit for a certain feedback plugin.
*
* @param assignId The assignment ID.
* @param userId User ID.
* @param plugin The plugin object.
* @param pluginData Object where to store the data to send.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when data has been gathered.
*/
async preparePluginFeedbackData(
assignId: number,
userId: number,
plugin: AddonModAssignPlugin,
pluginData: any,
siteId?: string,
): Promise<any> {
return await this.executeFunctionOnEnabled(
plugin.type,
'prepareFeedbackData',
[assignId, userId, plugin, pluginData, siteId],
);
}
/**
* Save draft data of the feedback plugin.
*
* @param assignId The assignment ID.
* @param userId User ID.
* @param plugin The plugin object.
* @param inputData Data to save.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when data has been saved.
*/
async saveFeedbackDraft(
assignId: number,
userId: number,
plugin: AddonModAssignPlugin,
inputData: any,
siteId?: string,
): Promise<any> {
return await this.executeFunctionOnEnabled(
plugin.type,
'saveDraft',
[assignId, userId, plugin, inputData, siteId],
);
}
}
export const AddonModAssignFeedbackDelegate = makeSingleton(AddonModAssignFeedbackDelegateService);

View File

@ -0,0 +1,146 @@
// (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 { Injectable } from '@angular/core';
import { Translate } from '@singletons';
import { AddonModAssignPlugin } from '../assign';
import { AddonModAssignFeedbackHandler } from '../feedback-delegate';
/**
* Default handler used when a feedback plugin doesn't have a specific implementation.
*/
@Injectable({ providedIn: 'root' })
export class AddonModAssignDefaultFeedbackHandler implements AddonModAssignFeedbackHandler {
name = 'AddonModAssignDefaultFeedbackHandler';
type = 'default';
/**
* Discard the draft data of the feedback plugin.
*
* @return If the function is async, it should return a Promise resolved when done.
*/
discardDraft(): void {
// Nothing to do.
}
/**
* Return the Component to use to display the plugin data.
* It's recommended to return the class of the component, but you can also return an instance of the component.
*
* @return The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(): void {
// Nothing to do.
}
/**
* Return the draft saved data of the feedback plugin.
*
* @return Data (or promise resolved with the data).
*/
getDraft(): void {
// Nothing to do.
}
/**
* Get files used by this plugin.
* The files returned by this function will be prefetched when the user prefetches the assign.
*
* @return The files (or promise resolved with the files).
*/
getPluginFiles(): any[] {
return [];
}
/**
* Get a readable name to use for the plugin.
*
* @param plugin The plugin object.
* @return The plugin name.
*/
getPluginName(plugin: AddonModAssignPlugin): string {
// Check if there's a translated string for the plugin.
const translationId = 'addon.mod_assign_feedback_' + plugin.type + '.pluginname';
const translation = Translate.instance.instant(translationId);
if (translationId != translation) {
// Translation found, use it.
return translation;
}
// Fallback to WS string.
if (plugin.name) {
return plugin.name;
}
return '';
}
/**
* Check if the feedback data has changed for this plugin.
*
* @return Boolean (or promise resolved with boolean): whether the data has changed.
*/
hasDataChanged(): boolean {
return false;
}
/**
* Check whether the plugin has draft data stored.
*
* @return Boolean or promise resolved with boolean: whether the plugin has draft data.
*/
hasDraftData(): boolean {
return false;
}
/**
* Whether or not the handler is enabled on a site level.
*
* @return True or promise resolved with true if enabled.
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* Prefetch any required data for the plugin.
* This should NOT prefetch files. Files to be prefetched should be returned by the getPluginFiles function.
*
* @return Promise resolved when done.
*/
async prefetch(): Promise<any> {
return;
}
/**
* Prepare and add to pluginData the data to send to the server based on the draft data saved.
*
* @return If the function is async, it should return a Promise resolved when done.
*/
prepareFeedbackData(): void {
// Nothing to do.
}
/**
* Save draft data of the feedback plugin.
*
* @return If the function is async, it should return a Promise resolved when done.
*/
saveDraft(): void {
// Nothing to do.
}
}

View File

@ -0,0 +1,210 @@
// (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 { Injectable } from '@angular/core';
import { Translate } from '@singletons';
import { AddonModAssignPlugin } from '../assign';
import { AddonModAssignSubmissionHandler } from '../submission-delegate';
/**
* Default handler used when a submission plugin doesn't have a specific implementation.
*/
@Injectable({ providedIn: 'root' })
export class AddonModAssignDefaultSubmissionHandler implements AddonModAssignSubmissionHandler {
name = 'AddonModAssignBaseSubmissionHandler';
type = 'base';
/**
* Whether the plugin can be edited in offline for existing submissions. In general, this should return false if the
* plugin uses Moodle filters. The reason is that the app only prefetches filtered data, and the user should edit
* unfiltered data.
*
* @return Boolean or promise resolved with boolean: whether it can be edited in offline.
*/
canEditOffline(): boolean | Promise<boolean> {
return false;
}
/**
* Check if a plugin has no data.
*
* @return Whether the plugin is empty.
*/
isEmpty(): boolean {
return true;
}
/**
* Should clear temporary data for a cancelled submission.
*/
clearTmpData(): void {
// Nothing to do.
}
/**
* This function will be called when the user wants to create a new submission based on the previous one.
* It should add to pluginData the data to send to server based in the data in plugin (previous attempt).
*
* @return If the function is async, it should return a Promise resolved when done.
*/
copySubmissionData(): void {
// Nothing to do.
}
/**
* Delete any stored data for the plugin and submission.
*
* @return If the function is async, it should return a Promise resolved when done.
*/
deleteOfflineData(): void {
// Nothing to do.
}
/**
* Return the Component to use to display the plugin data, either in read or in edit mode.
* It's recommended to return the class of the component, but you can also return an instance of the component.
*
* @return The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(): void {
// Nothing to do.
}
/**
* Get files used by this plugin.
* The files returned by this function will be prefetched when the user prefetches the assign.
*
* @return The files (or promise resolved with the files).
*/
getPluginFiles(): any[] {
return [];
}
/**
* Get a readable name to use for the plugin.
*
* @param plugin The plugin object.
* @return The plugin name.
*/
getPluginName(plugin: AddonModAssignPlugin): string {
// Check if there's a translated string for the plugin.
const translationId = 'addon.mod_assign_submission_' + plugin.type + '.pluginname';
const translation = Translate.instance.instant(translationId);
if (translationId != translation) {
// Translation found, use it.
return translation;
}
// Fallback to WS string.
if (plugin.name) {
return plugin.name;
}
return '';
}
/**
* Get the size of data (in bytes) this plugin will send to copy a previous submission.
*
* @param assign The assignment.
* @param plugin The plugin object.
* @return The size (or promise resolved with size).
*/
getSizeForCopy(): number {
return 0;
}
/**
* Get the size of data (in bytes) this plugin will send to add or edit a submission.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @return The size (or promise resolved with size).
*/
getSizeForEdit(): number {
return 0;
}
/**
* Check if the submission data has changed for this plugin.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param inputData Data entered by the user for the submission.
* @return Boolean (or promise resolved with boolean): whether the data has changed.
*/
hasDataChanged(): boolean {
return false;
}
/**
* Whether or not the handler is enabled on a site level.
*
* @return True or promise resolved with true if enabled.
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* Whether or not the handler is enabled for edit on a site level.
*
* @return Whether or not the handler is enabled for edit on a site level.
*/
isEnabledForEdit(): boolean {
return false;
}
/**
* Prefetch any required data for the plugin.
* This should NOT prefetch files. Files to be prefetched should be returned by the getPluginFiles function.
*
* @return Promise resolved when done.
*/
async prefetch(): Promise<any> {
return;
}
/**
* Prepare and add to pluginData the data to send to the server based on the input data.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param inputData Data entered by the user for the submission.
* @param pluginData Object where to store the data to send.
* @param offline Whether the user is editing in offline.
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return If the function is async, it should return a Promise resolved when done.
*/
prepareSubmissionData(): void {
// Nothing to do.
}
/**
* Prepare and add to pluginData the data to send to the server based on the offline data stored.
* This will be used when performing a synchronization.
*
* @return If the function is async, it should return a Promise resolved when done.
*/
prepareSyncData(): void {
// Nothing to do.
}
}

View File

@ -0,0 +1,32 @@
// (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 { Injectable } from '@angular/core';
import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler';
import { makeSingleton } from '@singletons';
/**
* Handler to treat links to assign index page.
*/
@Injectable({ providedIn: 'root' })
export class AddonModAssignIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler {
name = 'AddonModAssignIndexLinkHandler';
constructor() {
super('AddonModAssign', 'assign');
}
}
export const AddonModAssignIndexLinkHandler = makeSingleton(AddonModAssignIndexLinkHandlerService);

View File

@ -0,0 +1,32 @@
// (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 { Injectable } from '@angular/core';
import { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler';
import { makeSingleton } from '@singletons';
/**
* Handler to treat links to assign list page.
*/
@Injectable({ providedIn: 'root' })
export class AddonModAssignListLinkHandlerService extends CoreContentLinksModuleListHandler {
name = 'AddonModAssignListLinkHandler';
constructor() {
super('AddonModAssign', 'assign');
}
}
export const AddonModAssignListLinkHandler = makeSingleton(AddonModAssignListLinkHandlerService);

View File

@ -0,0 +1,94 @@
// (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 { CoreConstants } from '@/core/constants';
import { Injectable, Type } from '@angular/core';
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
import { AddonModAssignIndexComponent } from '../../components/index';
import { makeSingleton } from '@singletons';
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { AddonModAssign } from '../assign';
/**
* Handler to support assign modules.
*/
@Injectable({ providedIn: 'root' })
export class AddonModAssignModuleHandlerService implements CoreCourseModuleHandler {
static readonly PAGE_NAME = 'mod_assign';
name = 'AddonModAssign';
modName = 'assign';
supportedFeatures = {
[CoreConstants.FEATURE_GROUPS]: true,
[CoreConstants.FEATURE_GROUPINGS]: true,
[CoreConstants.FEATURE_MOD_INTRO]: true,
[CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true,
[CoreConstants.FEATURE_COMPLETION_HAS_RULES]: true,
[CoreConstants.FEATURE_GRADE_HAS_GRADE]: true,
[CoreConstants.FEATURE_GRADE_OUTCOMES]: true,
[CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: true,
[CoreConstants.FEATURE_ADVANCED_GRADING]: true,
[CoreConstants.FEATURE_PLAGIARISM]: true,
[CoreConstants.FEATURE_COMMENT]: true,
};
/**
* Check if the handler is enabled on a site level.
*
* @return Whether or not the handler is enabled on a site level.
*/
async isEnabled(): Promise<boolean> {
return AddonModAssign.instance.isPluginEnabled();
}
/**
* Get the data required to display the module in the course contents view.
*
* @param module The module object.
* @return Data to render the module.
*/
getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData {
return {
icon: CoreCourse.instance.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined),
title: module.name,
class: 'addon-mod_assign-handler',
showDownloadButton: true,
action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void {
options = options || {};
options.params = options.params || {};
Object.assign(options.params, { module });
const routeParams = '/' + courseId + '/' + module.id;
CoreNavigator.instance.navigateToSitePath(AddonModAssignModuleHandlerService.PAGE_NAME + routeParams, options);
},
};
}
/**
* Get the component to render the module. This is needed to support singleactivity course format.
* The component returned must implement CoreCourseModuleMainComponent.
*
* @return The component to use, undefined if not found.
*/
async getMainComponent(): Promise<Type<unknown> | undefined> {
return AddonModAssignIndexComponent;
}
}
export const AddonModAssignModuleHandler = makeSingleton(AddonModAssignModuleHandlerService);

View File

@ -0,0 +1,531 @@
// (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 { Injectable } from '@angular/core';
import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites';
import { makeSingleton } from '@singletons';
import {
AddonModAssign,
AddonModAssignAssign,
AddonModAssignProvider,
AddonModAssignSubmission,
AddonModAssignSubmissionStatusOptions,
} from '../assign';
import { AddonModAssignSubmissionDelegate } from '../submission-delegate';
import { AddonModAssignFeedbackDelegate } from '../feedback-delegate';
import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler';
import { CoreCourse, CoreCourseAnyModuleData, CoreCourseCommonModWSOptions } from '@features/course/services/course';
import { CoreWSExternalFile } from '@services/ws';
import { AddonModAssignHelper, AddonModAssignSubmissionFormatted } from '../assign-helper';
import { CoreCourseHelper } from '@features/course/services/course-helper';
import { CoreUtils } from '@services/utils/utils';
import { CoreFilepool } from '@services/filepool';
import { CoreGroups } from '@services/groups';
import { AddonModAssignSync, AddonModAssignSyncResult } from '../assign-sync';
import { CoreUser } from '@features/user/services/user';
import { CoreGradesHelper } from '@features/grades/services/grades-helper';
/**
* Handler to prefetch assigns.
*/
@Injectable({ providedIn: 'root' })
export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase {
name = 'AddonModAssign';
modName = 'assign';
component = AddonModAssignProvider.COMPONENT;
updatesNames = /^configuration$|^.*files$|^submissions$|^grades$|^gradeitems$|^outcomes$|^comments$/;
/**
* Check if a certain module can use core_course_check_updates to check if it has updates.
* If not defined, it will assume all modules can be checked.
* The modules that return false will always be shown as outdated when they're downloaded.
*
* @param module Module.
* @param courseId Course ID the module belongs to.
* @return Whether the module can use check_updates. The promise should never be rejected.
*/
async canUseCheckUpdates(module: CoreCourseAnyModuleData, courseId: number): Promise<boolean> {
// Teachers cannot use the WS because it doesn't check student submissions.
try {
const assign = await AddonModAssign.instance.getAssignment(courseId, module.id);
const data = await AddonModAssign.instance.getSubmissions(assign.id, { cmId: module.id });
if (data.canviewsubmissions) {
return false;
}
// Check if the user can view their own submission.
await AddonModAssign.instance.getSubmissionStatus(assign.id, { cmId: module.id });
return true;
} catch {
return false;
}
}
/**
* Get list of files. If not defined, we'll assume they're in module.contents.
*
* @param module Module.
* @param courseId Course ID the module belongs to.
* @return Promise resolved with the list of files.
*/
async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreWSExternalFile[]> {
const siteId = CoreSites.instance.getCurrentSiteId();
try {
const assign = await AddonModAssign.instance.getAssignment(courseId, module.id, { siteId });
// Get intro files and attachments.
let files = assign.introattachments || [];
files = files.concat(this.getIntroFilesFromInstance(module, assign));
// Now get the files in the submissions.
const submissionData = await AddonModAssign.instance.getSubmissions(assign.id, { cmId: module.id, siteId });
if (submissionData.canviewsubmissions) {
// Teacher, get all submissions.
const submissions =
await AddonModAssignHelper.instance.getSubmissionsUserData(assign, submissionData.submissions, 0, { siteId });
// Get all the files in the submissions.
const promises = submissions.map((submission) =>
this.getSubmissionFiles(assign, submission.submitid!, !!submission.blindid, siteId).then((submissionFiles) => {
files = files.concat(submissionFiles);
return;
}).catch((error) => {
if (error && error.errorcode == 'nopermission') {
// The user does not have persmission to view this submission, ignore it.
return;
}
throw error;
}));
await Promise.all(promises);
} else {
// Student, get only his/her submissions.
const userId = CoreSites.instance.getCurrentSiteUserId();
const blindMarking = !!assign.blindmarking && !assign.revealidentities;
const submissionFiles = await this.getSubmissionFiles(assign, userId, blindMarking, siteId);
files = files.concat(submissionFiles);
}
return files;
} catch {
// Error getting data, return empty list.
return [];
}
}
/**
* Get submission files.
*
* @param assign Assign.
* @param submitId User ID of the submission to get.
* @param blindMarking True if blind marking, false otherwise.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with array of files.
*/
protected async getSubmissionFiles(
assign: AddonModAssignAssign,
submitId: number,
blindMarking: boolean,
siteId?: string,
): Promise<CoreWSExternalFile[]> {
const submissionStatus = await AddonModAssign.instance.getSubmissionStatusWithRetry(assign, {
userId: submitId,
isBlind: blindMarking,
siteId,
});
const userSubmission = AddonModAssign.instance.getSubmissionObjectFromAttempt(assign, submissionStatus.lastattempt);
if (!submissionStatus.lastattempt || !userSubmission) {
return [];
}
const promises: Promise<CoreWSExternalFile[]>[] = [];
if (userSubmission.plugins) {
// Add submission plugin files.
userSubmission.plugins.forEach((plugin) => {
promises.push(AddonModAssignSubmissionDelegate.instance.getPluginFiles(assign, userSubmission, plugin, siteId));
});
}
if (submissionStatus.feedback && submissionStatus.feedback.plugins) {
// Add feedback plugin files.
submissionStatus.feedback.plugins.forEach((plugin) => {
promises.push(AddonModAssignFeedbackDelegate.instance.getPluginFiles(assign, userSubmission, plugin, siteId));
});
}
const filesLists = await Promise.all(promises);
return [].concat.apply([], filesLists);
}
/**
* Invalidate the prefetched content.
*
* @param moduleId The module ID.
* @param courseId The course ID the module belongs to.
* @return Promise resolved when the data is invalidated.
*/
async invalidateContent(moduleId: number, courseId: number): Promise<void> {
await AddonModAssign.instance.invalidateContent(moduleId, courseId);
}
/**
* Invalidate WS calls needed to determine module status.
*
* @param module Module.
* @param courseId Course ID the module belongs to.
* @return Promise resolved when invalidated.
*/
async invalidateModule(module: CoreCourseAnyModuleData): Promise<void> {
return CoreCourse.instance.invalidateModule(module.id);
}
/**
* Whether or not the handler is enabled on a site level.
*
* @return A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
*/
async isEnabled(): Promise<boolean> {
return AddonModAssign.instance.isPluginEnabled();
}
/**
* Prefetch a module.
*
* @param module Module.
* @param courseId Course ID the module belongs to.
* @return Promise resolved when done.
*/
prefetch(module: CoreCourseAnyModuleData, courseId?: number): Promise<void> {
return this.prefetchPackage(module, courseId, this.prefetchAssign.bind(this, module, courseId));
}
/**
* Prefetch an assignment.
*
* @param module Module.
* @param courseId Course ID the module belongs to.
* @return Promise resolved when done.
*/
protected async prefetchAssign(module: CoreCourseAnyModuleData, courseId?: number): Promise<void> {
const userId = CoreSites.instance.getCurrentSiteUserId();
courseId = courseId || module.course || CoreSites.instance.getCurrentSiteHomeId();
const siteId = CoreSites.instance.getCurrentSiteId();
const options: CoreSitesCommonWSOptions = {
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
siteId,
};
const modOptions: CoreCourseCommonModWSOptions = {
cmId: module.id,
...options,
};
// Get assignment to retrieve all its submissions.
const assign = await AddonModAssign.instance.getAssignment(courseId, module.id, options);
const promises: Promise<any>[] = [];
const blindMarking = assign.blindmarking && !assign.revealidentities;
if (blindMarking) {
promises.push(
CoreUtils.instance.ignoreErrors(AddonModAssign.instance.getAssignmentUserMappings(assign.id, -1, modOptions)),
);
}
promises.push(this.prefetchSubmissions(assign, courseId, module.id, userId, siteId));
promises.push(CoreCourseHelper.instance.getModuleCourseIdByInstance(assign.id, 'assign', siteId));
// Download intro files and attachments. Do not call getFiles because it'd call some WS twice.
let files = assign.introattachments || [];
files = files.concat(this.getIntroFilesFromInstance(module, assign));
promises.push(CoreFilepool.instance.addFilesToQueue(siteId, files, this.component, module.id));
await Promise.all(promises);
}
/**
* Prefetch assign submissions.
*
* @param assign Assign.
* @param courseId Course ID.
* @param moduleId Module ID.
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when prefetched, rejected otherwise.
*/
protected async prefetchSubmissions(
assign: AddonModAssignAssign,
courseId: number,
moduleId: number,
userId: number,
siteId: string,
): Promise<void> {
const modOptions: CoreCourseCommonModWSOptions = {
cmId: moduleId,
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
siteId,
};
// Get submissions.
const submissions = await AddonModAssign.instance.getSubmissions(assign.id, modOptions);
const promises: Promise<any>[] = [];
promises.push(this.prefetchParticipantSubmissions(
assign,
submissions.canviewsubmissions,
submissions.submissions,
moduleId,
courseId,
userId,
siteId,
));
// Prefetch own submission, we need to do this for teachers too so the response with error is cached.
promises.push(
this.prefetchSubmission(
assign,
courseId,
moduleId,
{
userId,
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
siteId,
},
true,
),
);
await Promise.all(promises);
}
protected async prefetchParticipantSubmissions(
assign: AddonModAssignAssign,
canviewsubmissions: boolean,
submissions: AddonModAssignSubmission[] = [],
moduleId: number,
courseId: number,
userId: number,
siteId: string,
): Promise<void> {
const options: CoreSitesCommonWSOptions = {
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
siteId,
};
const modOptions: CoreCourseCommonModWSOptions = {
cmId: moduleId,
...options,
};
// Always prefetch groupInfo.
const groupInfo = await CoreGroups.instance.getActivityGroupInfo(assign.cmid, false, undefined, siteId);
if (!canviewsubmissions) {
return;
}
// Teacher, prefetch all submissions.
if (!groupInfo.groups || groupInfo.groups.length == 0) {
groupInfo.groups = [{ id: 0, name: '' }];
}
const promises = groupInfo.groups.map((group) =>
AddonModAssignHelper.instance.getSubmissionsUserData(assign, submissions, group.id, options)
.then((submissions: AddonModAssignSubmissionFormatted[]) => {
const subPromises: Promise<any>[] = submissions.map((submission) => {
const submissionOptions = {
userId: submission.submitid,
groupId: group.id,
isBlind: !!submission.blindid,
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
siteId,
};
return this.prefetchSubmission(assign, courseId, moduleId, submissionOptions, true);
});
if (!assign.markingworkflow) {
// Get assignment grades only if workflow is not enabled to check grading date.
subPromises.push(AddonModAssign.instance.getAssignmentGrades(assign.id, modOptions));
}
// Prefetch the submission of the current user even if it does not exist, this will be create it.
if (!submissions || !submissions.find((subm: AddonModAssignSubmissionFormatted) => subm.submitid == userId)) {
const submissionOptions = {
userId,
groupId: group.id,
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
siteId,
};
subPromises.push(this.prefetchSubmission(assign, courseId, moduleId, submissionOptions));
}
return Promise.all(subPromises);
}).then(async () => {
// Participiants already fetched, we don't need to ignore cache now.
const participants = await AddonModAssignHelper.instance.getParticipants(assign, group.id, { siteId });
// Fail silently (Moodle < 3.2).
await CoreUtils.instance.ignoreErrors(
CoreUser.instance.prefetchUserAvatars(participants, 'profileimageurl', siteId),
);
return;
}));
await Promise.all(promises);
}
/**
* Prefetch a submission.
*
* @param assign Assign.
* @param courseId Course ID.
* @param moduleId Module ID.
* @param options Other options, see getSubmissionStatusWithRetry.
* @param resolveOnNoPermission If true, will avoid throwing if a nopermission error is raised.
* @return Promise resolved when prefetched, rejected otherwise.
*/
protected async prefetchSubmission(
assign: AddonModAssignAssign,
courseId: number,
moduleId: number,
options: AddonModAssignSubmissionStatusOptions = {},
resolveOnNoPermission = false,
): Promise<void> {
const submission = await AddonModAssign.instance.getSubmissionStatusWithRetry(assign, options);
const siteId = options.siteId!;
const userId = options.userId;
try {
const promises: Promise<any>[] = [];
const blindMarking = !!assign.blindmarking && !assign.revealidentities;
let userIds: number[] = [];
const userSubmission = AddonModAssign.instance.getSubmissionObjectFromAttempt(assign, submission.lastattempt);
if (submission.lastattempt) {
// Get IDs of the members who need to submit.
if (!blindMarking && submission.lastattempt.submissiongroupmemberswhoneedtosubmit) {
userIds = userIds.concat(submission.lastattempt.submissiongroupmemberswhoneedtosubmit);
}
if (userSubmission && userSubmission.id) {
// Prefetch submission plugins data.
if (userSubmission.plugins) {
userSubmission.plugins.forEach((plugin) => {
// Prefetch the plugin WS data.
promises.push(
AddonModAssignSubmissionDelegate.instance.prefetch(assign, userSubmission, plugin, siteId),
);
// Prefetch the plugin files.
promises.push(
AddonModAssignSubmissionDelegate.instance.getPluginFiles(assign, userSubmission, plugin, siteId)
.then((files) =>
CoreFilepool.instance.addFilesToQueue(siteId, files, this.component, module.id))
.catch(() => {
// Ignore errors.
}),
);
});
}
// Get ID of the user who did the submission.
if (userSubmission.userid) {
userIds.push(userSubmission.userid);
}
}
}
// Prefetch grade items.
if (userId) {
promises.push(CoreCourse.instance.getModuleBasicGradeInfo(moduleId, siteId).then((gradeInfo) => {
if (gradeInfo) {
promises.push(
CoreGradesHelper.instance.getGradeModuleItems(courseId, moduleId, userId, undefined, siteId, true),
);
}
return;
}));
}
// Prefetch feedback.
if (submission.feedback) {
// Get profile and image of the grader.
if (submission.feedback.grade && submission.feedback.grade.grader > 0) {
userIds.push(submission.feedback.grade.grader);
}
// Prefetch feedback plugins data.
if (submission.feedback.plugins && userSubmission && userSubmission.id) {
submission.feedback.plugins.forEach((plugin) => {
// Prefetch the plugin WS data.
promises.push(AddonModAssignFeedbackDelegate.instance.prefetch(assign, userSubmission, plugin, siteId));
// Prefetch the plugin files.
promises.push(
AddonModAssignFeedbackDelegate.instance.getPluginFiles(assign, userSubmission, plugin, siteId)
.then((files) => CoreFilepool.instance.addFilesToQueue(siteId, files, this.component, module.id))
.catch(() => {
// Ignore errors.
}),
);
});
}
}
// Prefetch user profiles.
promises.push(CoreUser.instance.prefetchProfiles(userIds, courseId, siteId));
await Promise.all(promises);
} catch (error) {
// Ignore if the user can't view their own submission.
if (resolveOnNoPermission && error.errorcode != 'nopermission') {
throw error;
}
}
}
/**
* Sync a module.
*
* @param module Module.
* @param courseId Course ID the module belongs to
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
sync(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise<AddonModAssignSyncResult> {
return AddonModAssignSync.instance.syncAssign(module.instance!, siteId);
}
}
export const AddonModAssignPrefetchHandler = makeSingleton(AddonModAssignPrefetchHandlerService);

View File

@ -0,0 +1,66 @@
// (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 { Injectable } from '@angular/core';
import { CoreCourseHelper } from '@features/course/services/course-helper';
import { CorePushNotificationsClickHandler } from '@features/pushnotifications/services/push-delegate';
import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications';
import { CoreUrlUtils } from '@services/utils/url';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton } from '@singletons';
import { AddonModAssign } from '../assign';
/**
* Handler for assign push notifications clicks.
*/
@Injectable({ providedIn: 'root' })
export class AddonModAssignPushClickHandlerService implements CorePushNotificationsClickHandler {
name = 'AddonModAssignPushClickHandler';
priority = 200;
featureName = 'CoreCourseModuleDelegate_AddonModAssign';
/**
* Check if a notification click is handled by this handler.
*
* @param notification The notification to check.
* @return Whether the notification click is handled by this handler
*/
async handles(notification: NotificationData): Promise<boolean> {
return CoreUtils.instance.isTrueOrOne(notification.notif) && notification.moodlecomponent == 'mod_assign' &&
notification.name == 'assign_notification';
}
/**
* Handle the notification click.
*
* @param notification The notification to check.
* @return Promise resolved when done.
*/
async handleClick(notification: NotificationData): Promise<void> {
const contextUrlParams = CoreUrlUtils.instance.extractUrlParams(notification.contexturl);
const courseId = Number(notification.courseid);
const moduleId = Number(contextUrlParams.id);
await CoreUtils.instance.ignoreErrors(AddonModAssign.instance.invalidateContent(moduleId, courseId, notification.site));
await CoreCourseHelper.instance.navigateToModule(moduleId, notification.site, courseId);
}
}
export const AddonModAssignPushClickHandler = makeSingleton(AddonModAssignPushClickHandlerService);
type NotificationData = CorePushNotificationsNotificationBasicData & {
courseid: number;
contexturl: string;
};

View File

@ -0,0 +1,50 @@
// (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 { Injectable } from '@angular/core';
import { CoreCronHandler } from '@services/cron';
import { makeSingleton } from '@singletons';
import { AddonModAssignSync } from '../assign-sync';
/**
* Synchronization cron handler.
*/
@Injectable({ providedIn: 'root' })
export class AddonModAssignSyncCronHandlerService implements CoreCronHandler {
name = 'AddonModAssignSyncCronHandler';
/**
* Execute the process.
* Receives the ID of the site affected, undefined for all sites.
*
* @param siteId ID of the site affected, undefined for all sites.
* @param force Wether the execution is forced (manual sync).
* @return Promise resolved when done, rejected if failure.
*/
execute(siteId?: string, force?: boolean): Promise<void> {
return AddonModAssignSync.instance.syncAllAssignments(siteId, force);
}
/**
* Get the time between consecutive executions.
*
* @return Time between consecutive executions (in ms).
*/
getInterval(): number {
return AddonModAssignSync.instance.syncInterval;
}
}
export const AddonModAssignSyncCronHandler = makeSingleton(AddonModAssignSyncCronHandlerService);

View File

@ -0,0 +1,565 @@
// (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 { Injectable } from '@angular/core';
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
import { AddonModAssignDefaultSubmissionHandler } from './handlers/default-submission';
import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin } from './assign';
import { makeSingleton } from '@singletons';
import { CoreWSExternalFile } from '@services/ws';
/**
* Interface that all submission handlers must implement.
*/
export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler {
/**
* Name of the type of submission the handler supports. E.g. 'file'.
*/
type: string;
/**
* Whether the plugin can be edited in offline for existing submissions. In general, this should return false if the
* plugin uses Moodle filters. The reason is that the app only prefetches filtered data, and the user should edit
* unfiltered data.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @return Boolean or promise resolved with boolean: whether it can be edited in offline.
*/
canEditOffline?(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
): boolean | Promise<boolean>;
/**
* Check if a plugin has no data.
*
* @param assign The assignment.
* @param plugin The plugin object.
* @return Whether the plugin is empty.
*/
isEmpty?(
assign: AddonModAssignAssign,
plugin: AddonModAssignPlugin,
): boolean;
/**
* Should clear temporary data for a cancelled submission.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param inputData Data entered by the user for the submission.
*/
clearTmpData?(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: any,
): void;
/**
* This function will be called when the user wants to create a new submission based on the previous one.
* It should add to pluginData the data to send to server based in the data in plugin (previous attempt).
*
* @param assign The assignment.
* @param plugin The plugin object.
* @param pluginData Object where to store the data to send.
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return If the function is async, it should return a Promise resolved when done.
*/
copySubmissionData?(
assign: AddonModAssignAssign,
plugin: AddonModAssignPlugin,
pluginData: any,
userId?: number,
siteId?: string,
): void | Promise<void>;
/**
* Delete any stored data for the plugin and submission.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param offlineData Offline data stored.
* @param siteId Site ID. If not defined, current site.
* @return If the function is async, it should return a Promise resolved when done.
*/
deleteOfflineData?(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
offlineData: any,
siteId?: string,
): void | Promise<any>;
/**
* Return the Component to use to display the plugin data, either in read or in edit mode.
* It's recommended to return the class of the component, but you can also return an instance of the component.
*
* @param plugin The plugin object.
* @param edit Whether the user is editing.
* @return The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent?(
plugin: AddonModAssignPlugin,
edit?: boolean,
): any | Promise<any>;
/**
* Get files used by this plugin.
* The files returned by this function will be prefetched when the user prefetches the assign.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param siteId Site ID. If not defined, current site.
* @return The files (or promise resolved with the files).
*/
getPluginFiles?(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
siteId?: string,
): CoreWSExternalFile[] | Promise<CoreWSExternalFile[]>;
/**
* Get a readable name to use for the plugin.
*
* @param plugin The plugin object.
* @return The plugin name.
*/
getPluginName?(plugin: AddonModAssignPlugin): string;
/**
* Get the size of data (in bytes) this plugin will send to copy a previous submission.
*
* @param assign The assignment.
* @param plugin The plugin object.
* @return The size (or promise resolved with size).
*/
getSizeForCopy?(
assign: AddonModAssignAssign,
plugin: AddonModAssignPlugin,
): number | Promise<number>;
/**
* Get the size of data (in bytes) this plugin will send to add or edit a submission.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param inputData Data entered by the user for the submission.
* @return The size (or promise resolved with size).
*/
getSizeForEdit?(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: any,
): number | Promise<number>;
/**
* Check if the submission data has changed for this plugin.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param inputData Data entered by the user for the submission.
* @return Boolean (or promise resolved with boolean): whether the data has changed.
*/
hasDataChanged?(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: any,
): boolean | Promise<boolean>;
/**
* Whether or not the handler is enabled for edit on a site level.
*
* @return Whether or not the handler is enabled for edit on a site level.
*/
isEnabledForEdit?(): boolean | Promise<boolean>;
/**
* Prefetch any required data for the plugin.
* This should NOT prefetch files. Files to be prefetched should be returned by the getPluginFiles function.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
prefetch?(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
siteId?: string,
): Promise<void>;
/**
* Prepare and add to pluginData the data to send to the server based on the input data.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param inputData Data entered by the user for the submission.
* @param pluginData Object where to store the data to send.
* @param offline Whether the user is editing in offline.
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return If the function is async, it should return a Promise resolved when done.
*/
prepareSubmissionData?(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: any,
pluginData: any,
offline?: boolean,
userId?: number,
siteId?: string,
): void | Promise<any>;
/**
* Prepare and add to pluginData the data to send to the server based on the offline data stored.
* This will be used when performing a synchronization.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param offlineData Offline data stored.
* @param pluginData Object where to store the data to send.
* @param siteId Site ID. If not defined, current site.
* @return If the function is async, it should return a Promise resolved when done.
*/
prepareSyncData?(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
offlineData: any,
pluginData: any,
siteId?: string,
): void | Promise<any>;
}
/**
* Delegate to register plugins for assign submission.
*/
@Injectable({ providedIn: 'root' })
export class AddonModAssignSubmissionDelegateService extends CoreDelegate<AddonModAssignSubmissionHandler> {
protected handlerNameProperty = 'type';
constructor(
protected defaultHandler: AddonModAssignDefaultSubmissionHandler,
) {
super('AddonModAssignSubmissionDelegate', true);
}
/**
* Whether the plugin can be edited in offline for existing submissions.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @return Promise resolved with boolean: whether it can be edited in offline.
*/
async canPluginEditOffline(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
): Promise<boolean | undefined> {
return await this.executeFunctionOnEnabled(plugin.type, 'canEditOffline', [assign, submission, plugin]);
}
/**
* Clear some temporary data for a certain plugin because a submission was cancelled.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param inputData Data entered by the user for the submission.
*/
clearTmpData(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: any,
): void {
return this.executeFunctionOnEnabled(plugin.type, 'clearTmpData', [assign, submission, plugin, inputData]);
}
/**
* Copy the data from last submitted attempt to the current submission for a certain plugin.
*
* @param assign The assignment.
* @param plugin The plugin object.
* @param pluginData Object where to store the data to send.
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data has been copied.
*/
async copyPluginSubmissionData(
assign: AddonModAssignAssign,
plugin: AddonModAssignPlugin,
pluginData: any,
userId?: number,
siteId?: string,
): Promise<void | undefined> {
return await this.executeFunctionOnEnabled(
plugin.type,
'copySubmissionData',
[assign, plugin, pluginData, userId, siteId],
);
}
/**
* Delete offline data stored for a certain submission and plugin.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param offlineData Offline data stored.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deletePluginOfflineData(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
offlineData: any,
siteId?: string,
): Promise<any | undefined> {
return await this.executeFunctionOnEnabled(
plugin.type,
'deleteOfflineData',
[assign, submission, plugin, offlineData, siteId],
);
}
/**
* Get the component to use for a certain submission plugin.
*
* @param plugin The plugin object.
* @param edit Whether the user is editing.
* @return Promise resolved with the component to use, undefined if not found.
*/
async getComponentForPlugin(plugin: AddonModAssignPlugin, edit?: boolean): Promise<any | undefined> {
return await this.executeFunctionOnEnabled(plugin.type, 'getComponent', [plugin, edit]);
}
/**
* Get files used by this plugin.
* The files returned by this function will be prefetched when the user prefetches the assign.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the files.
*/
async getPluginFiles(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
siteId?: string,
): Promise<CoreWSExternalFile[]> {
const files: CoreWSExternalFile[] | undefined =
await this.executeFunctionOnEnabled(plugin.type, 'getPluginFiles', [assign, submission, plugin, siteId]);
return files || [];
}
/**
* Get a readable name to use for a certain submission plugin.
*
* @param plugin Plugin to get the name for.
* @return Human readable name.
*/
getPluginName(plugin: AddonModAssignPlugin): string | undefined {
return this.executeFunctionOnEnabled(plugin.type, 'getPluginName', [plugin]);
}
/**
* Get the size of data (in bytes) this plugin will send to copy a previous submission.
*
* @param assign The assignment.
* @param plugin The plugin object.
* @return Promise resolved with size.
*/
async getPluginSizeForCopy(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): Promise<number | undefined> {
return await this.executeFunctionOnEnabled(plugin.type, 'getSizeForCopy', [assign, plugin]);
}
/**
* Get the size of data (in bytes) this plugin will send to add or edit a submission.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param inputData Data entered by the user for the submission.
* @return Promise resolved with size.
*/
async getPluginSizeForEdit(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: any,
): Promise<number | undefined> {
return await this.executeFunctionOnEnabled(
plugin.type,
'getSizeForEdit',
[assign, submission, plugin, inputData],
);
}
/**
* Check if the submission data has changed for a certain plugin.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param inputData Data entered by the user for the submission.
* @return Promise resolved with true if data has changed, resolved with false otherwise.
*/
async hasPluginDataChanged(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: any,
): Promise<boolean | undefined> {
return await this.executeFunctionOnEnabled(
plugin.type,
'hasDataChanged',
[assign, submission, plugin, inputData],
);
}
/**
* Check if a submission plugin is supported.
*
* @param pluginType Type of the plugin.
* @return Whether it's supported.
*/
isPluginSupported(pluginType: string): boolean {
return this.hasHandler(pluginType, true);
}
/**
* Check if a submission plugin is supported for edit.
*
* @param pluginType Type of the plugin.
* @return Whether it's supported for edit.
*/
async isPluginSupportedForEdit(pluginType: string): Promise<boolean | undefined> {
return await this.executeFunctionOnEnabled(pluginType, 'isEnabledForEdit');
}
/**
* Check if a plugin has no data.
*
* @param assign The assignment.
* @param plugin The plugin object.
* @return Whether the plugin is empty.
*/
isPluginEmpty(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): boolean | undefined {
return this.executeFunctionOnEnabled(plugin.type, 'isEmpty', [assign, plugin]);
}
/**
* Prefetch any required data for a submission plugin.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async prefetch(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
siteId?: string,
): Promise<void> {
return await this.executeFunctionOnEnabled(plugin.type, 'prefetch', [assign, submission, plugin, siteId]);
}
/**
* Prepare and add to pluginData the data to submit for a certain submission plugin.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param inputData Data entered by the user for the submission.
* @param pluginData Object where to store the data to send.
* @param offline Whether the user is editing in offline.
* @param userId User ID. If not defined, site's current user.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when data has been gathered.
*/
async preparePluginSubmissionData(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
inputData: any,
pluginData: any,
offline?: boolean,
userId?: number,
siteId?: string,
): Promise<any | undefined> {
return await this.executeFunctionOnEnabled(
plugin.type,
'prepareSubmissionData',
[assign, submission, plugin, inputData, pluginData, offline, userId, siteId],
);
}
/**
* Prepare and add to pluginData the data to send to server to synchronize an offline submission.
*
* @param assign The assignment.
* @param submission The submission.
* @param plugin The plugin object.
* @param offlineData Offline data stored.
* @param pluginData Object where to store the data to send.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when data has been gathered.
*/
async preparePluginSyncData(
assign: AddonModAssignAssign,
submission: AddonModAssignSubmission,
plugin: AddonModAssignPlugin,
offlineData: any,
pluginData: any,
siteId?: string,
): Promise<any | undefined> {
return this.executeFunctionOnEnabled(
plugin.type,
'prepareSyncData',
[assign, submission, plugin, offlineData, pluginData, siteId],
);
}
}
export class AddonModAssignSubmissionDelegate extends makeSingleton(AddonModAssignSubmissionDelegateService) {}

View File

@ -14,6 +14,7 @@
import { NgModule } from '@angular/core';
import { AddonModAssignModule } from './assign/assign.module';
import { AddonModBookModule } from './book/book.module';
import { AddonModLessonModule } from './lesson/lesson.module';
import { AddonModPageModule } from './page/page.module';
@ -21,6 +22,7 @@ import { AddonModPageModule } from './page/page.module';
@NgModule({
declarations: [],
imports: [
AddonModAssignModule,
AddonModBookModule,
AddonModLessonModule,
AddonModPageModule,

View File

@ -412,7 +412,7 @@ export class CoreGroupsProvider {
* @param groupInfo Group info.
* @return Group ID to use.
*/
validateGroupId(groupId: number, groupInfo: CoreGroupInfo): number {
validateGroupId(groupId = 0, groupInfo: CoreGroupInfo): number {
if (groupId > 0 && groupInfo && groupInfo.groups && groupInfo.groups.length > 0) {
// Check if the group is in the list of groups.
if (groupInfo.groups.some((group) => groupId == group.id)) {

View File

@ -380,6 +380,11 @@ export class CoreNavigatorService {
// IonTabs checks the URL to determine which path to load for deep linking, so we clear the URL.
// @todo this.location.replaceState('');
options = {
preferCurrentTab: true,
...options,
};
path = path.replace(/^(\.|\/main)?\//, '');
const pathRoot = /^[^/]+/.exec(path)?.[0] ?? '';
@ -389,7 +394,7 @@ export class CoreNavigatorService {
false,
);
if (options.preferCurrentTab === false && isMainMenuTab) {
if (!options.preferCurrentTab && isMainMenuTab) {
return this.navigate(`/main/${path}`, options);
}