MOBILE-3636 assign: Implement assignment base
parent
b0cf681ab6
commit
a4eefeb25a
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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};
|
|
@ -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;
|
||||
};
|
|
@ -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
|
@ -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;
|
||||
};
|
|
@ -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);
|
|
@ -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.
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
|
@ -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) {}
|
|
@ -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,
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue