commit
f56861aa17
|
@ -0,0 +1,62 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { CoreCronDelegate } from '@providers/cron';
|
||||
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
|
||||
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
|
||||
import { AddonModAssignProvider } from './providers/assign';
|
||||
import { AddonModAssignOfflineProvider } from './providers/assign-offline';
|
||||
import { AddonModAssignSyncProvider } from './providers/assign-sync';
|
||||
import { AddonModAssignHelperProvider } from './providers/helper';
|
||||
import { AddonModAssignFeedbackDelegate } from './providers/feedback-delegate';
|
||||
import { AddonModAssignSubmissionDelegate } from './providers/submission-delegate';
|
||||
import { AddonModAssignDefaultFeedbackHandler } from './providers/default-feedback-handler';
|
||||
import { AddonModAssignDefaultSubmissionHandler } from './providers/default-submission-handler';
|
||||
import { AddonModAssignModuleHandler } from './providers/module-handler';
|
||||
import { AddonModAssignPrefetchHandler } from './providers/prefetch-handler';
|
||||
import { AddonModAssignSyncCronHandler } from './providers/sync-cron-handler';
|
||||
import { AddonModAssignSubmissionModule } from './submission/submission.module';
|
||||
import { AddonModAssignFeedbackModule } from './feedback/feedback.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
],
|
||||
imports: [
|
||||
AddonModAssignSubmissionModule,
|
||||
AddonModAssignFeedbackModule
|
||||
],
|
||||
providers: [
|
||||
AddonModAssignProvider,
|
||||
AddonModAssignOfflineProvider,
|
||||
AddonModAssignSyncProvider,
|
||||
AddonModAssignHelperProvider,
|
||||
AddonModAssignFeedbackDelegate,
|
||||
AddonModAssignSubmissionDelegate,
|
||||
AddonModAssignDefaultFeedbackHandler,
|
||||
AddonModAssignDefaultSubmissionHandler,
|
||||
AddonModAssignModuleHandler,
|
||||
AddonModAssignPrefetchHandler,
|
||||
AddonModAssignSyncCronHandler
|
||||
]
|
||||
})
|
||||
export class AddonModAssignModule {
|
||||
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModAssignModuleHandler,
|
||||
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModAssignPrefetchHandler,
|
||||
cronDelegate: CoreCronDelegate, syncHandler: AddonModAssignSyncCronHandler) {
|
||||
moduleDelegate.registerHandler(moduleHandler);
|
||||
prefetchDelegate.registerHandler(prefetchHandler);
|
||||
cronDelegate.register(syncHandler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { Input } from '@angular/core';
|
||||
import { ModalController } from 'ionic-angular';
|
||||
|
||||
/**
|
||||
* Base class for component to render a feedback plugin.
|
||||
*/
|
||||
export class AddonModAssignFeedbackPluginComponent {
|
||||
@Input() assign: any; // The assignment.
|
||||
@Input() submission: any; // The submission.
|
||||
@Input() plugin: any; // The plugin object.
|
||||
@Input() userId: number; // The user ID of the submission.
|
||||
@Input() configs: any; // The configs for the plugin.
|
||||
@Input() canEdit: boolean; // Whether the user can edit.
|
||||
@Input() edit: boolean; // Whether the user is editing.
|
||||
|
||||
constructor(protected modalCtrl: ModalController) { }
|
||||
|
||||
/**
|
||||
* Open a modal to edit the feedback plugin.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved with the input data, rejected if cancelled.
|
||||
*/
|
||||
editFeedback(): Promise<any> {
|
||||
if (this.canEdit) {
|
||||
return new Promise((resolve, reject): void => {
|
||||
// Create the navigation modal.
|
||||
const modal = this.modalCtrl.create('AddonModAssignEditFeedbackModalPage', {
|
||||
assign: this.assign,
|
||||
submission: this.submission,
|
||||
plugin: this.plugin,
|
||||
userId: this.userId
|
||||
});
|
||||
|
||||
modal.present();
|
||||
modal.onDidDismiss((data) => {
|
||||
if (typeof data == 'undefined') {
|
||||
reject();
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return Promise.reject(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the data.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
invalidate(): Promise<any> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { Input } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Base class for component to render a submission plugin.
|
||||
*/
|
||||
export class AddonModAssignSubmissionPluginComponent {
|
||||
@Input() assign: any; // The assignment.
|
||||
@Input() submission: any; // The submission.
|
||||
@Input() plugin: any; // The plugin object.
|
||||
@Input() configs: any; // The configs for the plugin.
|
||||
@Input() edit: boolean; // Whether the user is editing.
|
||||
@Input() allowOffline: boolean; // Whether to allow offline.
|
||||
|
||||
constructor() {
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the data.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
invalidate(): Promise<any> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CorePipesModule } from '@pipes/pipes.module';
|
||||
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
|
||||
import { AddonModAssignIndexComponent } from './index/index';
|
||||
import { AddonModAssignSubmissionComponent } from './submission/submission';
|
||||
import { AddonModAssignSubmissionPluginComponent } from './submission-plugin/submission-plugin';
|
||||
import { AddonModAssignFeedbackPluginComponent } from './feedback-plugin/feedback-plugin';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModAssignIndexComponent,
|
||||
AddonModAssignSubmissionComponent,
|
||||
AddonModAssignSubmissionPluginComponent,
|
||||
AddonModAssignFeedbackPluginComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CorePipesModule,
|
||||
CoreCourseComponentsModule
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
exports: [
|
||||
AddonModAssignIndexComponent,
|
||||
AddonModAssignSubmissionComponent,
|
||||
AddonModAssignSubmissionPluginComponent,
|
||||
AddonModAssignFeedbackPluginComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModAssignIndexComponent
|
||||
]
|
||||
})
|
||||
export class AddonModAssignComponentsModule {}
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
<core-dynamic-component [component]="pluginComponent" [data]="data">
|
||||
<!-- This content will be replaced by the component if found. -->
|
||||
<core-loading [hideUntil]="pluginLoaded">
|
||||
<ion-item text-wrap *ngIf="text.length > 0 || files.length > 0">
|
||||
<h2>{{ plugin.name }}</h2>
|
||||
<ion-badge *ngIf="notSupported" color="primary">
|
||||
{{ 'addon.mod_assign.feedbacknotsupported' | translate }}
|
||||
</ion-badge>
|
||||
<p *ngIf="text">
|
||||
<core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="80" [fullOnClick]="true" [fullTitle]="plugin.name" [text]="text"></core-format-text>
|
||||
</p>
|
||||
<core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" [alwaysDownload]="true"></core-file>
|
||||
</ion-item>
|
||||
</core-loading>
|
||||
</core-dynamic-component>
|
|
@ -0,0 +1,104 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, Input, OnInit, Injector, ViewChild } from '@angular/core';
|
||||
import { AddonModAssignProvider } from '../../providers/assign';
|
||||
import { AddonModAssignHelperProvider } from '../../providers/helper';
|
||||
import { AddonModAssignFeedbackDelegate } from '../../providers/feedback-delegate';
|
||||
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
|
||||
|
||||
/**
|
||||
* Component that displays an assignment feedback plugin.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-assign-feedback-plugin',
|
||||
templateUrl: 'feedback-plugin.html',
|
||||
})
|
||||
export class AddonModAssignFeedbackPluginComponent implements OnInit {
|
||||
@ViewChild(CoreDynamicComponent) dynamicComponent: CoreDynamicComponent;
|
||||
|
||||
@Input() assign: any; // The assignment.
|
||||
@Input() submission: any; // The submission.
|
||||
@Input() plugin: any; // The plugin object.
|
||||
@Input() userId: number; // The user ID of the submission.
|
||||
@Input() canEdit: boolean | string; // Whether the user can edit.
|
||||
@Input() edit: boolean | string; // Whether the user is editing.
|
||||
|
||||
pluginComponent: any; // Component to render the plugin.
|
||||
data: any; // Data to pass to the component.
|
||||
|
||||
// Data to render the plugin if it isn't supported.
|
||||
component = AddonModAssignProvider.COMPONENT;
|
||||
text = '';
|
||||
files = [];
|
||||
notSupported: boolean;
|
||||
pluginLoaded: boolean;
|
||||
|
||||
constructor(protected injector: Injector, protected feedbackDelegate: AddonModAssignFeedbackDelegate,
|
||||
protected assignProvider: AddonModAssignProvider, protected assignHelper: AddonModAssignHelperProvider) { }
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
if (!this.plugin) {
|
||||
this.pluginLoaded = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.plugin.name = this.feedbackDelegate.getPluginName(this.plugin);
|
||||
if (!this.plugin.name) {
|
||||
this.pluginLoaded = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.edit = this.edit && this.edit !== 'false';
|
||||
this.canEdit = this.canEdit && this.canEdit !== 'false';
|
||||
|
||||
// Check if the plugin has defined its own component to render itself.
|
||||
this.feedbackDelegate.getComponentForPlugin(this.injector, this.plugin).then((component) => {
|
||||
this.pluginComponent = component;
|
||||
|
||||
if (component) {
|
||||
// Prepare the data to pass to the component.
|
||||
this.data = {
|
||||
assign: this.assign,
|
||||
submission: this.submission,
|
||||
plugin: this.plugin,
|
||||
userId: this.userId,
|
||||
configs: this.assignHelper.getPluginConfig(this.assign, 'assignfeedback', this.plugin.type),
|
||||
edit: this.edit,
|
||||
canEdit: this.canEdit
|
||||
};
|
||||
} else {
|
||||
// Data to render the plugin.
|
||||
this.text = this.assignProvider.getSubmissionPluginText(this.plugin);
|
||||
this.files = this.assignProvider.getSubmissionPluginAttachments(this.plugin);
|
||||
this.notSupported = this.feedbackDelegate.isPluginSupported(this.plugin.type);
|
||||
this.pluginLoaded = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the plugin data.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
invalidate(): Promise<any> {
|
||||
return Promise.resolve(this.dynamicComponent && this.dynamicComponent.callComponentFunction('invalidate', []));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
<!-- Buttons to add to the header. -->
|
||||
<core-navbar-buttons end>
|
||||
<core-context-menu>
|
||||
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></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]="'arrow-forward'"></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()" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="400" [content]="size" [iconDescription]="'cube'" (action)="removeFiles()" [iconAction]="'trash'"></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)">
|
||||
<ion-item text-wrap>
|
||||
<core-format-text [text]="description" [component]="component" [componentId]="componentId" maxHeight="120" (click)="expandDescription($event)"></core-format-text>
|
||||
</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. -->
|
||||
<div *ngIf="hasOffline" class="core-warning-card" icon-start>
|
||||
<ion-icon name="warning"></ion-icon>
|
||||
{{ 'core.hasdatatosync' | translate:{$a: moduleName} }}
|
||||
</div>
|
||||
|
||||
<!-- User can view all submissions (teacher). -->
|
||||
<ion-card *ngIf="assign && canViewSubmissions" class="core-list-align-detail-right">
|
||||
<ion-item text-wrap *ngIf="timeRemaining">
|
||||
<h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2>
|
||||
<p>{{ timeRemaining }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="lateSubmissions">
|
||||
<h2>{{ 'addon.mod_assign.latesubmissions' | translate }}</h2>
|
||||
<p>{{ lateSubmissions }}</p>
|
||||
</ion-item>
|
||||
|
||||
<!-- Summary of all submissions. -->
|
||||
<a ion-item text-wrap *ngIf="summary && summary.participantcount" (click)="goToSubmissionList()">
|
||||
<h2 *ngIf="assign.teamsubmission">{{ 'addon.mod_assign.numberofteams' | translate }}</h2>
|
||||
<h2 *ngIf="!assign.teamsubmission">{{ 'addon.mod_assign.numberofparticipants' | translate }}</h2>
|
||||
<ion-badge item-end *ngIf="showNumbers" color="primary">
|
||||
{{ summary.participantcount }}
|
||||
</ion-badge>
|
||||
</a>
|
||||
|
||||
<!-- Summary of submissions with draft status. -->
|
||||
<a ion-item text-wrap *ngIf="assign.submissiondrafts && summary && summary.submissionsenabled" [attr.detail-none]="(showNumbers && !summary.submissiondraftscount) ? true : null" (click)="goToSubmissionList(submissionStatusDraft, summary.submissiondraftscount)">
|
||||
<h2>{{ 'addon.mod_assign.numberofdraftsubmissions' | translate }}</h2>
|
||||
<ion-badge item-end *ngIf="showNumbers" color="primary">
|
||||
{{ summary.submissiondraftscount }}
|
||||
</ion-badge>
|
||||
</a>
|
||||
|
||||
<!-- Summary of submissions with submitted status. -->
|
||||
<a ion-item text-wrap *ngIf="summary && summary.submissionsenabled" [attr.detail-none]="(showNumbers && !summary.submissionssubmittedcount) ? true : null" (click)="goToSubmissionList(submissionStatusSubmitted, summary.submissionssubmittedcount)">
|
||||
<h2>{{ 'addon.mod_assign.numberofsubmittedassignments' | translate }}</h2>
|
||||
<ion-badge item-end *ngIf="showNumbers" color="primary">
|
||||
{{ summary.submissionssubmittedcount }}
|
||||
</ion-badge>
|
||||
</a>
|
||||
|
||||
<!-- Summary of submissions that need grading. -->
|
||||
<a ion-item text-wrap *ngIf="summary && summary.submissionsenabled && !assign.teamsubmission && showNumbers" [attr.detail-none]="needsGradingAvalaible ? null : true" (click)="goToSubmissionList(needGrading, needsGradingAvalaible)">
|
||||
<h2>{{ 'addon.mod_assign.numberofsubmissionsneedgrading' | translate }}</h2>
|
||||
<ion-badge item-end color="primary">
|
||||
{{ summary.submissionsneedgradingcount }}
|
||||
</ion-badge>
|
||||
</a>
|
||||
|
||||
<!-- Ungrouped users. -->
|
||||
<div *ngIf="assign.teamsubmission && summary && summary.warnofungroupedusers" class="core-info-card" icon-start>
|
||||
<ion-icon name="information"></ion-icon>
|
||||
{{ 'addon.mod_assign.ungroupedusers' | translate }}
|
||||
</div>
|
||||
</ion-card>
|
||||
|
||||
<!-- If it's a student, display his submission. -->
|
||||
<addon-mod-assign-submission *ngIf="loaded && !canViewSubmissions" [courseId]="courseId" [moduleId]="module.id"></addon-mod-assign-submission>
|
||||
|
||||
</core-loading>
|
|
@ -0,0 +1,314 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, Injector, ViewChild } from '@angular/core';
|
||||
import { Content, NavController } from 'ionic-angular';
|
||||
import { CoreGroupsProvider } from '@providers/groups';
|
||||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||
import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
|
||||
import { AddonModAssignProvider } from '../../providers/assign';
|
||||
import { AddonModAssignHelperProvider } from '../../providers/helper';
|
||||
import { AddonModAssignOfflineProvider } from '../../providers/assign-offline';
|
||||
import { AddonModAssignSyncProvider } from '../../providers/assign-sync';
|
||||
import * as moment from 'moment';
|
||||
import { AddonModAssignSubmissionComponent } from '../submission/submission';
|
||||
|
||||
/**
|
||||
* Component that displays an assignment.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-assign-index',
|
||||
templateUrl: 'index.html',
|
||||
})
|
||||
export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityComponent {
|
||||
@ViewChild(AddonModAssignSubmissionComponent) submissionComponent: AddonModAssignSubmissionComponent;
|
||||
|
||||
component = AddonModAssignProvider.COMPONENT;
|
||||
moduleName = 'assign';
|
||||
|
||||
assign: any; // The assign object.
|
||||
canViewSubmissions: boolean; // Whether the user can view all submissions.
|
||||
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: any; // The summary.
|
||||
needsGradingAvalaible: boolean; // Whether we can see the submissions that need grading.
|
||||
|
||||
// Status.
|
||||
submissionStatusSubmitted = AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED;
|
||||
submissionStatusDraft = AddonModAssignProvider.SUBMISSION_STATUS_DRAFT;
|
||||
needGrading = AddonModAssignProvider.NEED_GRADING;
|
||||
|
||||
protected userId: number; // Current user ID.
|
||||
protected syncEventName = AddonModAssignSyncProvider.AUTO_SYNCED;
|
||||
|
||||
// Observers.
|
||||
protected savedObserver;
|
||||
protected submittedObserver;
|
||||
protected gradedObserver;
|
||||
|
||||
constructor(injector: Injector, protected assignProvider: AddonModAssignProvider, @Optional() content: Content,
|
||||
protected assignHelper: AddonModAssignHelperProvider, protected assignOffline: AddonModAssignOfflineProvider,
|
||||
protected syncProvider: AddonModAssignSyncProvider, protected timeUtils: CoreTimeUtilsProvider,
|
||||
protected groupsProvider: CoreGroupsProvider, protected navCtrl: NavController) {
|
||||
super(injector, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit();
|
||||
|
||||
this.userId = this.sitesProvider.getCurrentSiteUserId();
|
||||
|
||||
this.loadContent(false, true).then(() => {
|
||||
this.assignProvider.logView(this.assign.id).then(() => {
|
||||
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
|
||||
}).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
|
||||
if (!this.canViewSubmissions) {
|
||||
// User can only see his submission, log view the user submission.
|
||||
this.assignProvider.logSubmissionView(this.assign.id).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
} else {
|
||||
// User can see all submissions, log grading view.
|
||||
this.assignProvider.logGradingView(this.assign.id).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Listen to events.
|
||||
this.savedObserver = this.eventsProvider.on(AddonModAssignProvider.SUBMISSION_SAVED_EVENT, (data) => {
|
||||
if (this.assign && data.assignmentId == this.assign.id && data.userId == this.userId) {
|
||||
// Assignment submission saved, refresh data.
|
||||
this.showLoadingAndRefresh(true, false);
|
||||
}
|
||||
}, this.siteId);
|
||||
|
||||
this.submittedObserver = this.eventsProvider.on(AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, (data) => {
|
||||
if (this.assign && data.assignmentId == this.assign.id && data.userId == this.userId) {
|
||||
// Assignment submitted, check completion.
|
||||
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
|
||||
|
||||
// Reload data since it can have offline data now.
|
||||
this.showLoadingAndRefresh(true, false);
|
||||
}
|
||||
}, this.siteId);
|
||||
|
||||
this.gradedObserver = this.eventsProvider.on(AddonModAssignProvider.GRADED_EVENT, (data) => {
|
||||
if (this.assign && data.assignmentId == this.assign.id && data.userId == this.userId) {
|
||||
// Assignment graded, refresh data.
|
||||
this.showLoadingAndRefresh(true, false);
|
||||
}
|
||||
}, this.siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand the description.
|
||||
*/
|
||||
expandDescription(ev?: Event): void {
|
||||
ev && ev.preventDefault();
|
||||
ev && ev.stopPropagation();
|
||||
|
||||
if (this.assign && (this.description || this.assign.introattachments)) {
|
||||
this.textUtils.expandText(this.translate.instant('core.description'), this.description, this.component,
|
||||
this.module.id, this.assign.introattachments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get assignment data.
|
||||
*
|
||||
* @param {boolean} [refresh=false] If it's refreshing content.
|
||||
* @param {boolean} [sync=false] If the refresh is needs syncing.
|
||||
* @param {boolean} [showErrors=false] If show errors to the user of hide them.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<any> {
|
||||
|
||||
// Get assignment data.
|
||||
return this.assignProvider.getAssignment(this.courseId, this.module.id).then((assignData) => {
|
||||
this.assign = assignData;
|
||||
|
||||
this.dataRetrieved.emit(this.assign);
|
||||
this.description = this.assign.intro || this.description;
|
||||
|
||||
if (sync) {
|
||||
// Try to synchronize the assign.
|
||||
return this.syncActivity(showErrors).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}
|
||||
}).then(() => {
|
||||
// Check if there's any offline data for this assign.
|
||||
return this.assignOffline.hasAssignOfflineData(this.assign.id);
|
||||
}).then((hasOffline) => {
|
||||
this.hasOffline = hasOffline;
|
||||
|
||||
// Get assignment submissions.
|
||||
return this.assignProvider.getSubmissions(this.assign.id).then((data) => {
|
||||
const time = this.timeUtils.timestamp();
|
||||
|
||||
this.canViewSubmissions = data.canviewsubmissions;
|
||||
|
||||
if (data.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 = this.translate.instant('addon.mod_assign.assignmentisdue');
|
||||
} else {
|
||||
this.timeRemaining = this.timeUtils.formatDuration(this.assign.duedate - time, 3);
|
||||
|
||||
if (this.assign.cutoffdate) {
|
||||
if (this.assign.cutoffdate > time) {
|
||||
const dateFormat = this.translate.instant('core.dfmediumdate');
|
||||
|
||||
this.lateSubmissions = this.translate.instant('addon.mod_assign.latesubmissionsaccepted',
|
||||
{$a: moment(this.assign.cutoffdate * 1000).format(dateFormat)});
|
||||
} else {
|
||||
this.lateSubmissions = this.translate.instant('addon.mod_assign.nomoresubmissionsaccepted');
|
||||
}
|
||||
} else {
|
||||
this.lateSubmissions = '';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.timeRemaining = '';
|
||||
this.lateSubmissions = '';
|
||||
}
|
||||
|
||||
// Check if groupmode is enabled to avoid showing wrong numbers.
|
||||
return this.groupsProvider.activityHasGroups(this.assign.cmid).then((hasGroups) => {
|
||||
this.showNumbers = !hasGroups;
|
||||
|
||||
return this.assignProvider.getSubmissionStatus(this.assign.id).then((response) => {
|
||||
this.summary = response.gradingsummary;
|
||||
|
||||
this.needsGradingAvalaible = response.gradingsummary.submissionsneedgradingcount > 0 &&
|
||||
this.sitesProvider.getCurrentSite().isVersionGreaterEqualThan('3.2');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
// All data obtained, now fill the context menu.
|
||||
this.fillContextMenu(refresh);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to view a list of submissions.
|
||||
*
|
||||
* @param {string} status Status to see.
|
||||
* @param {number} count Number of submissions with the status.
|
||||
*/
|
||||
goToSubmissionList(status: string, count: number): void {
|
||||
if (typeof status == 'undefined') {
|
||||
this.navCtrl.push('AddonModAssignSubmissionListPage', {
|
||||
courseId: this.courseId,
|
||||
moduleId: this.module.id,
|
||||
moduleName: this.moduleName
|
||||
});
|
||||
} else if (count || !this.showNumbers) {
|
||||
this.navCtrl.push('AddonModAssignSubmissionListPage', {
|
||||
status: status,
|
||||
courseId: this.courseId,
|
||||
moduleId: this.module.id,
|
||||
moduleName: this.moduleName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if sync has succeed from result sync data.
|
||||
*
|
||||
* @param {any} result Data returned by the sync function.
|
||||
* @return {boolean} If succeed or not.
|
||||
*/
|
||||
protected hasSyncSucceed(result: any): boolean {
|
||||
if (result.updated) {
|
||||
this.submissionComponent && this.submissionComponent.invalidateAndRefresh();
|
||||
}
|
||||
|
||||
return result.updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the invalidate content function.
|
||||
*
|
||||
* @return {Promise<any>} Resolved when done.
|
||||
*/
|
||||
protected invalidateContent(): Promise<any> {
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.assignProvider.invalidateAssignmentData(this.courseId));
|
||||
|
||||
if (this.assign) {
|
||||
promises.push(this.assignProvider.invalidateAllSubmissionData(this.assign.id));
|
||||
|
||||
if (this.canViewSubmissions) {
|
||||
promises.push(this.assignProvider.invalidateSubmissionStatusData(this.assign.id));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(promises).finally(() => {
|
||||
this.submissionComponent && this.submissionComponent.invalidateAndRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares sync event data with current data to check if refresh content is needed.
|
||||
*
|
||||
* @param {any} syncEventData Data receiven on sync observer.
|
||||
* @return {boolean} True if refresh is needed, false otherwise.
|
||||
*/
|
||||
protected isRefreshSyncNeeded(syncEventData: any): boolean {
|
||||
if (this.assign && syncEventData.assignId == this.assign.id) {
|
||||
if (syncEventData.warnings && syncEventData.warnings.length) {
|
||||
// Show warnings.
|
||||
this.domUtils.showErrorModal(syncEventData.warnings[0]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the sync of the activity.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected sync(): Promise<any> {
|
||||
return this.syncProvider.syncAssign(this.assign.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
super.ngOnDestroy();
|
||||
|
||||
this.savedObserver && this.savedObserver.off();
|
||||
this.submittedObserver && this.submittedObserver.off();
|
||||
this.gradedObserver && this.gradedObserver.off();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
<core-dynamic-component [component]="pluginComponent" [data]="data">
|
||||
<!-- This content will be replaced by the component if found. -->
|
||||
<core-loading [hideUntil]="pluginLoaded">
|
||||
<ion-item text-wrap *ngIf="text.length > 0 || files.length > 0">
|
||||
<h2>{{ plugin.name }}</h2>
|
||||
<ion-badge *ngIf="notSupported" color="primary">
|
||||
{{ 'addon.mod_assign.submissionnotsupported' | translate }}
|
||||
</ion-badge>
|
||||
<p *ngIf="text">
|
||||
<core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="80" [fullOnClick]="true" [fullTitle]="plugin.name" [text]="text"></core-format-text>
|
||||
</p>
|
||||
<core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" [alwaysDownload]="true"></core-file>
|
||||
</ion-item>
|
||||
</core-loading>
|
||||
</core-dynamic-component>
|
|
@ -0,0 +1,98 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, Input, OnInit, Injector, ViewChild } from '@angular/core';
|
||||
import { AddonModAssignProvider } from '../../providers/assign';
|
||||
import { AddonModAssignHelperProvider } from '../../providers/helper';
|
||||
import { AddonModAssignSubmissionDelegate } from '../../providers/submission-delegate';
|
||||
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
|
||||
|
||||
/**
|
||||
* Component that displays an assignment submission plugin.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-assign-submission-plugin',
|
||||
templateUrl: 'submission-plugin.html',
|
||||
})
|
||||
export class AddonModAssignSubmissionPluginComponent implements OnInit {
|
||||
@ViewChild(CoreDynamicComponent) dynamicComponent: CoreDynamicComponent;
|
||||
|
||||
@Input() assign: any; // The assignment.
|
||||
@Input() submission: any; // The submission.
|
||||
@Input() plugin: any; // The plugin object.
|
||||
@Input() edit: boolean | string; // Whether the user is editing.
|
||||
@Input() allowOffline: boolean | string; // Whether to allow offline.
|
||||
|
||||
pluginComponent: any; // Component to render the plugin.
|
||||
data: any; // Data to pass to the component.
|
||||
|
||||
// Data to render the plugin if it isn't supported.
|
||||
component = AddonModAssignProvider.COMPONENT;
|
||||
text = '';
|
||||
files = [];
|
||||
notSupported: boolean;
|
||||
pluginLoaded: boolean;
|
||||
|
||||
constructor(protected injector: Injector, protected submissionDelegate: AddonModAssignSubmissionDelegate,
|
||||
protected assignProvider: AddonModAssignProvider, protected assignHelper: AddonModAssignHelperProvider) { }
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
if (!this.plugin) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.plugin.name = this.submissionDelegate.getPluginName(this.plugin);
|
||||
if (!this.plugin.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.edit = this.edit && this.edit !== 'false';
|
||||
this.allowOffline = this.allowOffline && this.allowOffline !== 'false';
|
||||
|
||||
// Check if the plugin has defined its own component to render itself.
|
||||
this.submissionDelegate.getComponentForPlugin(this.injector, this.plugin, this.edit).then((component) => {
|
||||
this.pluginComponent = component;
|
||||
|
||||
if (component) {
|
||||
// Prepare the data to pass to the component.
|
||||
this.data = {
|
||||
assign: this.assign,
|
||||
submission: this.submission,
|
||||
plugin: this.plugin,
|
||||
configs: this.assignHelper.getPluginConfig(this.assign, 'assignsubmission', this.plugin.type),
|
||||
edit: this.edit,
|
||||
allowOffline: this.allowOffline
|
||||
};
|
||||
} else {
|
||||
// Data to render the plugin.
|
||||
this.text = this.assignProvider.getSubmissionPluginText(this.plugin);
|
||||
this.files = this.assignProvider.getSubmissionPluginAttachments(this.plugin);
|
||||
this.notSupported = this.submissionDelegate.isPluginSupported(this.plugin.type);
|
||||
this.pluginLoaded = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the plugin data.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
invalidate(): Promise<any> {
|
||||
return Promise.resolve(this.dynamicComponent && this.dynamicComponent.callComponentFunction('invalidate', []));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
<core-loading [hideUntil]="loaded">
|
||||
|
||||
<!-- User and status of the submission. -->
|
||||
<a ion-item text-wrap *ngIf="!blindMarking && user" (click)="openUserProfile(submitId)" [title]="user.fullname">
|
||||
<ion-avatar item-start>
|
||||
<img [src]="user.profileimageurl" core-external-content [alt]="'core.pictureof' | translate:{$a: user.fullname}" role="presentation" onError="this.src='assets/img/user-avatar.png'">
|
||||
</ion-avatar>
|
||||
<h2>{{ user.fullname }}</h2>
|
||||
<ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
|
||||
</a>
|
||||
|
||||
<!-- Status of the submission if user is blinded. -->
|
||||
<ion-item text-wrap *ngIf="blindMarking && !user">
|
||||
<h2>{{ 'addon.mod_assign.hiddenuser' | translate }} {{blindId}}</h2>
|
||||
<ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
|
||||
</ion-item>
|
||||
|
||||
<!-- Status of the submission in the rest of cases. -->
|
||||
<ion-item text-wrap *ngIf="(blindMarking && user) || (!blindMarking && !user)">
|
||||
<h2>{{ 'addon.mod_assign.submissionstatus' | translate }}</h2>
|
||||
<ng-container *ngTemplateOutlet="submissionStatus"></ng-container>
|
||||
</ion-item>
|
||||
|
||||
<!-- Tabs: see the submission or grade it. -->
|
||||
<core-tabs [selectedIndex]="selectedTab" [hideUntil]="loaded" parentScrollable="true">
|
||||
<!-- View the submission tab. -->
|
||||
<core-tab [title]="'addon.mod_assign.submission' | translate">
|
||||
<ng-template>
|
||||
<addon-mod-assign-submission-plugin *ngFor="let plugin of submissionPlugins" [assign]="assign" [submission]="userSubmission" [plugin]="plugin"></addon-mod-assign-submission-plugin>
|
||||
|
||||
<!-- Render some data about the submission. -->
|
||||
<ion-item text-wrap *ngIf="userSubmission && userSubmission.status != statusNew && userSubmission.timemodified">
|
||||
<h2>{{ 'addon.mod_assign.timemodified' | translate }}</h2>
|
||||
<p>{{ userSubmission.timemodified * 1000 | coreFormatDate:"dfmediumdate" }}</p>
|
||||
</ion-item>
|
||||
|
||||
<ion-item text-wrap *ngIf="timeRemaining" [ngClass]="[timeRemainingClass]">
|
||||
<h2>{{ 'addon.mod_assign.timeremaining' | translate }}</h2>
|
||||
<p><core-format-text [text]="timeRemaining"></core-format-text></p>
|
||||
</ion-item>
|
||||
|
||||
<ion-item text-wrap *ngIf="fromDate && !isSubmittedForGrading">
|
||||
<p *ngIf="assign.intro" [innerHTML]="'addon.mod_assign.allowsubmissionsfromdatesummary' | translate: {'$a': fromDate}"></p>
|
||||
<p *ngIf="!assign.intro" [innerHTML]="'addon.mod_assign.allowsubmissionsanddescriptionfromdatesummary' | translate: {'$a': fromDate}"></p>
|
||||
</ion-item>
|
||||
|
||||
<ion-item text-wrap *ngIf="assign.duedate && !isSubmittedForGrading">
|
||||
<h2>{{ 'addon.mod_assign.duedate' | translate }}</h2>
|
||||
<p *ngIf="assign.duedate" >{{ assign.duedate * 1000 | coreFormatDate:"dfmediumdate" }}</p>
|
||||
<p *ngIf="!assign.duedate" >{{ 'addon.mod_assign.duedateno' | translate }}</p>
|
||||
</ion-item>
|
||||
|
||||
<ion-item text-wrap *ngIf="assign.duedate && assign.cutoffdate && isSubmittedForGrading">
|
||||
<h2>{{ 'addon.mod_assign.cutoffdate' | translate }}</h2>
|
||||
<p>{{ assign.cutoffdate * 1000 | coreFormatDate:"dfmediumdate" }}</p>
|
||||
</ion-item>
|
||||
|
||||
<ion-item text-wrap *ngIf="assign.duedate && lastAttempt && lastAttempt.extensionduedate && !isSubmittedForGrading">
|
||||
<h2>{{ 'addon.mod_assign.extensionduedate' | translate }}</h2>
|
||||
<p>{{ lastAttempt.extensionduedate * 1000 | coreFormatDate:"dfmediumdate" }}</p>
|
||||
</ion-item>
|
||||
|
||||
<ion-item text-wrap *ngIf="currentAttempt && !isGrading">
|
||||
<h2>{{ 'addon.mod_assign.attemptnumber' | translate }}</h2>
|
||||
<p *ngIf="assign.maxattempts == unlimitedAttempts">{{ 'addon.mod_assign.outof' | translate : {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }}</p>
|
||||
<p *ngIf="assign.maxattempts != unlimitedAttempts">{{ 'addon.mod_assign.outof' | translate : {'$a': {'current': currentAttempt, 'total': assign.maxattempts} } }}</p>
|
||||
</ion-item>
|
||||
|
||||
<!-- Add or edit submission. -->
|
||||
<ion-item text-wrap *ngIf="canEdit">
|
||||
<div *ngIf="!unsupportedEditPlugins.length && !showErrorStatementEdit">
|
||||
<!-- If has offline data, show edit. -->
|
||||
<a ion-button block color="primary" *ngIf="hasOffline" (click)="goToEdit()">{{ 'addon.mod_assign.editsubmission' | translate }}</a>
|
||||
<!-- If no submission or is new, show add submission. -->
|
||||
<a ion-button block color="primary" *ngIf="!hasOffline && (!userSubmission || !userSubmission.status || userSubmission.status == statusNew)" (click)="goToEdit()">{{ 'addon.mod_assign.addsubmission' | translate }}</a>
|
||||
<!-- If reopened, show addfromprevious and addnewattempt. -->
|
||||
<ng-container *ngIf="!hasOffline && userSubmission && userSubmission.status == statusReopened">
|
||||
<a ion-button block color="primary" (click)="copyPrevious()">{{ 'addon.mod_assign.addnewattemptfromprevious' | translate }}</a>
|
||||
<a ion-button block color="primary" (click)="goToEdit()">{{ 'addon.mod_assign.addnewattempt' | translate }}</a>
|
||||
</ng-container>
|
||||
<!-- Else show editsubmission. -->
|
||||
<a ion-button block color="primary" *ngIf="!hasOffline && userSubmission && userSubmission.status && userSubmission.status != statusNew && userSubmission.status != statusReopened" (click)="goToEdit()">{{ 'addon.mod_assign.editsubmission' | translate }}</a>
|
||||
</div>
|
||||
<div *ngIf="unsupportedEditPlugins && unsupportedEditPlugins.length && !showErrorStatementEdit">
|
||||
<p class="core-danger-item">{{ 'addon.mod_assign.erroreditpluginsnotsupported' | translate }}</p>
|
||||
<p class="core-danger-item" *ngFor="let name of unsupportedEditPlugins">{{ name }}</p>
|
||||
</div>
|
||||
<div *ngIf="showErrorStatementEdit">
|
||||
<p class="core-danger-item">{{ 'addon.mod_assign.cannoteditduetostatementsubmission' | translate }}</p>
|
||||
</div>
|
||||
</ion-item>
|
||||
|
||||
<!-- Submit for grading form. -->
|
||||
<div *ngIf="canSubmit">
|
||||
<ion-item text-wrap *ngIf="submissionStatement">
|
||||
<ion-label><core-format-text [text]="submissionStatement"></core-format-text></ion-label>
|
||||
<ion-checkbox item-end name="submissionstatement" [(ngModel)]="submitModel.submissionStatement">
|
||||
</ion-checkbox>
|
||||
</ion-item>
|
||||
<!-- Submit button. -->
|
||||
<ion-item text-wrap *ngIf="!showErrorStatementSubmit">
|
||||
<a ion-button block (click)="submitForGrading(submitModel.submissionStatement)">{{ 'addon.mod_assign.submitassignment' | translate }}</a>
|
||||
<p>{{ 'addon.mod_assign.submitassignment_help' | translate }}</p>
|
||||
</ion-item>
|
||||
<!-- Error because we lack submissions statement. -->
|
||||
<ion-item text-wrap *ngIf="showErrorStatementSubmit">
|
||||
<p class="core-danger-item">{{ 'addon.mod_assign.cannotsubmitduetostatementsubmission' | translate }}</p>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<!-- Team members that need to submit it too. -->
|
||||
<ion-item text-wrap *ngIf="membersToSubmit && membersToSubmit.length > 0">
|
||||
<h2>{{ 'addon.mod_assign.userswhoneedtosubmit' | translate }}</h2>
|
||||
<div *ngFor="let user of membersToSubmit">
|
||||
<a *ngIf="user.fullname" (click)="openUserProfile(user.id)" [title]="user.fullname">
|
||||
<ion-avatar item-start>
|
||||
<img [src]="user.profileimageurl" core-external-content [alt]="'core.pictureof' | translate:{$a: user.fullname}" role="presentation" onError="this.src='assets/img/user-avatar.png'">
|
||||
</ion-avatar>
|
||||
<h2>{{ user.fullname }}</h2>
|
||||
</a>
|
||||
<p *ngIf="!user.fullname">
|
||||
{{ 'addon.mod_assign.hiddenuser' | translate }} <core-format-text [text]="user"></core-format-text>
|
||||
</p>
|
||||
</div>
|
||||
</ion-item>
|
||||
|
||||
<!-- Submission is locked. -->
|
||||
<ion-item text-wrap *ngIf="lastAttempt && lastAttempt.locked">
|
||||
<h2>{{ 'addon.mod_assign.submissionslocked' | translate }}</h2>
|
||||
</ion-item>
|
||||
|
||||
<!-- Editing status. -->
|
||||
<ion-item text-wrap *ngIf="lastAttempt && isSubmittedForGrading && lastAttempt.caneditowner !== undefined" [ngClass]="{submissioneditable: lastAttempt.caneditowner, submissionnoteditable: !lastAttempt.caneditowner}">
|
||||
<h2>{{ 'addon.mod_assign.editingstatus' | translate }}</h2>
|
||||
<p *ngIf="lastAttempt.caneditowner">{{ 'addon.mod_assign.submissioneditable' | translate }}</p>
|
||||
<p *ngIf="!lastAttempt.caneditowner">{{ 'addon.mod_assign.submissionnoteditable' | translate }}</p>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
</core-tab>
|
||||
|
||||
<!-- Grade the submission tab. -->
|
||||
<core-tab [title]="'addon.mod_assign.grade' | translate" *ngIf="feedback || isGrading">
|
||||
<ng-template>
|
||||
<!-- Current grade if method is advanced. -->
|
||||
<ion-item text-wrap *ngIf="feedback.gradefordisplay && (!isGrading || grade.method != 'simple')" class="core-grading-summary">
|
||||
<h2>{{ 'addon.mod_assign.currentgrade' | translate }}</h2>
|
||||
<p><core-format-text [text]="feedback.gradefordisplay"></core-format-text></p>
|
||||
<a ion-button item-end icon-only *ngIf="feedback.advancedgrade" (click)="showAdvancedGrade()">
|
||||
<ion-icon name="search"></ion-icon>
|
||||
</a>
|
||||
</ion-item>
|
||||
|
||||
<!-- Numeric grade. -->
|
||||
<ion-item text-wrap *ngIf="grade.method == 'simple' && !grade.scale">
|
||||
<ion-label stacked>{{ 'addon.mod_assign.gradeoutof' | translate: {$a: gradeInfo.grade} }}</ion-label>
|
||||
<ion-input type="number" [(ngModel)]="grade.grade" min="0" [max]="gradeInfo.grade" [lang]="grade.lang" core-input-errors></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<!-- Grade using a scale. -->
|
||||
<ion-item text-wrap *ngIf="grade.method == 'simple' && grade.scale">
|
||||
<ion-label>{{ 'addon.mod_assign.grade' | translate }}</ion-label>
|
||||
<ion-select [(ngModel)]="grade.grade" interface="popover">
|
||||
<ion-option *ngFor="let grade of grade.scale" [value]="grade.value">{{grade.label}}</ion-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<!-- Outcomes. -->
|
||||
<ion-item text-wrap *ngFor="let outcome of gradeInfo.outcomes">
|
||||
<ion-label>{{ outcome.name }}</ion-label>
|
||||
<ion-select *ngIf="canSaveGrades && outcome.itemNumber" [(ngModel)]="outcome.selectedId" interface="popover">
|
||||
<ion-option *ngFor="let grade of outcome.options" [value]="grade.value">{{grade.label}}</ion-option>
|
||||
</ion-select>
|
||||
<p item-content *ngIf="!canSaveGrades || !outcome.itemNumber">{{ outcome.selected }}</p>
|
||||
</ion-item>
|
||||
|
||||
<addon-mod-assign-feedback-plugin *ngFor="let plugin of feedback.plugins" [assign]="assign" [submission]="userSubmission" [userId]="submitId" [plugin]="plugin" [canEdit]="canSaveGrades"></addon-mod-assign-feedback-plugin>
|
||||
|
||||
<!-- Workflow status. -->
|
||||
<ion-item text-wrap *ngIf="workflowStatusTranslationId">
|
||||
<h2>{{ 'addon.mod_assign.markingworkflowstate' | translate }}</h2>
|
||||
<p>{{ workflowStatusTranslationId | translate }}</p>
|
||||
</ion-item>
|
||||
|
||||
<!--- Apply grade to all team members. -->
|
||||
<ion-item text-wrap *ngIf="assign.teamsubmission && canSaveGrades">
|
||||
<h2>{{ 'addon.mod_assign.groupsubmissionsettings' | translate }}</h2>
|
||||
<ion-label>{{ 'addon.mod_assign.applytoteam' | translate }}</ion-label>
|
||||
<ion-toggle [(ngModel)]="grade.applyToAll"></ion-toggle>
|
||||
</ion-item>
|
||||
|
||||
<!-- Attempt status. -->
|
||||
<ion-item text-wrap *ngIf="isGrading && assign.attemptreopenmethod != attemptReopenMethodNone">
|
||||
<h2>{{ 'addon.mod_assign.attemptsettings' | translate }}</h2>
|
||||
<p *ngIf="assign.maxattempts == unlimitedAttempts">{{ 'addon.mod_assign.outof' | translate : {'$a': {'current': currentAttempt, 'total': maxAttemptsText} } }}</p>
|
||||
<p *ngIf="assign.maxattempts != unlimitedAttempts">{{ 'addon.mod_assign.outof' | translate : {'$a': {'current': currentAttempt, 'total': assign.maxattempts} } }}</p>
|
||||
<p>{{ 'addon.mod_assign.attemptreopenmethod' | translate }}: {{ 'addon.mod_assign.attemptreopenmethod_' + assign.attemptreopenmethod | translate }}</p>
|
||||
<ng-container *ngIf="canSaveGrades && allowAddAttempt" >
|
||||
<ion-label>{{ 'addon.mod_assign.addattempt' | translate }}</ion-label>
|
||||
<ion-toggle [(ngModel)]="grade.addAttempt"></ion-toggle>
|
||||
</ng-container>
|
||||
</ion-item>
|
||||
|
||||
<!-- Data about the grader (teacher who graded). -->
|
||||
<ion-item text-wrap *ngIf="grader" (click)="openUserProfile(grader.id)" [title]="grader.fullname" detail-push>
|
||||
<ion-avatar item-start>
|
||||
<img [src]="grader.profileimageurl" core-external-content [alt]="'core.pictureof' | translate:{$a: grader.fullname}" role="presentation" onError="this.src='assets/img/user-avatar.png'">
|
||||
</ion-avatar>
|
||||
<h2>{{ 'addon.mod_assign.gradedby' | translate }}</h2>
|
||||
<h2>{{ grader.fullname }}</h2>
|
||||
<p *ngIf="feedback.gradeddate">{{ feedback.gradeddate * 1000 | coreFormatDate:"dfmediumdate" }}</p>
|
||||
</ion-item>
|
||||
|
||||
<!-- Warning message if cannot save grades. -->
|
||||
<div *ngIf="isGrading && !canSaveGrades" class="core-warning-card" icon-start>
|
||||
<ion-icon name="warning"></ion-icon>
|
||||
<p>{{ 'addon.mod_assign.cannotgradefromapp' | translate:{$a: moduleName} }}</p>
|
||||
<a ion-button block *ngIf="gradeUrl" [href]="gradeUrl" core-link icon-end>
|
||||
{{ 'core.openinbrowser' | translate }}
|
||||
<ion-icon name="open"></ion-icon>
|
||||
</a>
|
||||
</div>
|
||||
</ng-template>
|
||||
</core-tab>
|
||||
</core-tabs>
|
||||
</core-loading>
|
||||
|
||||
<!-- Template to render some data regarding the submission status. -->
|
||||
<ng-template #submissionStatus>
|
||||
<p *ngIf="assign && assign.teamsubmission && lastAttempt">
|
||||
<span *ngIf="lastAttempt.submissiongroup && lastAttempt.submissiongroupname">{{lastAttempt.submissiongroupname}}</span>
|
||||
<span *ngIf="assign.preventsubmissionnotingroup && !lastAttempt.submissiongroup && !lastAttempt.usergroups">{{ 'addon.mod_assign.noteam' | translate }}</span>
|
||||
<span *ngIf="assign.preventsubmissionnotingroup && !lastAttempt.submissiongroup && lastAttempt.usergroups">{{ 'addon.mod_assign.multipleteams' | translate }}</span>
|
||||
<span *ngIf="!assign.preventsubmissionnotingroup && !lastAttempt.submissiongroup">{{ 'addon.mod_assign.defaultteam' | translate }}</span>
|
||||
</p>
|
||||
<ion-badge item-end *ngIf="statusTranslated" [color]="statusColor">
|
||||
{{ statusTranslated }}
|
||||
</ion-badge>
|
||||
<ion-badge item-end *ngIf="gradingStatusTranslationId" [color]="gradingColor">
|
||||
{{ gradingStatusTranslationId | translate }}
|
||||
</ion-badge>
|
||||
</ng-template>
|
|
@ -0,0 +1,18 @@
|
|||
addon-mod-assign-submission {
|
||||
div.latesubmission,
|
||||
div.overdue {
|
||||
// @extend .core-danger-item;
|
||||
}
|
||||
|
||||
div.earlysubmission {
|
||||
// @extend .core-success-item;
|
||||
}
|
||||
|
||||
div.submissioneditable p {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
.core-grading-summary .advancedgrade {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,935 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, Input, OnInit, OnDestroy, ViewChild, Optional, ViewChildren, QueryList } from '@angular/core';
|
||||
import { NavController } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { CoreGroupsProvider } from '@providers/groups';
|
||||
import { CoreLangProvider } from '@providers/lang';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSyncProvider } from '@providers/sync';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
import { CoreFileUploaderHelperProvider } from '@core/fileuploader/providers/helper';
|
||||
import { CoreGradesHelperProvider } from '@core/grades/providers/helper';
|
||||
import { CoreUserProvider } from '@core/user/providers/user';
|
||||
import { AddonModAssignProvider } from '../../providers/assign';
|
||||
import { AddonModAssignHelperProvider } from '../../providers/helper';
|
||||
import { AddonModAssignOfflineProvider } from '../../providers/assign-offline';
|
||||
import * as moment from 'moment';
|
||||
import { CoreTabsComponent } from '@components/tabs/tabs';
|
||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||
import { AddonModAssignSubmissionPluginComponent } from '../submission-plugin/submission-plugin';
|
||||
|
||||
/**
|
||||
* Component that displays an assignment submission.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-assign-submission',
|
||||
templateUrl: 'submission.html',
|
||||
})
|
||||
export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(CoreTabsComponent) tabs: CoreTabsComponent;
|
||||
@ViewChildren(AddonModAssignSubmissionPluginComponent) submissionComponents: QueryList<AddonModAssignSubmissionPluginComponent>;
|
||||
|
||||
@Input() courseId: number; // Course ID the submission belongs to.
|
||||
@Input() moduleId: number; // Module ID the submission belongs to.
|
||||
@Input() submitId: number; // User that did the submission.
|
||||
@Input() blindId: number; // Blinded user ID (if it's blinded).
|
||||
@Input() showGrade: boolean | string; // Whether to display the grade tab at start.
|
||||
|
||||
loaded: boolean; // Whether data has been loaded.
|
||||
selectedTab: number; // Tab selected on start.
|
||||
assign: any; // The assignment the submission belongs to.
|
||||
userSubmission: any; // The submission object.
|
||||
isSubmittedForGrading: boolean; // Whether the submission has been submitted for grading.
|
||||
submitModel: any = {}; // Model where to store the data to submit (for grading).
|
||||
feedback: any; // The feedback.
|
||||
hasOffline: boolean; // Whether there is offline data.
|
||||
submittedOffline: boolean; // Whether it was submitted in offline.
|
||||
fromDate: string; // Readable date when the assign started accepting submissions.
|
||||
currentAttempt: number; // The current attempt number.
|
||||
maxAttemptsText: string; // The text for maximum attempts.
|
||||
blindMarking: boolean; // Whether blind marking is enabled.
|
||||
user: any; // The user.
|
||||
lastAttempt: any; // The last attempt.
|
||||
membersToSubmit: any[]; // Team members that need to submit the assignment.
|
||||
canSubmit: boolean; // Whether the user can submit for grading.
|
||||
canEdit: boolean; // Whether the user can edit the submission.
|
||||
submissionStatement: string; // The submission statement.
|
||||
showErrorStatementEdit: boolean; // Whether to show an error in edit due to submission statement.
|
||||
showErrorStatementSubmit: boolean; // Whether to show an error in submit due to submission statement.
|
||||
gradingStatusTranslationId: string; // Key of the text to display for the grading status.
|
||||
gradingColor: string; // Color to apply to the grading status.
|
||||
workflowStatusTranslationId: string; // Key of the text to display for the workflow status.
|
||||
submissionPlugins: string[]; // List of submission plugins names.
|
||||
timeRemaining: string; // Message about time remaining.
|
||||
timeRemainingClass: string; // Class to apply to time remaining message.
|
||||
statusTranslated: string; // Status.
|
||||
statusColor: string; // Color to apply to the status.
|
||||
unsupportedEditPlugins: string[]; // List of submission plugins that don't support edit.
|
||||
grade: any; // Data about the grade.
|
||||
grader: any; // Profile of the teacher that graded the submission.
|
||||
gradeInfo: any; // Grade data for the assignment, retrieved from the server.
|
||||
isGrading: boolean; // Whether the user is grading.
|
||||
canSaveGrades: boolean; // Whether the user can save the grades.
|
||||
allowAddAttempt: boolean; // Allow adding a new attempt when grading.
|
||||
gradeUrl: string; // URL to grade in browser.
|
||||
|
||||
// Some constants.
|
||||
statusNew = AddonModAssignProvider.SUBMISSION_STATUS_NEW;
|
||||
statusReopened = AddonModAssignProvider.SUBMISSION_STATUS_REOPENED;
|
||||
attemptReopenMethodNone = AddonModAssignProvider.ATTEMPT_REOPEN_METHOD_NONE;
|
||||
unlimitedAttempts = AddonModAssignProvider.UNLIMITED_ATTEMPTS;
|
||||
|
||||
protected siteId: string; // Current site ID.
|
||||
protected currentUserId: number; // Current user ID.
|
||||
protected previousAttempt: any; // The previous attempt.
|
||||
protected submissionStatusAvailable: boolean; // Whether we were able to retrieve the submission status.
|
||||
protected originalGrades: any = {}; // Object with the original grade data, to check for changes.
|
||||
protected isDestroyed: boolean; // Whether the component has been destroyed.
|
||||
|
||||
constructor(protected navCtrl: NavController, protected appProvider: CoreAppProvider, protected domUtils: CoreDomUtilsProvider,
|
||||
sitesProvider: CoreSitesProvider, protected syncProvider: CoreSyncProvider, protected timeUtils: CoreTimeUtilsProvider,
|
||||
protected textUtils: CoreTextUtilsProvider, protected translate: TranslateService, protected utils: CoreUtilsProvider,
|
||||
protected eventsProvider: CoreEventsProvider, protected courseProvider: CoreCourseProvider,
|
||||
protected fileUploaderHelper: CoreFileUploaderHelperProvider, protected gradesHelper: CoreGradesHelperProvider,
|
||||
protected userProvider: CoreUserProvider, protected groupsProvider: CoreGroupsProvider,
|
||||
protected langProvider: CoreLangProvider, protected assignProvider: AddonModAssignProvider,
|
||||
protected assignHelper: AddonModAssignHelperProvider, protected assignOfflineProvider: AddonModAssignOfflineProvider,
|
||||
@Optional() protected splitviewCtrl: CoreSplitViewComponent) {
|
||||
|
||||
this.siteId = sitesProvider.getCurrentSiteId();
|
||||
this.currentUserId = sitesProvider.getCurrentSiteUserId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.selectedTab = this.showGrade && this.showGrade !== 'false' ? 1 : 0;
|
||||
this.isSubmittedForGrading = !!this.submitId;
|
||||
|
||||
this.loadData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the time remaining message and class.
|
||||
*
|
||||
* @param {any} response Response of get submission status.
|
||||
*/
|
||||
protected calculateTimeRemaining(response: any): void {
|
||||
if (this.assign.duedate > 0) {
|
||||
const time = this.timeUtils.timestamp(),
|
||||
dueDate = response.lastattempt && response.lastattempt.extensionduedate ?
|
||||
response.lastattempt.extensionduedate : this.assign.duedate,
|
||||
timeRemaining = dueDate - time;
|
||||
|
||||
if (timeRemaining <= 0) {
|
||||
if (!this.userSubmission || this.userSubmission.status != AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED) {
|
||||
|
||||
if ((response.lastattempt && response.lastattempt.submissionsenabled) ||
|
||||
(response.gradingsummary && response.gradingsummary.submissionsenabled)) {
|
||||
this.timeRemaining = this.translate.instant('addon.mod_assign.overdue',
|
||||
{$a: this.timeUtils.formatDuration(-timeRemaining, 3) });
|
||||
this.timeRemainingClass = 'overdue';
|
||||
} else {
|
||||
this.timeRemaining = this.translate.instant('addon.mod_assign.duedatereached');
|
||||
this.timeRemainingClass = '';
|
||||
}
|
||||
} else {
|
||||
|
||||
const timeSubmittedDiff = this.userSubmission.timemodified - dueDate;
|
||||
if (timeSubmittedDiff > 0) {
|
||||
this.timeRemaining = this.translate.instant('addon.mod_assign.submittedlate',
|
||||
{$a: this.timeUtils.formatDuration(timeSubmittedDiff, 2) });
|
||||
this.timeRemainingClass = 'latesubmission';
|
||||
} else {
|
||||
this.timeRemaining = this.translate.instant('addon.mod_assign.submittedearly',
|
||||
{$a: this.timeUtils.formatDuration(-timeSubmittedDiff, 2) });
|
||||
this.timeRemainingClass = 'earlysubmission';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.timeRemaining = this.timeUtils.formatDuration(timeRemaining, 3);
|
||||
this.timeRemainingClass = '';
|
||||
}
|
||||
} else {
|
||||
this.timeRemaining = '';
|
||||
this.timeRemainingClass = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user can leave the view. If there are changes to be saved, it will ask for confirm.
|
||||
*
|
||||
* @return {Promise<void>} Promise resolved if can leave the view, rejected otherwise.
|
||||
*/
|
||||
canLeave(): Promise<void> {
|
||||
// Check if there is data to save.
|
||||
return this.hasDataToSave().then((modified) => {
|
||||
if (modified) {
|
||||
// Modified, confirm user wants to go back.
|
||||
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit')).then(() => {
|
||||
return this.discardDrafts().catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a previous attempt and then go to edit.
|
||||
*/
|
||||
copyPrevious(): void {
|
||||
if (!this.appProvider.isOnline()) {
|
||||
this.domUtils.showErrorModal('core.networkerrormsg', true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.previousAttempt) {
|
||||
// Cannot access previous attempts, just go to edit.
|
||||
return this.goToEdit();
|
||||
}
|
||||
|
||||
const previousSubmission = this.assignProvider.getSubmissionObjectFromAttempt(this.assign, this.previousAttempt);
|
||||
let modal = this.domUtils.showModalLoading();
|
||||
|
||||
this.assignHelper.getSubmissionSizeForCopy(this.assign, previousSubmission).catch(() => {
|
||||
// Error calculating size, return -1.
|
||||
return -1;
|
||||
}).then((size) => {
|
||||
modal.dismiss();
|
||||
|
||||
// Confirm action.
|
||||
return this.fileUploaderHelper.confirmUploadFile(size, true);
|
||||
}).then(() => {
|
||||
// User confirmed, copy the attempt.
|
||||
modal = this.domUtils.showModalLoading('core.sending', true);
|
||||
|
||||
this.assignHelper.copyPreviousAttempt(this.assign, previousSubmission).then(() => {
|
||||
// Now go to edit.
|
||||
this.goToEdit();
|
||||
|
||||
if (!this.assign.submissiondrafts) {
|
||||
// No drafts allowed, so it was submitted. Trigger event.
|
||||
this.eventsProvider.trigger(AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, {
|
||||
assignmentId: this.assign.id,
|
||||
submissionId: this.userSubmission.id,
|
||||
userId: this.currentUserId
|
||||
}, this.siteId);
|
||||
} else {
|
||||
// Invalidate and refresh data to update this view.
|
||||
this.invalidateAndRefresh();
|
||||
}
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'core.error', true);
|
||||
}).finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
}).catch(() => {
|
||||
// Cancelled.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Discard feedback drafts.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected discardDrafts(): Promise<any> {
|
||||
if (this.feedback && this.feedback.plugins) {
|
||||
return this.assignHelper.discardFeedbackPluginData(this.assign.id, this.submitId, this.feedback);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to the page to add or edit submission.
|
||||
*/
|
||||
goToEdit(): void {
|
||||
this.navCtrl.push('AddonModAssignEditPage', {
|
||||
moduleId: this.moduleId,
|
||||
courseId: this.courseId,
|
||||
userId: this.submitId,
|
||||
blindId: this.blindId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there's data to save (grade).
|
||||
*
|
||||
* @return {Promise<boolean>} Promise resolved with boolean: whether there's data to save.
|
||||
*/
|
||||
protected hasDataToSave(): Promise<boolean> {
|
||||
if (!this.canSaveGrades || !this.loaded) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
// Check if numeric grade and toggles changed.
|
||||
if (this.originalGrades.grade != this.grade.grade || this.originalGrades.addAttempt != this.grade.addAttempt ||
|
||||
this.originalGrades.applyToAll != this.grade.applyToAll) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// Check if outcomes changed.
|
||||
if (this.gradeInfo && this.gradeInfo.outcomes) {
|
||||
for (const x in this.gradeInfo.outcomes) {
|
||||
const outcome = this.gradeInfo.outcomes[x];
|
||||
|
||||
if (this.originalGrades.outcomes[outcome.id] == 'undefined' ||
|
||||
this.originalGrades.outcomes[outcome.id] != outcome.selectedId) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.feedback && this.feedback.plugins) {
|
||||
return this.assignHelper.hasFeedbackDataChanged(this.assign, this.submitId, this.feedback).catch(() => {
|
||||
// Error ocurred, consider there are no changes.
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate and refresh data.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
invalidateAndRefresh(): Promise<any> {
|
||||
this.loaded = false;
|
||||
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.assignProvider.invalidateAssignmentData(this.courseId));
|
||||
if (this.assign) {
|
||||
promises.push(this.assignProvider.invalidateSubmissionStatusData(this.assign.id, this.submitId, !!this.blindId));
|
||||
promises.push(this.assignProvider.invalidateAssignmentUserMappingsData(this.assign.id));
|
||||
promises.push(this.assignProvider.invalidateListParticipantsData(this.assign.id));
|
||||
}
|
||||
promises.push(this.gradesHelper.invalidateGradeModuleItems(this.courseId, this.submitId));
|
||||
promises.push(this.courseProvider.invalidateModule(this.moduleId));
|
||||
|
||||
// Invalidate plugins.
|
||||
if (this.submissionComponents && this.submissionComponents.length) {
|
||||
this.submissionComponents.forEach((component) => {
|
||||
promises.push(component.invalidate());
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.all(promises).catch(() => {
|
||||
// Ignore errors.
|
||||
}).then(() => {
|
||||
return this.loadData();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the data to render the submission.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected loadData(): Promise<any> {
|
||||
let isBlind = !!this.blindId;
|
||||
|
||||
this.previousAttempt = undefined;
|
||||
|
||||
if (!this.submitId) {
|
||||
this.submitId = this.currentUserId;
|
||||
isBlind = false;
|
||||
}
|
||||
|
||||
// Get the assignment.
|
||||
return this.assignProvider.getAssignment(this.courseId, this.moduleId).then((assign) => {
|
||||
const time = this.timeUtils.timestamp(),
|
||||
promises = [];
|
||||
|
||||
this.assign = assign;
|
||||
|
||||
if (assign.allowsubmissionsfromdate && assign.allowsubmissionsfromdate >= time) {
|
||||
this.fromDate = moment(assign.allowsubmissionsfromdate * 1000).format(this.translate.instant('core.dfmediumdate'));
|
||||
}
|
||||
|
||||
this.currentAttempt = 0;
|
||||
this.maxAttemptsText = this.translate.instant('addon.mod_assign.unlimitedattempts');
|
||||
this.blindMarking = this.isSubmittedForGrading && assign.blindmarking && !assign.revealidentities;
|
||||
|
||||
if (!this.blindMarking && this.submitId != this.currentUserId) {
|
||||
promises.push(this.userProvider.getProfile(this.submitId, this.courseId).then((profile) => {
|
||||
this.user = profile;
|
||||
}));
|
||||
}
|
||||
|
||||
// Check if there's any offline data for this submission.
|
||||
promises.push(this.assignOfflineProvider.getSubmission(assign.id, this.submitId).then((data) => {
|
||||
this.hasOffline = data && data.pluginData && Object.keys(data.pluginData).length > 0;
|
||||
this.submittedOffline = data && data.submitted;
|
||||
}).catch(() => {
|
||||
// No offline data found.
|
||||
this.hasOffline = false;
|
||||
this.submittedOffline = false;
|
||||
}));
|
||||
|
||||
return Promise.all(promises);
|
||||
}).then(() => {
|
||||
// Get submission status.
|
||||
return this.assignProvider.getSubmissionStatus(this.assign.id, this.submitId, isBlind);
|
||||
}).then((response) => {
|
||||
|
||||
const promises = [];
|
||||
|
||||
this.submissionStatusAvailable = true;
|
||||
this.lastAttempt = response.lastattempt;
|
||||
this.membersToSubmit = [];
|
||||
|
||||
// Search the previous attempt.
|
||||
if (response.previousattempts && response.previousattempts.length > 0) {
|
||||
const previousAttempts = response.previousattempts.sort((a, b) => {
|
||||
return a.attemptnumber - b.attemptnumber;
|
||||
});
|
||||
this.previousAttempt = previousAttempts[previousAttempts.length - 1];
|
||||
}
|
||||
|
||||
// Treat last attempt.
|
||||
this.treatLastAttempt(response, promises);
|
||||
|
||||
// Calculate the time remaining.
|
||||
this.calculateTimeRemaining(response);
|
||||
|
||||
// Load the feedback.
|
||||
promises.push(this.loadFeedback(response.feedback));
|
||||
|
||||
// Check if there's any unsupported plugin for editing.
|
||||
if (!this.userSubmission || !this.userSubmission.plugins) {
|
||||
// Submission not created yet, we have to use assign configs to detect the plugins used.
|
||||
this.userSubmission = {};
|
||||
this.userSubmission.plugins = this.assignHelper.getPluginsEnabled(this.assign, 'assignsubmission');
|
||||
}
|
||||
|
||||
// Get the submission plugins that don't support editing.
|
||||
promises.push(this.assignProvider.getUnsupportedEditPlugins(this.userSubmission.plugins).then((list) => {
|
||||
this.unsupportedEditPlugins = list;
|
||||
}));
|
||||
|
||||
return Promise.all(promises);
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'Error getting assigment data.');
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the data to render the feedback and grade.
|
||||
*
|
||||
* @param {any} feedback The feedback data from the submission status.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected loadFeedback(feedback: any): Promise<any> {
|
||||
this.grade = {
|
||||
method: false,
|
||||
grade: false,
|
||||
modified: 0,
|
||||
gradingStatus: false,
|
||||
addAttempt : false,
|
||||
applyToAll: false,
|
||||
scale: false,
|
||||
lang: false
|
||||
};
|
||||
|
||||
this.originalGrades = {
|
||||
grade: false,
|
||||
addAttempt: false,
|
||||
applyToAll: false,
|
||||
outcomes: {}
|
||||
};
|
||||
|
||||
if (feedback) {
|
||||
this.feedback = feedback;
|
||||
|
||||
// If we have data about the grader, get its profile.
|
||||
if (feedback.grade && feedback.grade.grader) {
|
||||
this.userProvider.getProfile(feedback.grade.grader, this.courseId).then((profile) => {
|
||||
this.grader = profile;
|
||||
}).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}
|
||||
|
||||
// Check if the grade uses advanced grading.
|
||||
if (feedback.gradefordisplay) {
|
||||
const position = feedback.gradefordisplay.indexOf('class="advancedgrade"');
|
||||
if (position > -1) {
|
||||
this.feedback.advancedgrade = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Do not override already loaded grade.
|
||||
if (feedback.grade && feedback.grade.grade && !this.grade.grade) {
|
||||
const parsedGrade = parseFloat(feedback.grade.grade);
|
||||
this.grade.grade = parsedGrade || parsedGrade == 0 ? parsedGrade : null;
|
||||
}
|
||||
} else {
|
||||
// If no feedback, always show Submission.
|
||||
this.selectedTab = 0;
|
||||
this.tabs.selectTab(0);
|
||||
}
|
||||
|
||||
this.grade.gradingStatus = this.lastAttempt && this.lastAttempt.gradingstatus;
|
||||
|
||||
// Get the grade for the assign.
|
||||
return this.courseProvider.getModuleBasicGradeInfo(this.moduleId).then((gradeInfo) => {
|
||||
this.gradeInfo = gradeInfo;
|
||||
|
||||
if (!gradeInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure outcomes is an array.
|
||||
gradeInfo.outcomes = gradeInfo.outcomes || [];
|
||||
|
||||
if (!this.isDestroyed) {
|
||||
// Block the assignment.
|
||||
this.syncProvider.blockOperation(AddonModAssignProvider.COMPONENT, this.assign.id);
|
||||
}
|
||||
|
||||
// Treat the grade info.
|
||||
return this.treatGradeInfo();
|
||||
}).then(() => {
|
||||
if (!this.isGrading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isManual = this.assign.attemptreopenmethod == AddonModAssignProvider.ATTEMPT_REOPEN_METHOD_MANUAL,
|
||||
isUnlimited = this.assign.maxattempts == AddonModAssignProvider.UNLIMITED_ATTEMPTS,
|
||||
isLessThanMaxAttempts = this.userSubmission && (this.userSubmission.attemptnumber < (this.assign.maxattempts - 1));
|
||||
|
||||
this.allowAddAttempt = isManual && (!this.userSubmission || isUnlimited || isLessThanMaxAttempts);
|
||||
|
||||
if (this.assign.teamsubmission) {
|
||||
this.grade.applyToAll = true;
|
||||
this.originalGrades.applyToAll = true;
|
||||
}
|
||||
if (this.assign.markingworkflow && this.grade.gradingStatus) {
|
||||
this.workflowStatusTranslationId =
|
||||
this.assignProvider.getSubmissionGradingStatusTranslationId(this.grade.gradingStatus);
|
||||
}
|
||||
|
||||
if (!this.feedback || !this.feedback.plugins) {
|
||||
// Feedback plugins not present, we have to use assign configs to detect the plugins used.
|
||||
this.feedback = {};
|
||||
this.feedback.plugins = this.assignHelper.getPluginsEnabled(this.assign, 'assignfeedback');
|
||||
}
|
||||
|
||||
// Check if there's any offline data for this submission.
|
||||
if (this.canSaveGrades) {
|
||||
// Submission grades aren't identified by attempt number so it can retrieve the feedback for a previous attempt.
|
||||
// The app will not treat that as an special case.
|
||||
return this.assignOfflineProvider.getSubmissionGrade(this.assign.id, this.submitId).catch(() => {
|
||||
// Grade not found.
|
||||
}).then((data) => {
|
||||
|
||||
// Load offline grades.
|
||||
if (data && (!feedback || !feedback.gradeddate || feedback.gradeddate < data.timemodified)) {
|
||||
// If grade has been modified from gradebook, do not use offline.
|
||||
if (this.grade.modified < data.timemodified) {
|
||||
this.grade.grade = data.grade;
|
||||
this.gradingStatusTranslationId = 'addon.mod_assign.gradenotsynced';
|
||||
this.gradingColor = '';
|
||||
this.originalGrades.grade = this.grade.grade;
|
||||
}
|
||||
|
||||
this.grade.applyToAll = data.applyToAll;
|
||||
this.grade.addAttempt = data.addAttempt;
|
||||
this.originalGrades.applyToAll = this.grade.applyToAll;
|
||||
this.originalGrades.addAttempt = this.grade.addAttempt;
|
||||
|
||||
if (data.outcomes && Object.keys(data.outcomes).length) {
|
||||
this.gradeInfo.outcomes.forEach((outcome) => {
|
||||
if (typeof data.outcomes[outcome.itemNumber] != 'undefined') {
|
||||
// If outcome has been modified from gradebook, do not use offline.
|
||||
if (outcome.modified < data.timemodified) {
|
||||
outcome.selectedId = data.outcomes[outcome.itemNumber];
|
||||
this.originalGrades.outcomes[outcome.id] = outcome.selectedId;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// User cannot save grades in the app. Load the URL to grade it in browser.
|
||||
return this.courseProvider.getModule(this.moduleId, this.courseId, undefined, true).then((mod) => {
|
||||
this.gradeUrl = mod.url + '&action=grader&userid=' + this.submitId;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a user profile.
|
||||
*
|
||||
* @param {number} userId User to open.
|
||||
*/
|
||||
openUserProfile(userId: number): void {
|
||||
// Open a user profile. If this component is inside a split view, use the master nav to open it.
|
||||
const navCtrl = this.splitviewCtrl ? this.splitviewCtrl.getMasterNav() : this.navCtrl;
|
||||
navCtrl.push('CoreUserProfilePage', { userId: userId, courseId: this.courseId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the submission status name and class.
|
||||
*
|
||||
* @param {any} status Submission status.
|
||||
*/
|
||||
protected setStatusNameAndClass(status: any): void {
|
||||
if (this.hasOffline || this.submittedOffline) {
|
||||
// Offline data.
|
||||
this.statusTranslated = this.translate.instant('core.notsent');
|
||||
this.statusColor = 'warning';
|
||||
} else if (!this.assign.teamsubmission) {
|
||||
|
||||
// Single submission.
|
||||
if (this.userSubmission && this.userSubmission.status != this.statusNew) {
|
||||
this.statusTranslated = this.translate.instant('addon.mod_assign.submissionstatus_' + this.userSubmission.status);
|
||||
this.statusColor = this.assignProvider.getSubmissionStatusColor(this.userSubmission.status);
|
||||
} else {
|
||||
if (!status.lastattempt.submissionsenabled) {
|
||||
this.statusTranslated = this.translate.instant('addon.mod_assign.noonlinesubmissions');
|
||||
this.statusColor = this.assignProvider.getSubmissionStatusColor('noonlinesubmissions');
|
||||
} else {
|
||||
this.statusTranslated = this.translate.instant('addon.mod_assign.noattempt');
|
||||
this.statusColor = this.assignProvider.getSubmissionStatusColor('noattempt');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// Team submission.
|
||||
if (!status.lastattempt.submissiongroup && this.assign.preventsubmissionnotingroup) {
|
||||
this.statusTranslated = this.translate.instant('addon.mod_assign.nosubmission');
|
||||
this.statusColor = this.assignProvider.getSubmissionStatusColor('nosubmission');
|
||||
} else if (this.userSubmission && this.userSubmission.status != this.statusNew) {
|
||||
this.statusTranslated = this.translate.instant('addon.mod_assign.submissionstatus_' + this.userSubmission.status);
|
||||
this.statusColor = this.assignProvider.getSubmissionStatusColor(this.userSubmission.status);
|
||||
} else {
|
||||
if (!status.lastattempt.submissionsenabled) {
|
||||
this.statusTranslated = this.translate.instant('addon.mod_assign.noonlinesubmissions');
|
||||
this.statusColor = this.assignProvider.getSubmissionStatusColor('noonlinesubmissions');
|
||||
} else {
|
||||
this.statusTranslated = this.translate.instant('addon.mod_assign.nosubmission');
|
||||
this.statusColor = this.assignProvider.getSubmissionStatusColor('nosubmission');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show advanced grade.
|
||||
*/
|
||||
showAdvancedGrade(): void {
|
||||
if (this.feedback && this.feedback.advancedgrade) {
|
||||
this.textUtils.expandText(this.translate.instant('core.grades.grade'), this.feedback.gradefordisplay,
|
||||
AddonModAssignProvider.COMPONENT, this.moduleId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit for grading.
|
||||
*
|
||||
* @param {boolean} acceptStatement Whether the statement has been accepted.
|
||||
*/
|
||||
submitForGrading(acceptStatement: boolean): void {
|
||||
if (this.assign.requiresubmissionstatement && !acceptStatement) {
|
||||
this.domUtils.showErrorModal('addon.mod_assign.acceptsubmissionstatement', true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask for confirmation. @todo plugin precheck_submission
|
||||
this.domUtils.showConfirm(this.translate.instant('addon.mod_assign.confirmsubmission')).then(() => {
|
||||
const modal = this.domUtils.showModalLoading('core.sending', true);
|
||||
|
||||
this.assignProvider.submitForGrading(this.assign.id, this.courseId, acceptStatement, this.userSubmission.timemodified,
|
||||
this.hasOffline).then(() => {
|
||||
|
||||
// Submitted, trigger event.
|
||||
this.eventsProvider.trigger(AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, {
|
||||
assignmentId: this.assign.id,
|
||||
submissionId: this.userSubmission.id,
|
||||
userId: this.currentUserId
|
||||
}, this.siteId);
|
||||
}).finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'core.error', true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a grade and feedback.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
submitGrade(): Promise<any> {
|
||||
// Check if there's something to be saved.
|
||||
return this.hasDataToSave().then((modified) => {
|
||||
if (!modified) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attemptNumber = this.userSubmission ? this.userSubmission.attemptnumber : -1,
|
||||
outcomes = {},
|
||||
// Scale "no grade" uses -1 instead of 0.
|
||||
grade = this.grade.scale && this.grade.grade == 0 ? -1 : this.utils.unformatFloat(this.grade.grade);
|
||||
|
||||
if (grade === false) {
|
||||
// Grade is invalid.
|
||||
return Promise.reject(this.translate.instant('core.grades.badgrade'));
|
||||
}
|
||||
|
||||
const modal = this.domUtils.showModalLoading('core.sending', true);
|
||||
let pluginPromise;
|
||||
|
||||
this.gradeInfo.outcomes.forEach((outcome) => {
|
||||
if (outcome.itemNumber) {
|
||||
outcomes[outcome.itemNumber] = outcome.selectedId;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.feedback && this.feedback.plugins) {
|
||||
pluginPromise = this.assignHelper.prepareFeedbackPluginData(this.assign.id, this.submitId, this.feedback);
|
||||
} else {
|
||||
pluginPromise = Promise.resolve({});
|
||||
}
|
||||
|
||||
return pluginPromise.then((pluginData) => {
|
||||
// We have all the data, now send it.
|
||||
return this.assignProvider.submitGradingForm(this.assign.id, this.submitId, this.courseId, grade, attemptNumber,
|
||||
this.grade.addAttempt, this.grade.gradingStatus, this.grade.applyToAll, outcomes, pluginData).then(() => {
|
||||
|
||||
// Data sent, discard draft.
|
||||
return this.discardDrafts();
|
||||
}).finally(() => {
|
||||
// Invalidate and refresh data.
|
||||
this.invalidateAndRefresh();
|
||||
|
||||
this.eventsProvider.trigger(AddonModAssignProvider.GRADED_EVENT, {
|
||||
assignmentId: this.assign.id,
|
||||
submissionId: this.submitId,
|
||||
userId: this.currentUserId
|
||||
}, this.siteId);
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'core.error', true);
|
||||
}).finally(() => {
|
||||
// Select submission view.
|
||||
this.tabs.selectTab(0);
|
||||
modal.dismiss();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Treat the grade info.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected treatGradeInfo(): Promise<any> {
|
||||
// Check if grading method is simple or not.
|
||||
if (this.gradeInfo.advancedgrading && this.gradeInfo.advancedgrading[0] &&
|
||||
typeof this.gradeInfo.advancedgrading[0].method != 'undefined') {
|
||||
this.grade.method = this.gradeInfo.advancedgrading[0].method || 'simple';
|
||||
} else {
|
||||
this.grade.method = 'simple';
|
||||
}
|
||||
|
||||
this.isGrading = true;
|
||||
this.canSaveGrades = this.grade.method == 'simple'; // Grades can be saved if simple grading.
|
||||
|
||||
if (this.gradeInfo.scale) {
|
||||
this.grade.scale = this.utils.makeMenuFromList(this.gradeInfo.scale, this.translate.instant('core.nograde'));
|
||||
} else {
|
||||
// Get current language to format grade input field.
|
||||
this.langProvider.getCurrentLanguage().then((lang) => {
|
||||
this.grade.lang = lang;
|
||||
});
|
||||
}
|
||||
|
||||
// Treat outcomes.
|
||||
if (this.assignProvider.isOutcomesEditEnabled()) {
|
||||
this.gradeInfo.outcomes.forEach((outcome) => {
|
||||
if (outcome.scale) {
|
||||
outcome.options =
|
||||
this.utils.makeMenuFromList(outcome.scale, this.translate.instant('core.grades.nooutcome'));
|
||||
}
|
||||
outcome.selectedId = 0;
|
||||
this.originalGrades.outcomes[outcome.id] = outcome.selectedId;
|
||||
});
|
||||
}
|
||||
|
||||
// Get grade items.
|
||||
return this.gradesHelper.getGradeModuleItems(this.courseId, this.moduleId, this.submitId).then((grades) => {
|
||||
const outcomes = [];
|
||||
|
||||
grades.forEach((grade) => {
|
||||
if (!grade.outcomeid && !grade.scaleid) {
|
||||
|
||||
// Not using outcomes or scale, get the numeric grade.
|
||||
if (this.grade.scale) {
|
||||
this.grade.grade = this.gradesHelper.getGradeValueFromLabel(this.grade.scale, grade.gradeformatted);
|
||||
} else {
|
||||
const parsedGrade = parseFloat(grade.gradeformatted);
|
||||
this.grade.grade = parsedGrade || parsedGrade == 0 ? parsedGrade : null;
|
||||
}
|
||||
|
||||
this.grade.modified = grade.gradedategraded;
|
||||
this.originalGrades.grade = this.grade.grade;
|
||||
} else if (grade.outcomeid) {
|
||||
|
||||
// Only show outcomes with info on it, outcomeid could be null if outcomes are disabled on site.
|
||||
this.gradeInfo.outcomes.forEach((outcome) => {
|
||||
if (outcome.id == grade.outcomeid) {
|
||||
outcome.selected = grade.gradeformatted;
|
||||
outcome.modified = grade.gradedategraded;
|
||||
if (outcome.options) {
|
||||
outcome.selectedId = this.gradesHelper.getGradeValueFromLabel(outcome.options, outcome.selected);
|
||||
this.originalGrades.outcomes[outcome.id] = outcome.selectedId;
|
||||
outcome.itemNumber = grade.itemnumber;
|
||||
}
|
||||
outcomes.push(outcome);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.gradeInfo.outcomes = outcomes;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Treat the last attempt.
|
||||
*
|
||||
* @param {any} response Response of get submission status.
|
||||
* @param {any[]} promises List where to add the promises.
|
||||
*/
|
||||
protected treatLastAttempt(response: any, promises: any[]): void {
|
||||
if (!response.lastattempt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const submissionStatementMissing = this.assign.requiresubmissionstatement &&
|
||||
typeof this.assign.submissionstatement == 'undefined';
|
||||
|
||||
this.canSubmit = !this.isSubmittedForGrading && !this.submittedOffline && (response.lastattempt.cansubmit ||
|
||||
(this.hasOffline && this.assignProvider.canSubmitOffline(this.assign, response)));
|
||||
this.canEdit = !this.isSubmittedForGrading && response.lastattempt.canedit &&
|
||||
(!this.submittedOffline || !this.assign.submissiondrafts);
|
||||
|
||||
// Get submission statement if needed.
|
||||
if (this.assign.requiresubmissionstatement && this.assign.submissiondrafts && this.submitId == this.currentUserId) {
|
||||
this.submissionStatement = this.assign.submissionstatement;
|
||||
this.submitModel.submissionStatement = false;
|
||||
} else {
|
||||
this.submissionStatement = undefined;
|
||||
this.submitModel.submissionStatement = true; // No submission statement, so it's accepted.
|
||||
}
|
||||
|
||||
// Show error if submission statement should be shown but it couldn't be retrieved.
|
||||
this.showErrorStatementEdit = submissionStatementMissing && !this.assign.submissiondrafts &&
|
||||
this.submitId == this.currentUserId;
|
||||
this.showErrorStatementSubmit = submissionStatementMissing && this.assign.submissiondrafts;
|
||||
|
||||
this.userSubmission = this.assignProvider.getSubmissionObjectFromAttempt(this.assign, response.lastattempt);
|
||||
|
||||
if (this.assign.attemptreopenmethod != this.attemptReopenMethodNone && this.userSubmission) {
|
||||
this.currentAttempt = this.userSubmission.attemptnumber + 1;
|
||||
}
|
||||
|
||||
this.setStatusNameAndClass(response);
|
||||
|
||||
if (this.assign.teamsubmission) {
|
||||
if (response.lastattempt.submissiongroup) {
|
||||
// Get the name of the group.
|
||||
promises.push(this.groupsProvider.getActivityAllowedGroups(this.assign.cmid).then((groups) => {
|
||||
groups.forEach((group) => {
|
||||
if (group.id == response.lastattempt.submissiongroup) {
|
||||
this.lastAttempt.submissiongroupname = group.name;
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
// Get the members that need to submit.
|
||||
if (this.userSubmission && this.userSubmission.status != this.statusNew) {
|
||||
response.lastattempt.submissiongroupmemberswhoneedtosubmit.forEach((member) => {
|
||||
if (this.blindMarking) {
|
||||
// Users not blinded! (Moodle < 3.1.1, 3.2).
|
||||
promises.push(this.assignProvider.getAssignmentUserMappings(this.assign.id, member).then((blindId) => {
|
||||
this.membersToSubmit.push(blindId);
|
||||
}));
|
||||
} else {
|
||||
promises.push(this.userProvider.getProfile(member, this.courseId).then((profile) => {
|
||||
this.membersToSubmit.push(profile);
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
response.lastattempt.submissiongroupmemberswhoneedtosubmitblind.forEach((member) => {
|
||||
this.membersToSubmit.push(member);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get grading text and color.
|
||||
this.gradingStatusTranslationId = this.assignProvider.getSubmissionGradingStatusTranslationId(
|
||||
response.lastattempt.gradingstatus);
|
||||
this.gradingColor = this.assignProvider.getSubmissionGradingStatusColor(response.lastattempt.gradingstatus);
|
||||
|
||||
// Get the submission plugins.
|
||||
if (this.userSubmission) {
|
||||
if (!this.assign.teamsubmission || !response.lastattempt.submissiongroup || !this.assign.preventsubmissionnotingroup) {
|
||||
if (this.previousAttempt && this.previousAttempt.submission.plugins &&
|
||||
this.userSubmission.status == this.statusReopened) {
|
||||
// Get latest attempt if avalaible.
|
||||
this.submissionPlugins = this.previousAttempt.submission.plugins;
|
||||
} else {
|
||||
this.submissionPlugins = this.userSubmission.plugins;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.isDestroyed = true;
|
||||
|
||||
if (this.assign && this.isGrading) {
|
||||
this.syncProvider.unblockOperation(AddonModAssignProvider.COMPONENT, this.assign.id);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { AddonModAssignFeedbackCommentsHandler } from './providers/handler';
|
||||
import { AddonModAssignFeedbackCommentsComponent } from './component/comments';
|
||||
import { AddonModAssignFeedbackDelegate } from '../../providers/feedback-delegate';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModAssignFeedbackCommentsComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule
|
||||
],
|
||||
providers: [
|
||||
AddonModAssignFeedbackCommentsHandler
|
||||
],
|
||||
exports: [
|
||||
AddonModAssignFeedbackCommentsComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModAssignFeedbackCommentsComponent
|
||||
]
|
||||
})
|
||||
export class AddonModAssignFeedbackCommentsModule {
|
||||
constructor(feedbackDelegate: AddonModAssignFeedbackDelegate, handler: AddonModAssignFeedbackCommentsHandler) {
|
||||
feedbackDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<!-- Read only. -->
|
||||
<ion-item text-wrap *ngIf="(text || canEdit) && !edit">
|
||||
<h2>{{ plugin.name }}</h2>
|
||||
<p>
|
||||
<core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="80" [fullOnClick]="true" [fullTitle]="plugin.name" [text]="text"></core-format-text>
|
||||
</p>
|
||||
<div item-end>
|
||||
<div text-right>
|
||||
<button ion-button icon-only clear *ngIf="canEdit" (click)="editComment()" color="dark">
|
||||
<ion-icon name="create"></ion-icon>
|
||||
</button>
|
||||
</div>
|
||||
<ion-note *ngIf="!isSent" color="dark">
|
||||
<ion-icon name="clock"></ion-icon>
|
||||
{{ 'core.notsent' | translate }}
|
||||
</ion-note>
|
||||
</div>
|
||||
</ion-item>
|
||||
|
||||
<!-- Edit -->
|
||||
<ion-item text-wrap *ngIf="edit && loaded">
|
||||
<!-- @todo: [component]="component" [componentId]="assign.cmid" -->
|
||||
<core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" name="assignfeedbackcomments_editor"></core-rich-text-editor>
|
||||
</ion-item>
|
|
@ -0,0 +1,148 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, ElementRef } from '@angular/core';
|
||||
import { ModalController } from 'ionic-angular';
|
||||
import { FormBuilder, FormControl } from '@angular/forms';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { AddonModAssignProvider } from '../../../providers/assign';
|
||||
import { AddonModAssignOfflineProvider } from '../../../providers/assign-offline';
|
||||
import { AddonModAssignFeedbackDelegate } from '../../../providers/feedback-delegate';
|
||||
import { AddonModAssignFeedbackPluginComponent } from '../../../classes/feedback-plugin-component';
|
||||
import { AddonModAssignFeedbackCommentsHandler } from '../providers/handler';
|
||||
|
||||
/**
|
||||
* Component to render a comments feedback plugin.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-assign-feedback-comments',
|
||||
templateUrl: 'comments.html'
|
||||
})
|
||||
export class AddonModAssignFeedbackCommentsComponent extends AddonModAssignFeedbackPluginComponent implements OnInit {
|
||||
|
||||
control: FormControl;
|
||||
component = AddonModAssignProvider.COMPONENT;
|
||||
text: string;
|
||||
isSent: boolean;
|
||||
loaded: boolean;
|
||||
|
||||
protected element: HTMLElement;
|
||||
|
||||
constructor(modalCtrl: ModalController, element: ElementRef, protected domUtils: CoreDomUtilsProvider,
|
||||
protected textUtils: CoreTextUtilsProvider, protected assignOfflineProvider: AddonModAssignOfflineProvider,
|
||||
protected assignProvider: AddonModAssignProvider, protected fb: FormBuilder,
|
||||
protected feedbackDelegate: AddonModAssignFeedbackDelegate) {
|
||||
super(modalCtrl);
|
||||
|
||||
this.element = element.nativeElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
let promise,
|
||||
rteEnabled;
|
||||
|
||||
// Check if rich text editor is enabled.
|
||||
if (this.edit) {
|
||||
promise = this.domUtils.isRichTextEditorEnabled();
|
||||
} else {
|
||||
// We aren't editing, so no rich text editor.
|
||||
promise = Promise.resolve(false);
|
||||
}
|
||||
|
||||
promise.then((enabled) => {
|
||||
rteEnabled = enabled;
|
||||
|
||||
return this.getText(rteEnabled);
|
||||
}).then((text) => {
|
||||
|
||||
this.text = text;
|
||||
|
||||
if (!this.canEdit && !this.edit) {
|
||||
// User cannot edit the comment. Show it full when clicked.
|
||||
this.element.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (this.text) {
|
||||
// Open a new state with the text.
|
||||
this.textUtils.expandText(this.plugin.name, this.text, this.component, this.assign.cmid);
|
||||
}
|
||||
});
|
||||
} else if (this.edit) {
|
||||
this.control = this.fb.control(text);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the comment.
|
||||
*/
|
||||
editComment(): void {
|
||||
this.editFeedback().then((inputData) => {
|
||||
const text = AddonModAssignFeedbackCommentsHandler.getTextFromInputData(this.textUtils, this.plugin, inputData);
|
||||
|
||||
// Update the text and save it as draft.
|
||||
this.isSent = false;
|
||||
this.text = text;
|
||||
this.feedbackDelegate.saveFeedbackDraft(this.assign.id, this.userId, this.plugin, {
|
||||
text: text,
|
||||
format: 1
|
||||
});
|
||||
}).catch(() => {
|
||||
// User cancelled, nothing to do.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text for the plugin.
|
||||
*
|
||||
* @param {boolean} rteEnabled Whether Rich Text Editor is enabled.
|
||||
* @return {Promise<string>} Promise resolved with the text.
|
||||
*/
|
||||
protected getText(rteEnabled: boolean): Promise<string> {
|
||||
// Check if the user already modified the comment.
|
||||
return this.feedbackDelegate.getPluginDraftData(this.assign.id, this.userId, this.plugin).then((draft) => {
|
||||
if (draft) {
|
||||
this.isSent = false;
|
||||
|
||||
return draft.text;
|
||||
} else {
|
||||
// There is no draft saved. Check if we have anything offline.
|
||||
return this.assignOfflineProvider.getSubmissionGrade(this.assign.id, this.userId).catch(() => {
|
||||
// No offline data found.
|
||||
}).then((offlineData) => {
|
||||
if (offlineData && offlineData.pluginData && offlineData.pluginData.assignfeedbackcomments_editor) {
|
||||
// Save offline as draft.
|
||||
this.isSent = false;
|
||||
this.feedbackDelegate.saveFeedbackDraft(this.assign.id, this.userId, this.plugin,
|
||||
offlineData.pluginData.assignfeedbackcomments_editor);
|
||||
|
||||
return offlineData.pluginData.assignfeedbackcomments_editor.text;
|
||||
}
|
||||
|
||||
// No offline data found, return online text.
|
||||
this.isSent = true;
|
||||
|
||||
return this.assignProvider.getSubmissionPluginText(this.plugin, this.edit && !rteEnabled);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"pluginname": "Feedback comments"
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
|
||||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, Injector } from '@angular/core';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { AddonModAssignProvider } from '../../../providers/assign';
|
||||
import { AddonModAssignOfflineProvider } from '../../../providers/assign-offline';
|
||||
import { AddonModAssignFeedbackHandler } from '../../../providers/feedback-delegate';
|
||||
import { AddonModAssignFeedbackCommentsComponent } from '../component/comments';
|
||||
|
||||
/**
|
||||
* Handler for comments feedback plugin.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModAssignFeedbackCommentsHandler implements AddonModAssignFeedbackHandler {
|
||||
name = 'AddonModAssignFeedbackCommentsHandler';
|
||||
type = 'comments';
|
||||
|
||||
protected drafts = {}; // Store the data in this service so it isn't lost if the user performs a PTR in the page.
|
||||
|
||||
constructor(private sitesProvider: CoreSitesProvider, private domUtils: CoreDomUtilsProvider,
|
||||
private textUtils: CoreTextUtilsProvider, private assignProvider: AddonModAssignProvider,
|
||||
private assignOfflineProvider: AddonModAssignOfflineProvider) { }
|
||||
|
||||
/**
|
||||
* Discard the draft data of the feedback plugin.
|
||||
*
|
||||
* @param {number} assignId The assignment ID.
|
||||
* @param {number} userId User ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} If the function is async, it should return a Promise resolved when done.
|
||||
*/
|
||||
discardDraft(assignId: number, userId: number, siteId?: string): void | Promise<any> {
|
||||
const id = this.getDraftId(assignId, userId, siteId);
|
||||
if (typeof this.drafts[id] != 'undefined') {
|
||||
delete this.drafts[id];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {Injector} injector Injector.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||
*/
|
||||
getComponent(injector: Injector, plugin: any): any | Promise<any> {
|
||||
return AddonModAssignFeedbackCommentsComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the draft saved data of the feedback plugin.
|
||||
*
|
||||
* @param {number} assignId The assignment ID.
|
||||
* @param {number} userId User ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {any|Promise<any>} Data (or promise resolved with the data).
|
||||
*/
|
||||
getDraft(assignId: number, userId: number, siteId?: string): any | Promise<any> {
|
||||
const id = this.getDraftId(assignId, userId, siteId);
|
||||
|
||||
if (typeof this.drafts[id] != 'undefined') {
|
||||
return this.drafts[id];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a draft ID.
|
||||
*
|
||||
* @param {number} assignId The assignment ID.
|
||||
* @param {number} userId User ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {string} Draft ID.
|
||||
*/
|
||||
protected getDraftId(assignId: number, userId: number, siteId?: string): string {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
return siteId + '#' + assignId + '#' + userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text to submit.
|
||||
*
|
||||
* @param {CoreTextUtilsProvider} textUtils Text utils instance.
|
||||
* @param {any} plugin Plugin.
|
||||
* @param {any} inputData Data entered in the feedback edit form.
|
||||
* @return {string} Text to submit.
|
||||
*/
|
||||
static getTextFromInputData(textUtils: CoreTextUtilsProvider, plugin: any, inputData: any): string {
|
||||
const files = plugin.fileareas && plugin.fileareas[0] ? plugin.fileareas[0].files : [];
|
||||
let text = inputData.assignfeedbackcomments_editor;
|
||||
|
||||
// The input data can have a string or an object with text and format. Get the text.
|
||||
if (text && text.text) {
|
||||
text = text.text;
|
||||
}
|
||||
|
||||
return textUtils.restorePluginfileUrls(text, files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the feedback data has changed for this plugin.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the feedback.
|
||||
* @param {number} userId User ID of the submission.
|
||||
* @return {boolean|Promise<boolean>} Boolean (or promise resolved with boolean): whether the data has changed.
|
||||
*/
|
||||
hasDataChanged(assign: any, submission: any, plugin: any, inputData: any, userId: number): boolean | Promise<boolean> {
|
||||
// Get it from plugin or offline.
|
||||
return this.assignOfflineProvider.getSubmissionGrade(assign.id, userId).catch(() => {
|
||||
// No offline data found.
|
||||
}).then((data) => {
|
||||
if (data && data.pluginData && data.pluginData.assignfeedbackcomments_editor) {
|
||||
return data.pluginData.assignfeedbackcomments_editor.text;
|
||||
}
|
||||
|
||||
// No offline data found, get text from plugin.
|
||||
return this.domUtils.isRichTextEditorEnabled().then((enabled) => {
|
||||
return this.assignProvider.getSubmissionPluginText(plugin, !enabled);
|
||||
});
|
||||
}).then((initialText) => {
|
||||
const newText = AddonModAssignFeedbackCommentsHandler.getTextFromInputData(this.textUtils, plugin, inputData);
|
||||
|
||||
if (typeof newText == 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if text has changed.
|
||||
return initialText != newText;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the plugin has draft data stored.
|
||||
*
|
||||
* @param {number} assignId The assignment ID.
|
||||
* @param {number} userId User ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {boolean|Promise<boolean>} Boolean or promise resolved with boolean: whether the plugin has draft data.
|
||||
*/
|
||||
hasDraftData(assignId: number, userId: number, siteId?: string): boolean | Promise<boolean> {
|
||||
const draft = this.getDraft(assignId, userId, siteId);
|
||||
|
||||
return !!draft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare and add to pluginData the data to send to the server based on the draft data saved.
|
||||
*
|
||||
* @param {number} assignId The assignment ID.
|
||||
* @param {number} userId User ID.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} pluginData Object where to store the data to send.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} If the function is async, it should return a Promise resolved when done.
|
||||
*/
|
||||
prepareFeedbackData(assignId: number, userId: number, plugin: any, pluginData: any, siteId?: string): void | Promise<any> {
|
||||
const draft = this.getDraft(assignId, userId, siteId);
|
||||
|
||||
if (draft) {
|
||||
return this.domUtils.isRichTextEditorEnabled().then((enabled) => {
|
||||
if (!enabled) {
|
||||
// Rich text editor not enabled, add some HTML to the text if needed.
|
||||
draft.text = this.textUtils.formatHtmlLines(draft.text);
|
||||
}
|
||||
|
||||
pluginData.assignfeedbackcomments_editor = draft;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save draft data of the feedback plugin.
|
||||
*
|
||||
* @param {number} assignId The assignment ID.
|
||||
* @param {number} userId User ID.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} data The data to save.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} If the function is async, it should return a Promise resolved when done.
|
||||
*/
|
||||
saveDraft(assignId: number, userId: number, plugin: any, data: any, siteId?: string): void | Promise<any> {
|
||||
if (data) {
|
||||
this.drafts[this.getDraftId(assignId, userId, siteId)] = data;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<!-- Read only. -->
|
||||
<ion-item text-wrap *ngIf="files && files.length">
|
||||
<h2>{{plugin.name}}</h2>
|
||||
<div no-lines>
|
||||
<core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" [alwaysDownload]="true"></core-file>
|
||||
</div>
|
||||
</ion-item>
|
|
@ -0,0 +1,44 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 } from '@angular/core';
|
||||
import { ModalController } from 'ionic-angular';
|
||||
import { AddonModAssignProvider } from '../../../providers/assign';
|
||||
import { AddonModAssignFeedbackPluginComponent } from '../../../classes/feedback-plugin-component';
|
||||
|
||||
/**
|
||||
* Component to render a edit pdf feedback plugin.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-assign-feedback-edit-pdf',
|
||||
templateUrl: 'editpdf.html'
|
||||
})
|
||||
export class AddonModAssignFeedbackEditPdfComponent extends AddonModAssignFeedbackPluginComponent implements OnInit {
|
||||
|
||||
component = AddonModAssignProvider.COMPONENT;
|
||||
files: any[];
|
||||
|
||||
constructor(modalCtrl: ModalController, protected assignProvider: AddonModAssignProvider) {
|
||||
super(modalCtrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
if (this.plugin) {
|
||||
this.files = this.assignProvider.getSubmissionPluginAttachments(this.plugin);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { AddonModAssignFeedbackEditPdfHandler } from './providers/handler';
|
||||
import { AddonModAssignFeedbackEditPdfComponent } from './component/editpdf';
|
||||
import { AddonModAssignFeedbackDelegate } from '../../providers/feedback-delegate';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModAssignFeedbackEditPdfComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule
|
||||
],
|
||||
providers: [
|
||||
AddonModAssignFeedbackEditPdfHandler
|
||||
],
|
||||
exports: [
|
||||
AddonModAssignFeedbackEditPdfComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModAssignFeedbackEditPdfComponent
|
||||
]
|
||||
})
|
||||
export class AddonModAssignFeedbackEditPdfModule {
|
||||
constructor(feedbackDelegate: AddonModAssignFeedbackDelegate, handler: AddonModAssignFeedbackEditPdfHandler) {
|
||||
feedbackDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"pluginname": "Annotate PDF"
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
|
||||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, Injector } from '@angular/core';
|
||||
import { AddonModAssignProvider } from '../../../providers/assign';
|
||||
import { AddonModAssignFeedbackHandler } from '../../../providers/feedback-delegate';
|
||||
import { AddonModAssignFeedbackEditPdfComponent } from '../component/editpdf';
|
||||
|
||||
/**
|
||||
* Handler for edit pdf feedback plugin.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModAssignFeedbackEditPdfHandler implements AddonModAssignFeedbackHandler {
|
||||
name = 'AddonModAssignFeedbackEditPdfHandler';
|
||||
type = 'editpdf';
|
||||
|
||||
constructor(private assignProvider: AddonModAssignProvider) { }
|
||||
|
||||
/**
|
||||
* 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 {Injector} injector Injector.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||
*/
|
||||
getComponent(injector: Injector, plugin: any): any | Promise<any> {
|
||||
return AddonModAssignFeedbackEditPdfComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files used by this plugin.
|
||||
* The files returned by this function will be prefetched when the user prefetches the assign.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {any[]|Promise<any[]>} The files (or promise resolved with the files).
|
||||
*/
|
||||
getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise<any[]> {
|
||||
return this.assignProvider.getSubmissionPluginAttachments(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { AddonModAssignFeedbackCommentsModule } from './comments/comments.module';
|
||||
import { AddonModAssignFeedbackEditPdfModule } from './editpdf/editpdf.module';
|
||||
import { AddonModAssignFeedbackFileModule } from './file/file.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
AddonModAssignFeedbackCommentsModule,
|
||||
AddonModAssignFeedbackEditPdfModule,
|
||||
AddonModAssignFeedbackFileModule
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
exports: []
|
||||
})
|
||||
export class AddonModAssignFeedbackModule { }
|
|
@ -0,0 +1,7 @@
|
|||
<!-- Read only. -->
|
||||
<ion-item text-wrap *ngIf="files && files.length">
|
||||
<h2>{{plugin.name}}</h2>
|
||||
<div no-lines>
|
||||
<core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="assign.cmid" [alwaysDownload]="true"></core-file>
|
||||
</div>
|
||||
</ion-item>
|
|
@ -0,0 +1,44 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 } from '@angular/core';
|
||||
import { ModalController } from 'ionic-angular';
|
||||
import { AddonModAssignProvider } from '../../../providers/assign';
|
||||
import { AddonModAssignFeedbackPluginComponent } from '../../../classes/feedback-plugin-component';
|
||||
|
||||
/**
|
||||
* Component to render a file feedback plugin.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-assign-feedback-file',
|
||||
templateUrl: 'file.html'
|
||||
})
|
||||
export class AddonModAssignFeedbackFileComponent extends AddonModAssignFeedbackPluginComponent implements OnInit {
|
||||
|
||||
component = AddonModAssignProvider.COMPONENT;
|
||||
files: any[];
|
||||
|
||||
constructor(modalCtrl: ModalController, protected assignProvider: AddonModAssignProvider) {
|
||||
super(modalCtrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
if (this.plugin) {
|
||||
this.files = this.assignProvider.getSubmissionPluginAttachments(this.plugin);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { AddonModAssignFeedbackFileHandler } from './providers/handler';
|
||||
import { AddonModAssignFeedbackFileComponent } from './component/file';
|
||||
import { AddonModAssignFeedbackDelegate } from '../../providers/feedback-delegate';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModAssignFeedbackFileComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule
|
||||
],
|
||||
providers: [
|
||||
AddonModAssignFeedbackFileHandler
|
||||
],
|
||||
exports: [
|
||||
AddonModAssignFeedbackFileComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModAssignFeedbackFileComponent
|
||||
]
|
||||
})
|
||||
export class AddonModAssignFeedbackFileModule {
|
||||
constructor(feedbackDelegate: AddonModAssignFeedbackDelegate, handler: AddonModAssignFeedbackFileHandler) {
|
||||
feedbackDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"pluginname": "File feedback"
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
|
||||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, Injector } from '@angular/core';
|
||||
import { AddonModAssignProvider } from '../../../providers/assign';
|
||||
import { AddonModAssignFeedbackHandler } from '../../../providers/feedback-delegate';
|
||||
import { AddonModAssignFeedbackFileComponent } from '../component/file';
|
||||
|
||||
/**
|
||||
* Handler for file feedback plugin.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModAssignFeedbackFileHandler implements AddonModAssignFeedbackHandler {
|
||||
name = 'AddonModAssignFeedbackFileHandler';
|
||||
type = 'file';
|
||||
|
||||
constructor(private assignProvider: AddonModAssignProvider) { }
|
||||
|
||||
/**
|
||||
* 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 {Injector} injector Injector.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||
*/
|
||||
getComponent(injector: Injector, plugin: any): any | Promise<any> {
|
||||
return AddonModAssignFeedbackFileComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files used by this plugin.
|
||||
* The files returned by this function will be prefetched when the user prefetches the assign.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {any[]|Promise<any[]>} The files (or promise resolved with the files).
|
||||
*/
|
||||
getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise<any[]> {
|
||||
return this.assignProvider.getSubmissionPluginAttachments(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
{
|
||||
"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",
|
||||
"gradenotsynced": "Grade not synced",
|
||||
"gradedon": "Graded on",
|
||||
"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",
|
||||
"multipleteams": "Member of more than one group",
|
||||
"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",
|
||||
"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>",
|
||||
"savechanges": "Save changes",
|
||||
"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",
|
||||
"submissionstatusheading": "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",
|
||||
"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.",
|
||||
"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,16 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title><core-format-text [text]="plugin.name"></core-format-text></ion-title>
|
||||
<ion-buttons end>
|
||||
<button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon name="close"></ion-icon>
|
||||
</button>
|
||||
</ion-buttons>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<form name="addon-mod_assign-edit-feedback-form" *ngIf="userId && plugin">
|
||||
<addon-mod-assign-feedback-plugin [assign]="assign" [submission]="submission" [userId]="userId" [plugin]="plugin" [edit]="true"></addon-mod-assign-feedback-plugin>
|
||||
<button ion-button block (click)="done()">{{ 'core.done' | translate }}</button>
|
||||
</form>
|
||||
</ion-content>
|
|
@ -0,0 +1,35 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { IonicPageModule } from 'ionic-angular';
|
||||
import { AddonModAssignEditFeedbackModalPage } from './edit-feedback-modal';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { AddonModAssignComponentsModule } from '../../components/components.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModAssignEditFeedbackModalPage
|
||||
],
|
||||
imports: [
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
AddonModAssignComponentsModule,
|
||||
IonicPageModule.forChild(AddonModAssignEditFeedbackModalPage),
|
||||
TranslateModule.forChild()
|
||||
]
|
||||
})
|
||||
export class AddonModAssignEditFeedbackModalPageModule {}
|
|
@ -0,0 +1,103 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, Input } from '@angular/core';
|
||||
import { IonicPage, ViewController, NavParams } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { AddonModAssignFeedbackDelegate } from '../../providers/feedback-delegate';
|
||||
|
||||
/**
|
||||
* Modal that allows editing a feedback plugin.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-mod-assign-edit-feedback-modal' })
|
||||
@Component({
|
||||
selector: 'page-addon-mod-assign-edit-feedback-modal',
|
||||
templateUrl: 'edit-feedback-modal.html',
|
||||
})
|
||||
export class AddonModAssignEditFeedbackModalPage {
|
||||
|
||||
@Input() assign: any; // The assignment.
|
||||
@Input() submission: any; // The submission.
|
||||
@Input() plugin: any; // The plugin object.
|
||||
@Input() userId: number; // The user ID of the submission.
|
||||
|
||||
protected forceLeave = false; // To allow leaving the page without checking for changes.
|
||||
|
||||
constructor(params: NavParams, protected viewCtrl: ViewController, protected domUtils: CoreDomUtilsProvider,
|
||||
protected translate: TranslateService, protected feedbackDelegate: AddonModAssignFeedbackDelegate) {
|
||||
|
||||
this.assign = params.get('assign');
|
||||
this.submission = params.get('submission');
|
||||
this.plugin = params.get('plugin');
|
||||
this.userId = params.get('userId');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can leave the page or not.
|
||||
*
|
||||
* @return {boolean|Promise<void>} Resolved if we can leave it, rejected if not.
|
||||
*/
|
||||
ionViewCanLeave(): boolean | Promise<void> {
|
||||
if (this.forceLeave) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.hasDataChanged().then((changed) => {
|
||||
if (changed) {
|
||||
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal.
|
||||
*
|
||||
* @param {any} data Data to return to the page.
|
||||
*/
|
||||
closeModal(data: any): void {
|
||||
this.viewCtrl.dismiss(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Done editing.
|
||||
*/
|
||||
done(): void {
|
||||
// Close the modal, sending the input data.
|
||||
this.forceLeave = true;
|
||||
this.closeModal(this.getInputData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the input data.
|
||||
*
|
||||
* @return {any} Object with the data.
|
||||
*/
|
||||
protected getInputData(): any {
|
||||
return this.domUtils.getDataFromForm(document.forms['addon-mod_assign-edit-feedback-form']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if data has changed.
|
||||
*
|
||||
* @return {Promise<boolean>} Promise resolved with boolean: whether the data has changed.
|
||||
*/
|
||||
protected hasDataChanged(): Promise<boolean> {
|
||||
return this.feedbackDelegate.hasPluginDataChanged(this.assign, this.userId, this.plugin, this.getInputData(), this.userId)
|
||||
.catch(() => {
|
||||
// Ignore errors.
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
|
||||
|
||||
<ion-buttons end>
|
||||
<button ion-button clear (click)="save()" [attr.aria-label]="'core.save' | translate">
|
||||
{{ 'core.save' | translate }}
|
||||
</button>
|
||||
</ion-buttons>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<ion-list>
|
||||
<!-- @todo: plagiarism_print_disclosure -->
|
||||
<form name="addon-mod_assign-edit-form" *ngIf="userSubmission && userSubmission.plugins && userSubmission.plugins.length">
|
||||
<!-- Submission statement. -->
|
||||
<ion-item text-wrap *ngIf="submissionStatement">
|
||||
<ion-label><core-format-text [text]="submissionStatement"></core-format-text></ion-label>
|
||||
<ion-checkbox item-end name="submissionstatement" [(ngModel)]="submissionStatementAccepted"></ion-checkbox>
|
||||
|
||||
<!-- ion-checkbox doesn't use an input. Create a hidden input to hold the value. -->
|
||||
<input item-content type="hidden" [ngModel]="submissionStatementAccepted" name="submissionstatement">
|
||||
</ion-item>
|
||||
|
||||
<addon-mod-assign-submission-plugin *ngFor="let plugin of userSubmission.plugins" [assign]="assign" [submission]="userSubmission" [plugin]="plugin" [edit]="true" [allowOffline]="allowOffline"></addon-mod-assign-submission-plugin>
|
||||
</form>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,35 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { IonicPageModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { AddonModAssignComponentsModule } from '../../components/components.module';
|
||||
import { AddonModAssignEditPage } from './edit';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModAssignEditPage,
|
||||
],
|
||||
imports: [
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
AddonModAssignComponentsModule,
|
||||
IonicPageModule.forChild(AddonModAssignEditPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class AddonModAssignEditPageModule {}
|
|
@ -0,0 +1,339 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { IonicPage, NavController, NavParams } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSyncProvider } from '@providers/sync';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreFileUploaderHelperProvider } from '@core/fileuploader/providers/helper';
|
||||
import { AddonModAssignProvider } from '../../providers/assign';
|
||||
import { AddonModAssignOfflineProvider } from '../../providers/assign-offline';
|
||||
import { AddonModAssignSyncProvider } from '../../providers/assign-sync';
|
||||
import { AddonModAssignHelperProvider } from '../../providers/helper';
|
||||
|
||||
/**
|
||||
* Page that allows adding or editing an assigment submission.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-mod-assign-edit' })
|
||||
@Component({
|
||||
selector: 'page-addon-mod-assign-edit',
|
||||
templateUrl: 'edit.html',
|
||||
})
|
||||
export class AddonModAssignEditPage implements OnInit, OnDestroy {
|
||||
title: string; // Title to display.
|
||||
assign: any; // Assignment.
|
||||
courseId: number; // Course ID the assignment belongs to.
|
||||
userSubmission: any; // The user submission.
|
||||
allowOffline: boolean; // Whether offline is allowed.
|
||||
submissionStatement: string; // The submission statement.
|
||||
submissionStatementAccepted: boolean; // Whether submission statement is accepted.
|
||||
loaded: boolean; // Whether data has been loaded.
|
||||
|
||||
protected moduleId: number; // Module ID the submission belongs to.
|
||||
protected userId: number; // User doing the submission.
|
||||
protected isBlind: boolean; // Whether blind is used.
|
||||
protected editText: string; // "Edit submission" translated text.
|
||||
protected saveOffline = false; // Whether to save data in offline.
|
||||
protected hasOffline = false; // Whether the assignment has offline data.
|
||||
protected isDestroyed = false; // Whether the component has been destroyed.
|
||||
protected forceLeave = false; // To allow leaving the page without checking for changes.
|
||||
|
||||
constructor(navParams: NavParams, protected navCtrl: NavController, protected sitesProvider: CoreSitesProvider,
|
||||
protected syncProvider: CoreSyncProvider, protected domUtils: CoreDomUtilsProvider,
|
||||
protected translate: TranslateService, protected fileUploaderHelper: CoreFileUploaderHelperProvider,
|
||||
protected eventsProvider: CoreEventsProvider, protected assignProvider: AddonModAssignProvider,
|
||||
protected assignOfflineProvider: AddonModAssignOfflineProvider, protected assignHelper: AddonModAssignHelperProvider,
|
||||
protected assignSyncProvider: AddonModAssignSyncProvider) {
|
||||
|
||||
this.moduleId = navParams.get('moduleId');
|
||||
this.courseId = navParams.get('courseId');
|
||||
this.userId = sitesProvider.getCurrentSiteUserId(); // Right now we can only edit current user's submissions.
|
||||
this.isBlind = !!navParams.get('blindId');
|
||||
|
||||
this.editText = translate.instant('addon.mod_assign.editsubmission');
|
||||
this.title = this.editText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.fetchAssignment().finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can leave the page or not.
|
||||
*
|
||||
* @return {boolean|Promise<void>} Resolved if we can leave it, rejected if not.
|
||||
*/
|
||||
ionViewCanLeave(): boolean | Promise<void> {
|
||||
if (this.forceLeave) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if data has changed.
|
||||
return this.hasDataChanged().then((changed) => {
|
||||
if (changed) {
|
||||
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
|
||||
}
|
||||
}).then(() => {
|
||||
// Nothing has changed or user confirmed to leave. Clear temporary data from plugins.
|
||||
this.assignHelper.clearSubmissionPluginTmpData(this.assign, this.userSubmission, this.getInputData());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch assignment data.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchAssignment(): Promise<any> {
|
||||
const currentUserId = this.sitesProvider.getCurrentSiteUserId();
|
||||
|
||||
// Get assignment data.
|
||||
return this.assignProvider.getAssignment(this.courseId, this.moduleId).then((assign) => {
|
||||
this.assign = assign;
|
||||
this.title = assign.name || this.title;
|
||||
|
||||
if (!this.isDestroyed) {
|
||||
// Block the assignment.
|
||||
this.syncProvider.blockOperation(AddonModAssignProvider.COMPONENT, assign.id);
|
||||
}
|
||||
|
||||
// Wait for sync to be over (if any).
|
||||
return this.assignSyncProvider.waitForSync(assign.id);
|
||||
}).then(() => {
|
||||
|
||||
// Get submission status. Ignore cache to get the latest data.
|
||||
return this.assignProvider.getSubmissionStatus(this.assign.id, this.userId, this.isBlind, false, true).catch((err) => {
|
||||
// Cannot connect. Get cached data.
|
||||
return this.assignProvider.getSubmissionStatus(this.assign.id, this.userId, this.isBlind).then((response) => {
|
||||
const userSubmission = this.assignProvider.getSubmissionObjectFromAttempt(this.assign, response.lastattempt);
|
||||
|
||||
// Check if the user can edit it in offline.
|
||||
return this.assignHelper.canEditSubmissionOffline(this.assign, userSubmission).then((canEditOffline) => {
|
||||
if (canEditOffline) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Submission cannot be edited in offline, reject.
|
||||
this.allowOffline = false;
|
||||
|
||||
return Promise.reject(err);
|
||||
});
|
||||
});
|
||||
}).then((response) => {
|
||||
if (!response.lastattempt.canedit) {
|
||||
// Can't edit. Reject.
|
||||
return Promise.reject(this.translate.instant('core.nopermissions', {$a: this.editText}));
|
||||
}
|
||||
|
||||
this.userSubmission = this.assignProvider.getSubmissionObjectFromAttempt(this.assign, response.lastattempt);
|
||||
this.allowOffline = true; // If offline isn't allowed we shouldn't have reached this point.
|
||||
|
||||
// Only show submission statement if we are editing our own submission.
|
||||
if (this.assign.requiresubmissionstatement && !this.assign.submissiondrafts && this.userId == currentUserId) {
|
||||
this.submissionStatement = this.assign.submissionstatement;
|
||||
} else {
|
||||
this.submissionStatement = undefined;
|
||||
}
|
||||
|
||||
// Check if there's any offline data for this submission.
|
||||
return this.assignOfflineProvider.getSubmission(this.assign.id, this.userId).then((data) => {
|
||||
this.hasOffline = data && data.pluginData && Object.keys(data.pluginData).length > 0;
|
||||
}).catch(() => {
|
||||
// No offline data found.
|
||||
this.hasOffline = false;
|
||||
});
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'Error getting assigment data.');
|
||||
|
||||
// Leave the player.
|
||||
this.leaveWithoutCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the input data.
|
||||
*
|
||||
* @return {any} Input data.
|
||||
*/
|
||||
protected getInputData(): any {
|
||||
return this.domUtils.getDataFromForm(document.forms['addon-mod_assign-edit-form']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if data has changed.
|
||||
*
|
||||
* @return {Promise<boolean>} Promise resolved with boolean: whether data has changed.
|
||||
*/
|
||||
protected hasDataChanged(): Promise<boolean> {
|
||||
// Usually the hasSubmissionDataChanged call will be resolved inmediately, causing the modal to be shown just an instant.
|
||||
// We'll wait a bit before showing it to prevent this "blink".
|
||||
let modal,
|
||||
showModal = true;
|
||||
|
||||
setTimeout(() => {
|
||||
if (showModal) {
|
||||
modal = this.domUtils.showModalLoading();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
const data = this.getInputData();
|
||||
|
||||
return this.assignHelper.hasSubmissionDataChanged(this.assign, this.userSubmission, data).finally(() => {
|
||||
if (modal) {
|
||||
modal.dismiss();
|
||||
} else {
|
||||
showModal = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Leave the view without checking for changes.
|
||||
*/
|
||||
protected leaveWithoutCheck(): void {
|
||||
this.forceLeave = true;
|
||||
this.navCtrl.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data to submit based on the input data.
|
||||
*
|
||||
* @param {any} inputData The input data.
|
||||
* @return {Promise<any>} Promise resolved with the data to submit.
|
||||
*/
|
||||
protected prepareSubmissionData(inputData: any): Promise<any> {
|
||||
// If there's offline data, always save it in offline.
|
||||
this.saveOffline = this.hasOffline;
|
||||
|
||||
return this.assignHelper.prepareSubmissionPluginData(this.assign, this.userSubmission, inputData, this.hasOffline)
|
||||
.catch((error) => {
|
||||
|
||||
if (this.allowOffline && !this.saveOffline) {
|
||||
// Cannot submit in online, prepare for offline usage.
|
||||
this.saveOffline = true;
|
||||
|
||||
return this.assignHelper.prepareSubmissionPluginData(this.assign, this.userSubmission, inputData, true);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the submission.
|
||||
*/
|
||||
save(): void {
|
||||
// Check if data has changed.
|
||||
this.hasDataChanged().then((changed) => {
|
||||
if (changed) {
|
||||
this.saveSubmission().then(() => {
|
||||
this.leaveWithoutCheck();
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'Error saving submission.');
|
||||
});
|
||||
} else {
|
||||
// Nothing to save, just go back.
|
||||
this.leaveWithoutCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the submission.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected saveSubmission(): Promise<any> {
|
||||
const inputData = this.getInputData();
|
||||
|
||||
if (this.submissionStatement && (!inputData.submissionstatement || inputData.submissionstatement === 'false')) {
|
||||
return Promise.reject(this.translate.instant('addon.mod_assign.acceptsubmissionstatement'));
|
||||
}
|
||||
|
||||
let modal = this.domUtils.showModalLoading();
|
||||
|
||||
// Get size to ask for confirmation.
|
||||
return this.assignHelper.getSubmissionSizeForEdit(this.assign, this.userSubmission, inputData).catch(() => {
|
||||
// Error calculating size, return -1.
|
||||
return -1;
|
||||
}).then((size) => {
|
||||
modal.dismiss();
|
||||
|
||||
// Confirm action.
|
||||
return this.fileUploaderHelper.confirmUploadFile(size, true, this.allowOffline);
|
||||
}).then(() => {
|
||||
modal = this.domUtils.showModalLoading('core.sending', true);
|
||||
|
||||
return this.prepareSubmissionData(inputData).then((pluginData) => {
|
||||
if (!Object.keys(pluginData).length) {
|
||||
// Nothing to save.
|
||||
return;
|
||||
}
|
||||
|
||||
let promise;
|
||||
|
||||
if (this.saveOffline) {
|
||||
// Save submission in offline.
|
||||
promise = this.assignOfflineProvider.saveSubmission(this.assign.id, this.courseId, pluginData,
|
||||
this.userSubmission.timemodified, !this.assign.submissiondrafts, this.userId);
|
||||
} else {
|
||||
// Try to send it to server.
|
||||
promise = this.assignProvider.saveSubmission(this.assign.id, this.courseId, pluginData, this.allowOffline,
|
||||
this.userSubmission.timemodified, this.assign.submissiondrafts, this.userId);
|
||||
}
|
||||
|
||||
return promise.then(() => {
|
||||
// Submission saved, trigger event.
|
||||
const params = {
|
||||
assignmentId: this.assign.id,
|
||||
submissionId: this.userSubmission.id,
|
||||
userId: this.userId,
|
||||
};
|
||||
|
||||
this.eventsProvider.trigger(AddonModAssignProvider.SUBMISSION_SAVED_EVENT, params,
|
||||
this.sitesProvider.getCurrentSiteId());
|
||||
|
||||
if (!this.assign.submissiondrafts) {
|
||||
// No drafts allowed, so it was submitted. Trigger event.
|
||||
this.eventsProvider.trigger(AddonModAssignProvider.SUBMITTED_FOR_GRADING_EVENT, params,
|
||||
this.sitesProvider.getCurrentSiteId());
|
||||
}
|
||||
});
|
||||
});
|
||||
}).finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.isDestroyed = false;
|
||||
|
||||
// Unblock the assignment.
|
||||
if (this.assign) {
|
||||
this.syncProvider.unblockOperation(AddonModAssignProvider.COMPONENT, this.assign.id);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
|
||||
|
||||
<ion-buttons end>
|
||||
<!-- The buttons defined by the component will be added in here. -->
|
||||
</ion-buttons>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher [enabled]="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,33 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { IonicPageModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { AddonModAssignComponentsModule } from '../../components/components.module';
|
||||
import { AddonModAssignIndexPage } from './index';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModAssignIndexPage,
|
||||
],
|
||||
imports: [
|
||||
CoreDirectivesModule,
|
||||
AddonModAssignComponentsModule,
|
||||
IonicPageModule.forChild(AddonModAssignIndexPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class AddonModAssignIndexPageModule {}
|
|
@ -0,0 +1,48 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, ViewChild } from '@angular/core';
|
||||
import { IonicPage, NavParams } from 'ionic-angular';
|
||||
import { AddonModAssignIndexComponent } from '../../components/index/index';
|
||||
|
||||
/**
|
||||
* Page that displays an assign.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-mod-assign-index' })
|
||||
@Component({
|
||||
selector: 'page-addon-mod-assign-index',
|
||||
templateUrl: 'index.html',
|
||||
})
|
||||
export class AddonModAssignIndexPage {
|
||||
@ViewChild(AddonModAssignIndexComponent) assignComponent: AddonModAssignIndexComponent;
|
||||
|
||||
title: string;
|
||||
module: any;
|
||||
courseId: number;
|
||||
|
||||
constructor(navParams: NavParams) {
|
||||
this.module = navParams.get('module') || {};
|
||||
this.courseId = navParams.get('courseId');
|
||||
this.title = this.module.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update some data based on the assign instance.
|
||||
*
|
||||
* @param {any} assign Assign instance.
|
||||
*/
|
||||
updateData(assign: any): void {
|
||||
this.title = assign.name || this.title;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
|
||||
|
||||
<ion-buttons end></ion-buttons>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<core-split-view>
|
||||
<ion-content>
|
||||
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshList($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<core-empty-box *ngIf="!submissions || submissions.length == 0" icon="paper" [message]="'addon.mod_assign.submissionstatus_' | translate">
|
||||
</core-empty-box>
|
||||
|
||||
<ion-list>
|
||||
<!-- List of submissions. -->
|
||||
<ng-container *ngFor="let submission of submissions">
|
||||
<a ion-item text-wrap (click)="loadSubmission(submission)" [class.core-split-item-selected]="submission.id == selectedSubmissionId">
|
||||
<ion-avatar item-start *ngIf="submission.userprofileimageurl">
|
||||
<img [src]="submission.userprofileimageurl" [alt]="'core.pictureof' | translate:{$a: submission.userfullname}" core-external-content role="presentation" onError="this.src='assets/img/user-avatar.png'">
|
||||
</ion-avatar>
|
||||
<h2 *ngIf="submission.userfullname">{{submission.userfullname}}</h2>
|
||||
<h2 *ngIf="!submission.userfullname">{{ 'addon.mod_assign.hiddenuser' | translate }}{{submission.blindid}}</h2>
|
||||
<p *ngIf="assign.teamsubmission">
|
||||
<span *ngIf="submission.groupname">{{submission.groupname}}</span>
|
||||
<span *ngIf="assign.preventsubmissionnotingroup && !submission.groupname && !submission.manyGroups && !submission.blindid">{{ 'addon.mod_assign.noteam' | translate }}</span>
|
||||
<span *ngIf="assign.preventsubmissionnotingroup && !submission.groupname && submission.manyGroups && !submission.blindid">{{ 'addon.mod_assign.multipleteams' | translate }}</span>
|
||||
<span *ngIf="!assign.preventsubmissionnotingroup && !submission.groupname">{{ 'addon.mod_assign.defaultteam' | translate }}</span>
|
||||
</p>
|
||||
<ion-badge text-center [color]="submission.statusColor" *ngIf="submission.statusTranslated">
|
||||
{{ submission.statusTranslated }}
|
||||
</ion-badge>
|
||||
<ion-badge text-center [color]="submission.gradingColor" *ngIf="submission.gradingStatusTranslationId">
|
||||
{{ submission.gradingStatusTranslationId | translate }}
|
||||
</ion-badge>
|
||||
</a>
|
||||
</ng-container>
|
||||
|
||||
<ion-item text-wrap class="core-warning-card" *ngIf="!haveAllParticipants" icon-start>
|
||||
<ion-icon name="warning"></ion-icon>
|
||||
{{ 'addon.mod_assign.notallparticipantsareshown' | translate }}
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
</ion-content>
|
||||
</core-split-view>
|
|
@ -0,0 +1,33 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { IonicPageModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { AddonModAssignSubmissionListPage } from './submission-list';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModAssignSubmissionListPage,
|
||||
],
|
||||
imports: [
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
IonicPageModule.forChild(AddonModAssignSubmissionListPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class AddonModAssignSubmissionListPageModule {}
|
|
@ -0,0 +1,273 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { IonicPage, NavParams } from 'ionic-angular';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { AddonModAssignProvider } from '../../providers/assign';
|
||||
import { AddonModAssignOfflineProvider } from '../../providers/assign-offline';
|
||||
import { AddonModAssignHelperProvider } from '../../providers/helper';
|
||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||
|
||||
/**
|
||||
* Page that displays a list of submissions of an assignment.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-mod-assign-submission-list' })
|
||||
@Component({
|
||||
selector: 'page-addon-mod-assign-submission-list',
|
||||
templateUrl: 'submission-list.html',
|
||||
})
|
||||
export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
|
||||
@ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
|
||||
|
||||
title: string; // Title to display.
|
||||
assign: any; // Assignment.
|
||||
submissions: any[]; // List of submissions
|
||||
loaded: boolean; // Whether data has been loaded.
|
||||
haveAllParticipants: boolean; // Whether all participants have been loaded.
|
||||
selectedSubmissionId: number; // Selected submission ID.
|
||||
|
||||
protected moduleId: number; // Module ID the submission belongs to.
|
||||
protected courseId: number; // Course ID the assignment belongs to.
|
||||
protected selectedStatus: string; // The status to see.
|
||||
protected gradedObserver; // Observer to refresh data when a grade changes.
|
||||
|
||||
constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider,
|
||||
protected domUtils: CoreDomUtilsProvider, protected translate: TranslateService,
|
||||
protected assignProvider: AddonModAssignProvider, protected assignOfflineProvider: AddonModAssignOfflineProvider,
|
||||
protected assignHelper: AddonModAssignHelperProvider) {
|
||||
|
||||
this.moduleId = navParams.get('moduleId');
|
||||
this.courseId = navParams.get('courseId');
|
||||
this.selectedStatus = navParams.get('status');
|
||||
|
||||
if (this.selectedStatus) {
|
||||
if (this.selectedStatus == AddonModAssignProvider.NEED_GRADING) {
|
||||
this.title = this.translate.instant('addon.mod_assign.numberofsubmissionsneedgrading');
|
||||
} else {
|
||||
this.title = this.translate.instant('addon.mod_assign.submissionstatus_' + this.selectedStatus);
|
||||
}
|
||||
} else {
|
||||
this.title = this.translate.instant('addon.mod_assign.numberofparticipants');
|
||||
}
|
||||
|
||||
// Update data if some grade changes.
|
||||
this.gradedObserver = eventsProvider.on(AddonModAssignProvider.GRADED_EVENT, (data) => {
|
||||
if (this.assign && data.assignmentId == this.assign.id && data.userId == sitesProvider.getCurrentSiteUserId()) {
|
||||
// Grade changed, refresh the data.
|
||||
this.loaded = false;
|
||||
|
||||
this.refreshAllData().finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
}, sitesProvider.getCurrentSiteId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.fetchAssignment().finally(() => {
|
||||
if (!this.selectedSubmissionId && this.splitviewCtrl.isOn() && this.submissions.length > 0) {
|
||||
// Take first and load it.
|
||||
this.loadSubmission(this.submissions[0]);
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can leave the page or not.
|
||||
*
|
||||
* @return {boolean|Promise<void>} Resolved if we can leave it, rejected if not.
|
||||
*/
|
||||
ionViewCanLeave(): boolean | Promise<void> {
|
||||
// If split view is enabled, check if we can leave the details page.
|
||||
if (this.splitviewCtrl.isOn()) {
|
||||
const detailsPage = this.splitviewCtrl.getDetailsNav().getActive().instance;
|
||||
if (detailsPage && detailsPage.ionViewCanLeave) {
|
||||
return detailsPage.ionViewCanLeave();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch assignment data.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchAssignment(): Promise<any> {
|
||||
let participants,
|
||||
submissionsData;
|
||||
|
||||
// Get assignment data.
|
||||
return this.assignProvider.getAssignment(this.courseId, this.moduleId).then((assign) => {
|
||||
this.title = assign.name || this.title;
|
||||
this.assign = assign;
|
||||
this.haveAllParticipants = true;
|
||||
|
||||
// Get assignment submissions.
|
||||
return this.assignProvider.getSubmissions(assign.id);
|
||||
}).then((data) => {
|
||||
if (!data.canviewsubmissions) {
|
||||
// User shouldn't be able to reach here.
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
submissionsData = data;
|
||||
|
||||
// Get the participants.
|
||||
return this.assignHelper.getParticipants(this.assign).then((parts) => {
|
||||
this.haveAllParticipants = true;
|
||||
participants = parts;
|
||||
}).catch(() => {
|
||||
this.haveAllParticipants = false;
|
||||
});
|
||||
}).then(() => {
|
||||
// We want to show the user data on each submission.
|
||||
return this.assignProvider.getSubmissionsUserData(submissionsData.submissions, this.courseId, this.assign.id,
|
||||
this.assign.blindmarking && !this.assign.revealidentities, participants);
|
||||
}).then((submissions) => {
|
||||
|
||||
// Filter the submissions to get only the ones with the right status and add some extra data.
|
||||
const getNeedGrading = this.selectedStatus == AddonModAssignProvider.NEED_GRADING,
|
||||
searchStatus = getNeedGrading ? AddonModAssignProvider.SUBMISSION_STATUS_SUBMITTED : this.selectedStatus,
|
||||
promises = [];
|
||||
|
||||
this.submissions = [];
|
||||
submissions.forEach((submission) => {
|
||||
if (!searchStatus || searchStatus == submission.status) {
|
||||
promises.push(this.assignOfflineProvider.getSubmissionGrade(this.assign.id, submission.userid).catch(() => {
|
||||
// Ignore errors.
|
||||
}).then((data) => {
|
||||
let promise,
|
||||
notSynced = false;
|
||||
|
||||
// Load offline grades.
|
||||
if (data && submission.timemodified < data.timemodified) {
|
||||
notSynced = true;
|
||||
}
|
||||
|
||||
if (getNeedGrading) {
|
||||
// Only show the submissions that need to be graded.
|
||||
promise = this.assignProvider.needsSubmissionToBeGraded(submission, this.assign.id);
|
||||
} else {
|
||||
promise = Promise.resolve(true);
|
||||
}
|
||||
|
||||
return promise.then((add) => {
|
||||
if (!add) {
|
||||
return;
|
||||
}
|
||||
|
||||
submission.statusColor = this.assignProvider.getSubmissionStatusColor(submission.status);
|
||||
submission.gradingColor = this.assignProvider.getSubmissionGradingStatusColor(submission.gradingstatus);
|
||||
|
||||
// Show submission status if not submitted for grading.
|
||||
if (submission.statusColor != 'success' || !submission.gradingstatus) {
|
||||
submission.statusTranslated = this.translate.instant('addon.mod_assign.submissionstatus_' +
|
||||
submission.status);
|
||||
} else {
|
||||
submission.statusTranslated = false;
|
||||
}
|
||||
|
||||
if (notSynced) {
|
||||
submission.gradingStatusTranslationId = 'addon.mod_assign.gradenotsynced';
|
||||
submission.gradingColor = '';
|
||||
} else if (submission.statusColor != 'danger' || submission.gradingColor != 'danger') {
|
||||
// Show grading status if one of the statuses is not done.
|
||||
submission.gradingStatusTranslationId =
|
||||
this.assignProvider.getSubmissionGradingStatusTranslationId(submission.gradingstatus);
|
||||
} else {
|
||||
submission.gradingStatusTranslationId = false;
|
||||
}
|
||||
|
||||
this.submissions.push(submission);
|
||||
});
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'Error getting assigment data.');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a certain submission.
|
||||
*
|
||||
* @param {any} submission The submission to load.
|
||||
*/
|
||||
loadSubmission(submission: any): void {
|
||||
if (this.selectedSubmissionId === submission.id && this.splitviewCtrl.isOn()) {
|
||||
// Already selected.
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedSubmissionId = submission.id;
|
||||
|
||||
this.splitviewCtrl.push('AddonModAssignSubmissionReviewPage', {
|
||||
courseId: this.courseId,
|
||||
moduleId: this.moduleId,
|
||||
submitId: submission.submitid,
|
||||
blindId: submission.blindid
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh all the data.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected refreshAllData(): Promise<any> {
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.assignProvider.invalidateAssignmentData(this.courseId));
|
||||
if (this.assign) {
|
||||
promises.push(this.assignProvider.invalidateAllSubmissionData(this.assign.id));
|
||||
promises.push(this.assignProvider.invalidateAssignmentUserMappingsData(this.assign.id));
|
||||
promises.push(this.assignProvider.invalidateListParticipantsData(this.assign.id));
|
||||
}
|
||||
|
||||
return Promise.all(promises).finally(() => {
|
||||
return this.fetchAssignment();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the list.
|
||||
*
|
||||
* @param {any} refresher Refresher.
|
||||
*/
|
||||
refreshList(refresher: any): void {
|
||||
this.refreshAllData().finally(() => {
|
||||
refresher.complete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.gradedObserver && this.gradedObserver.off();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
|
||||
|
||||
<ion-buttons end></ion-buttons>
|
||||
</ion-navbar>
|
||||
|
||||
<core-navbar-buttons end>
|
||||
<button [hidden]="!canSaveGrades" ion-button button-clear (click)="submitGrade()" [attr.aria-label]="'core.done' | translate">
|
||||
{{ 'core.done' | translate }}
|
||||
</button>
|
||||
</core-navbar-buttons>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
|
||||
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshSubmission($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<addon-mod-assign-submission [courseId]="courseId" [moduleId]="moduleId" [submitId]="submitId" [blindId]="blindId" [showGrade]="showGrade"></addon-mod-assign-submission>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,35 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { IonicPageModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { AddonModAssignComponentsModule } from '../../components/components.module';
|
||||
import { AddonModAssignSubmissionReviewPage } from './submission-review';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModAssignSubmissionReviewPage,
|
||||
],
|
||||
imports: [
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
AddonModAssignComponentsModule,
|
||||
IonicPageModule.forChild(AddonModAssignSubmissionReviewPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class AddonModAssignSubmissionReviewPageModule {}
|
|
@ -0,0 +1,154 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { IonicPage, NavController, NavParams } from 'ionic-angular';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
import { AddonModAssignProvider } from '../../providers/assign';
|
||||
import { AddonModAssignSubmissionComponent } from '../../components/submission/submission';
|
||||
|
||||
/**
|
||||
* Page that displays a submission.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-mod-assign-submission-review' })
|
||||
@Component({
|
||||
selector: 'page-addon-mod-assign-submission-review',
|
||||
templateUrl: 'submission-review.html',
|
||||
})
|
||||
export class AddonModAssignSubmissionReviewPage implements OnInit {
|
||||
@ViewChild(AddonModAssignSubmissionComponent) submissionComponent: AddonModAssignSubmissionComponent;
|
||||
|
||||
title: string; // Title to display.
|
||||
moduleId: number; // Module ID the submission belongs to.
|
||||
courseId: number; // Course ID the assignment belongs to.
|
||||
submitId: number; // User that did the submission.
|
||||
blindId: number; // Blinded user ID (if it's blinded).
|
||||
showGrade: boolean; // Whether to display the grade at start.
|
||||
loaded: boolean; // Whether data has been loaded.
|
||||
canSaveGrades: boolean; // Whether the user can save grades.
|
||||
|
||||
protected assign: any; // The assignment the submission belongs to.
|
||||
protected blindMarking: boolean; // Whether it uses blind marking.
|
||||
protected forceLeave = false; // To allow leaving the page without checking for changes.
|
||||
|
||||
constructor(navParams: NavParams, protected navCtrl: NavController, protected courseProvider: CoreCourseProvider,
|
||||
protected appProvider: CoreAppProvider, protected assignProvider: AddonModAssignProvider) {
|
||||
|
||||
this.moduleId = navParams.get('moduleId');
|
||||
this.courseId = navParams.get('courseId');
|
||||
this.submitId = navParams.get('submitId');
|
||||
this.blindId = navParams.get('blindId');
|
||||
this.showGrade = !!navParams.get('showGrade');
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.fetchSubmission().finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can leave the page or not.
|
||||
*
|
||||
* @return {boolean|Promise<void>} Resolved if we can leave it, rejected if not.
|
||||
*/
|
||||
ionViewCanLeave(): boolean | Promise<void> {
|
||||
if (!this.submissionComponent || this.forceLeave) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if data has changed.
|
||||
return this.submissionComponent.canLeave();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the submission.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchSubmission(): Promise<any> {
|
||||
return this.assignProvider.getAssignment(this.courseId, this.moduleId).then((assignment) => {
|
||||
this.assign = assignment;
|
||||
this.title = this.assign.name;
|
||||
|
||||
this.blindMarking = this.assign.blindmarking && !this.assign.revealidentities;
|
||||
|
||||
return this.courseProvider.getModuleBasicGradeInfo(this.moduleId).then((gradeInfo) => {
|
||||
if (gradeInfo) {
|
||||
// Grades can be saved if simple grading.
|
||||
if (gradeInfo.advancedgrading && gradeInfo.advancedgrading[0] &&
|
||||
typeof gradeInfo.advancedgrading[0].method != 'undefined') {
|
||||
|
||||
const method = gradeInfo.advancedgrading[0].method || 'simple';
|
||||
this.canSaveGrades = method == 'simple';
|
||||
} else {
|
||||
this.canSaveGrades = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh all the data.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected refreshAllData(): Promise<any> {
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.assignProvider.invalidateAssignmentData(this.courseId));
|
||||
if (this.assign) {
|
||||
promises.push(this.assignProvider.invalidateSubmissionData(this.assign.id));
|
||||
promises.push(this.assignProvider.invalidateAssignmentUserMappingsData(this.assign.id));
|
||||
promises.push(this.assignProvider.invalidateSubmissionStatusData(this.assign.id, this.submitId, this.blindMarking));
|
||||
}
|
||||
|
||||
return Promise.all(promises).finally(() => {
|
||||
this.submissionComponent && this.submissionComponent.invalidateAndRefresh();
|
||||
|
||||
return this.fetchSubmission();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the data.
|
||||
*
|
||||
* @param {any} refresher Refresher.
|
||||
*/
|
||||
refreshSubmission(refresher: any): void {
|
||||
this.refreshAllData().finally(() => {
|
||||
refresher.complete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a grade and feedback.
|
||||
*/
|
||||
submitGrade(): void {
|
||||
if (this.submissionComponent) {
|
||||
this.submissionComponent.submitGrade().then(() => {
|
||||
// Grade submitted, leave the view if not in tablet.
|
||||
if (!this.appProvider.isWide()) {
|
||||
this.forceLeave = true;
|
||||
this.navCtrl.pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,503 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { CoreFileProvider } from '@providers/file';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||
|
||||
/**
|
||||
* Service to handle offline assign.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModAssignOfflineProvider {
|
||||
|
||||
protected logger;
|
||||
|
||||
// Variables for database.
|
||||
protected SUBMISSIONS_TABLE = 'addon_mod_assign_submissions';
|
||||
protected SUBMISSIONS_GRADES_TABLE = 'addon_mod_assign_submissions_grading';
|
||||
protected tablesSchema = [
|
||||
{
|
||||
name: this.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: this.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']
|
||||
}
|
||||
];
|
||||
|
||||
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider,
|
||||
private fileProvider: CoreFileProvider, private timeUtils: CoreTimeUtilsProvider) {
|
||||
this.logger = logger.getInstance('AddonModAssignOfflineProvider');
|
||||
this.sitesProvider.createTablesFromSchema(this.tablesSchema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a submission.
|
||||
*
|
||||
* @param {number} assignId Assignment ID.
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved if deleted, rejected if failure.
|
||||
*/
|
||||
deleteSubmission(assignId: number, userId?: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
userId = userId || site.getUserId();
|
||||
|
||||
return site.getDb().deleteRecords(this.SUBMISSIONS_TABLE, {assignId, userId});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a submission grade.
|
||||
*
|
||||
* @param {number} assignId Assignment ID.
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved if deleted, rejected if failure.
|
||||
*/
|
||||
deleteSubmissionGrade(assignId: number, userId?: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
userId = userId || site.getUserId();
|
||||
|
||||
return site.getDb().deleteRecords(this.SUBMISSIONS_GRADES_TABLE, {assignId, userId});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the assignments ids that have something to be synced.
|
||||
*
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<number[]>} Promise resolved with assignments id that have something to be synced.
|
||||
*/
|
||||
getAllAssigns(siteId?: string): Promise<number[]> {
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.getAllSubmissions(siteId));
|
||||
promises.push(this.getAllSubmissionsGrade(siteId));
|
||||
|
||||
return Promise.all(promises).then((results) => {
|
||||
// Flatten array.
|
||||
results = [].concat.apply([], results);
|
||||
|
||||
// Get assign id.
|
||||
results = results.map((object) => {
|
||||
return object.assignId;
|
||||
});
|
||||
|
||||
// Get unique values.
|
||||
results = results.filter((id, pos) => {
|
||||
return results.indexOf(id) == pos;
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the stored submissions from all the assignments.
|
||||
*
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any[]} Promise resolved with submissions.
|
||||
*/
|
||||
protected getAllSubmissions(siteId?: string): Promise<any[]> {
|
||||
return this.sitesProvider.getSiteDb(siteId).then((db) => {
|
||||
return db.getAllRecords(this.SUBMISSIONS_TABLE);
|
||||
}).then((submissions) => {
|
||||
|
||||
// Parse the plugin data.
|
||||
submissions.forEach((submission) => {
|
||||
submission.pluginData = this.textUtils.parseJSON(submission.pluginData, {});
|
||||
});
|
||||
|
||||
return submissions;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the stored submissions grades from all the assignments.
|
||||
*
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any[]>} Promise resolved with submissions grades.
|
||||
*/
|
||||
protected getAllSubmissionsGrade(siteId?: string): Promise<any[]> {
|
||||
return this.sitesProvider.getSiteDb(siteId).then((db) => {
|
||||
return db.getAllRecords(this.SUBMISSIONS_GRADES_TABLE);
|
||||
}).then((submissions) => {
|
||||
|
||||
// Parse the plugin data and outcomes.
|
||||
submissions.forEach((submission) => {
|
||||
submission.outcomes = this.textUtils.parseJSON(submission.outcomes, {});
|
||||
submission.pluginData = this.textUtils.parseJSON(submission.pluginData, {});
|
||||
});
|
||||
|
||||
return submissions;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the stored submissions for a certain assignment.
|
||||
*
|
||||
* @param {number} assignId Assignment ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any[]>} Promise resolved with submissions.
|
||||
*/
|
||||
getAssignSubmissions(assignId: number, siteId?: string): Promise<any[]> {
|
||||
return this.sitesProvider.getSiteDb(siteId).then((db) => {
|
||||
return db.getRecords(this.SUBMISSIONS_TABLE, {assignId});
|
||||
}).then((submissions) => {
|
||||
|
||||
// Parse the plugin data.
|
||||
submissions.forEach((submission) => {
|
||||
submission.pluginData = this.textUtils.parseJSON(submission.pluginData, {});
|
||||
});
|
||||
|
||||
return submissions;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the stored submissions grades for a certain assignment.
|
||||
*
|
||||
* @param {number} assignId Assignment ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any[]>} Promise resolved with submissions grades.
|
||||
*/
|
||||
getAssignSubmissionsGrade(assignId: number, siteId?: string): Promise<any[]> {
|
||||
return this.sitesProvider.getSiteDb(siteId).then((db) => {
|
||||
return db.getRecords(this.SUBMISSIONS_GRADES_TABLE, {assignId});
|
||||
}).then((submissions) => {
|
||||
|
||||
// Parse the plugin data and outcomes.
|
||||
submissions.forEach((submission) => {
|
||||
submission.outcomes = this.textUtils.parseJSON(submission.outcomes, {});
|
||||
submission.pluginData = this.textUtils.parseJSON(submission.pluginData, {});
|
||||
});
|
||||
|
||||
return submissions;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a stored submission.
|
||||
*
|
||||
* @param {number} assignId Assignment ID.
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved with submission.
|
||||
*/
|
||||
getSubmission(assignId: number, userId?: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
userId = userId || site.getUserId();
|
||||
|
||||
return site.getDb().getRecord(this.SUBMISSIONS_TABLE, {assignId, userId});
|
||||
}).then((submission) => {
|
||||
|
||||
// Parse the plugin data.
|
||||
submission.pluginData = this.textUtils.parseJSON(submission.pluginData, {});
|
||||
|
||||
return submission;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the folder where to store files for an offline submission.
|
||||
*
|
||||
* @param {number} assignId Assignment ID.
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<string>} Promise resolved with the path.
|
||||
*/
|
||||
getSubmissionFolder(assignId: number, userId?: number, siteId?: string): Promise<string> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
userId = userId || site.getUserId();
|
||||
|
||||
const siteFolderPath = this.fileProvider.getSiteFolder(site.getId()),
|
||||
submissionFolderPath = 'offlineassign/' + assignId + '/' + userId;
|
||||
|
||||
return this.textUtils.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 {number} assignId Assignment ID.
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved with submission grade.
|
||||
*/
|
||||
getSubmissionGrade(assignId: number, userId?: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
userId = userId || site.getUserId();
|
||||
|
||||
return site.getDb().getRecord(this.SUBMISSIONS_GRADES_TABLE, {assignId, userId});
|
||||
}).then((submission) => {
|
||||
|
||||
// Parse the plugin data and outcomes.
|
||||
submission.outcomes = this.textUtils.parseJSON(submission.outcomes, {});
|
||||
submission.pluginData = this.textUtils.parseJSON(submission.pluginData, {});
|
||||
|
||||
return submission;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the folder where to store files for a certain plugin in an offline submission.
|
||||
*
|
||||
* @param {number} assignId Assignment ID.
|
||||
* @param {string} pluginName Name of the plugin. Must be unique (both in submission and feedback plugins).
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<string>} Promise resolved with the path.
|
||||
*/
|
||||
getSubmissionPluginFolder(assignId: number, pluginName: string, userId?: number, siteId?: string): Promise<string> {
|
||||
return this.getSubmissionFolder(assignId, userId, siteId).then((folderPath) => {
|
||||
return this.textUtils.concatenatePaths(folderPath, pluginName);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the assignment has something to be synced.
|
||||
*
|
||||
* @param {number} assignId Assignment ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<boolean>} Promise resolved with boolean: whether the assignment has something to be synced.
|
||||
*/
|
||||
hasAssignOfflineData(assignId: number, siteId?: string): Promise<boolean> {
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.getAssignSubmissions(assignId, siteId));
|
||||
promises.push(this.getAssignSubmissionsGrade(assignId, siteId));
|
||||
|
||||
return Promise.all(promises).then((results) => {
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const result = results[i];
|
||||
|
||||
if (result && result.length) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}).catch(() => {
|
||||
// No offline data found.
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark/Unmark a submission as being submitted.
|
||||
*
|
||||
* @param {number} assignId Assignment ID.
|
||||
* @param {number} courseId Course ID the assign belongs to.
|
||||
* @param {boolean} submitted True to mark as submitted, false to mark as not submitted.
|
||||
* @param {boolean} acceptStatement True to accept the submission statement, false otherwise.
|
||||
* @param {number} timemodified The time the submission was last modified in online.
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved if marked, rejected if failure.
|
||||
*/
|
||||
markSubmitted(assignId: number, courseId: number, submitted: boolean, acceptStatement: boolean, timemodified: number,
|
||||
userId?: number, siteId?: string): Promise<any> {
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
userId = userId || site.getUserId();
|
||||
|
||||
// Check if there's a submission stored.
|
||||
return this.getSubmission(assignId, userId, site.getId()).catch(() => {
|
||||
// No submission, create an empty one.
|
||||
const now = this.timeUtils.timestamp();
|
||||
|
||||
return {
|
||||
assignId: assignId,
|
||||
courseId: courseId,
|
||||
userId: userId,
|
||||
onlineTimemodified: timemodified,
|
||||
timecreated: now,
|
||||
timemodified: now
|
||||
};
|
||||
}).then((submission) => {
|
||||
// Mark the submission.
|
||||
submission.submitted = submitted ? 1 : 0;
|
||||
submission.submissionStatement = acceptStatement ? 1 : 0;
|
||||
submission.pluginData = submission.pluginData ? JSON.stringify(submission.pluginData) : '{}';
|
||||
|
||||
return site.getDb().insertRecord(this.SUBMISSIONS_TABLE, submission);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a submission to be sent later.
|
||||
*
|
||||
* @param {number} assignId Assignment ID.
|
||||
* @param {number} courseId Course ID the assign belongs to.
|
||||
* @param {any} pluginData Data to save.
|
||||
* @param {number} timemodified The time the submission was last modified in online.
|
||||
* @param {boolean} submitted True if submission has been submitted, false otherwise.
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved if stored, rejected if failure.
|
||||
*/
|
||||
saveSubmission(assignId: number, courseId: number, pluginData: any, timemodified: number, submitted: boolean, userId?: number,
|
||||
siteId?: string): Promise<any> {
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
userId = userId || site.getUserId();
|
||||
|
||||
const now = this.timeUtils.timestamp(),
|
||||
entry = {
|
||||
assignId: assignId,
|
||||
courseId: courseId,
|
||||
pluginData: pluginData ? JSON.stringify(pluginData) : '{}',
|
||||
userId: userId,
|
||||
submitted: submitted ? 1 : 0,
|
||||
timecreated: now,
|
||||
timemodified: now,
|
||||
onlineTimemodified: timemodified
|
||||
};
|
||||
|
||||
return site.getDb().insertRecord(this.SUBMISSIONS_TABLE, entry);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a grading to be sent later.
|
||||
*
|
||||
* @param {number} assignId Assign ID.
|
||||
* @param {number} userId User ID.
|
||||
* @param {number} courseId Course ID the assign belongs to.
|
||||
* @param {number} grade Grade to submit.
|
||||
* @param {number} attemptNumber Number of the attempt being graded.
|
||||
* @param {boolean} addAttempt Admit the user to attempt again.
|
||||
* @param {string} workflowState Next workflow State.
|
||||
* @param {boolean} applyToAll If it's a team submission, whether the grade applies to all group members.
|
||||
* @param {any} outcomes Object including all outcomes values. If empty, any of them will be sent.
|
||||
* @param {any} pluginData Plugin data to save.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved if stored, rejected if failure.
|
||||
*/
|
||||
submitGradingForm(assignId: number, userId: number, courseId: number, grade: number, attemptNumber: number, addAttempt: boolean,
|
||||
workflowState: string, applyToAll: boolean, outcomes: any, pluginData: any, siteId?: string): Promise<any> {
|
||||
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const now = this.timeUtils.timestamp(),
|
||||
entry = {
|
||||
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 site.getDb().insertRecord(this.SUBMISSIONS_GRADES_TABLE, entry);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,431 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreSyncProvider } from '@providers/sync';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
import { CoreGradesHelperProvider } from '@core/grades/providers/helper';
|
||||
import { CoreSyncBaseProvider } from '@classes/base-sync';
|
||||
import { AddonModAssignProvider } from './assign';
|
||||
import { AddonModAssignOfflineProvider } from './assign-offline';
|
||||
import { AddonModAssignSubmissionDelegate } from './submission-delegate';
|
||||
|
||||
/**
|
||||
* Data returned by an assign sync.
|
||||
*/
|
||||
export interface AddonModAssignSyncResult {
|
||||
/**
|
||||
* List of warnings.
|
||||
* @type {string[]}
|
||||
*/
|
||||
warnings: string[];
|
||||
|
||||
/**
|
||||
* Whether data was updated in the site.
|
||||
* @type {boolean}
|
||||
*/
|
||||
updated: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service to sync assigns.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModAssignSyncProvider extends CoreSyncBaseProvider {
|
||||
|
||||
static AUTO_SYNCED = 'addon_mod_assign_autom_synced';
|
||||
static SYNC_TIME = 300000;
|
||||
|
||||
protected componentTranslate: string;
|
||||
|
||||
constructor(loggerProvider: CoreLoggerProvider, sitesProvider: CoreSitesProvider, appProvider: CoreAppProvider,
|
||||
syncProvider: CoreSyncProvider, textUtils: CoreTextUtilsProvider, translate: TranslateService,
|
||||
private courseProvider: CoreCourseProvider, private eventsProvider: CoreEventsProvider,
|
||||
private assignProvider: AddonModAssignProvider, private assignOfflineProvider: AddonModAssignOfflineProvider,
|
||||
private utils: CoreUtilsProvider, private submissionDelegate: AddonModAssignSubmissionDelegate,
|
||||
private gradesHelper: CoreGradesHelperProvider) {
|
||||
|
||||
super('AddonModAssignSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate);
|
||||
|
||||
this.componentTranslate = courseProvider.translateModuleName('assign');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get scale selected option.
|
||||
*
|
||||
* @param {string} options Possible options.
|
||||
* @param {number} selected Selected option to search.
|
||||
* @return {number} Index of the selected option.
|
||||
*/
|
||||
protected getSelectedScaleId(options: string, selected: string): number {
|
||||
let optionsList = options.split(',');
|
||||
|
||||
optionsList = optionsList.map((value) => {
|
||||
return 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 {number} assignId Assign ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<boolean>} Promise resolved with boolean: whether it has data to sync.
|
||||
*/
|
||||
hasDataToSync(assignId: number, siteId?: string): Promise<boolean> {
|
||||
return this.assignOfflineProvider.hasAssignOfflineData(assignId, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to synchronize all the assignments in a certain site or in all sites.
|
||||
*
|
||||
* @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
|
||||
* @return {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
|
||||
*/
|
||||
syncAllAssignments(siteId?: string): Promise<any> {
|
||||
return this.syncOnSites('all assignments', this.syncAllAssignmentsFunc.bind(this), [], siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync all assignments on a site.
|
||||
*
|
||||
* @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
|
||||
* @param {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
|
||||
*/
|
||||
protected syncAllAssignmentsFunc(siteId?: string): Promise<any> {
|
||||
// Get all assignments that have offline data.
|
||||
return this.assignOfflineProvider.getAllAssigns(siteId).then((assignIds) => {
|
||||
const promises = [];
|
||||
|
||||
// Sync all assignments that haven't been synced for a while.
|
||||
assignIds.forEach((assignId) => {
|
||||
promises.push(this.syncAssignIfNeeded(assignId, siteId).then((data) => {
|
||||
if (data && data.updated) {
|
||||
// Sync done. Send event.
|
||||
this.eventsProvider.trigger(AddonModAssignSyncProvider.AUTO_SYNCED, {
|
||||
assignId: assignId,
|
||||
warnings: data.warnings
|
||||
}, siteId);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync an assignment only if a certain time has passed since the last time.
|
||||
*
|
||||
* @param {number} assignId Assign ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<void|AddonModAssignSyncResult>} Promise resolved when the assign is synced or it doesn't need to be synced.
|
||||
*/
|
||||
syncAssignIfNeeded(assignId: number, siteId?: string): Promise<void | AddonModAssignSyncResult> {
|
||||
return this.isSyncNeeded(assignId, siteId).then((needed) => {
|
||||
if (needed) {
|
||||
return this.syncAssign(assignId, siteId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to synchronize an assign.
|
||||
*
|
||||
* @param {number} assignId Assign ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<AddonModAssignSyncResult>} Promise resolved in success.
|
||||
*/
|
||||
syncAssign(assignId: number, siteId?: string): Promise<AddonModAssignSyncResult> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
const promises = [],
|
||||
result: AddonModAssignSyncResult = {
|
||||
warnings: [],
|
||||
updated: false
|
||||
};
|
||||
let assign,
|
||||
courseId,
|
||||
syncPromise;
|
||||
|
||||
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 (this.syncProvider.isBlocked(AddonModAssignProvider.COMPONENT, assignId, siteId)) {
|
||||
this.logger.debug('Cannot sync assign ' + assignId + ' because it is blocked.');
|
||||
|
||||
return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate}));
|
||||
}
|
||||
|
||||
this.logger.debug('Try to sync assign ' + assignId + ' in site ' + siteId);
|
||||
|
||||
// Get offline submissions to be sent.
|
||||
promises.push(this.assignOfflineProvider.getAssignSubmissions(assignId, siteId).catch(() => {
|
||||
// No offline data found, return empty array.
|
||||
return [];
|
||||
}));
|
||||
|
||||
// Get offline submission grades to be sent.
|
||||
promises.push(this.assignOfflineProvider.getAssignSubmissionsGrade(assignId, siteId).catch(() => {
|
||||
// No offline data found, return empty array.
|
||||
return [];
|
||||
}));
|
||||
|
||||
syncPromise = Promise.all(promises).then((results) => {
|
||||
const submissions = results[0],
|
||||
grades = results[1];
|
||||
|
||||
if (!submissions.length && !grades.length) {
|
||||
// Nothing to sync.
|
||||
return;
|
||||
} else if (!this.appProvider.isOnline()) {
|
||||
// Cannot sync in offline.
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
courseId = submissions.length > 0 ? submissions[0].courseId : grades[0].courseId;
|
||||
|
||||
return this.assignProvider.getAssignmentById(courseId, assignId, siteId).then((assignData) => {
|
||||
assign = assignData;
|
||||
|
||||
const promises = [];
|
||||
|
||||
submissions.forEach((submission) => {
|
||||
promises.push(this.syncSubmission(assign, submission, result.warnings, siteId).then(() => {
|
||||
result.updated = true;
|
||||
}));
|
||||
});
|
||||
|
||||
grades.forEach((grade) => {
|
||||
promises.push(this.syncSubmissionGrade(assign, grade, result.warnings, courseId, siteId).then(() => {
|
||||
result.updated = true;
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}).then(() => {
|
||||
if (result.updated) {
|
||||
// Data has been sent to server. Now invalidate the WS calls.
|
||||
return this.assignProvider.invalidateContent(assign.cmid, courseId, siteId).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
// Sync finished, set sync time.
|
||||
return this.setSyncTime(assignId, siteId).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}).then(() => {
|
||||
// All done, return the result.
|
||||
return result;
|
||||
});
|
||||
|
||||
return this.addOngoingSync(assignId, syncPromise, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize a submission.
|
||||
*
|
||||
* @param {any} assign Assignment.
|
||||
* @param {any} offlineData Submission offline data.
|
||||
* @param {string[]} warnings List of warnings.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved if success, rejected otherwise.
|
||||
*/
|
||||
protected syncSubmission(assign: any, offlineData: any, warnings: string[], siteId?: string): Promise<any> {
|
||||
const userId = offlineData.userId,
|
||||
pluginData = {};
|
||||
let discardError,
|
||||
submission;
|
||||
|
||||
return this.assignProvider.getSubmissionStatus(assign.id, userId, false, true, true, siteId).then((status) => {
|
||||
const promises = [];
|
||||
|
||||
submission = this.assignProvider.getSubmissionObjectFromAttempt(assign, status.lastattempt);
|
||||
|
||||
if (submission.timemodified != offlineData.onlineTimemodified) {
|
||||
// The submission was modified in Moodle, discard the submission.
|
||||
discardError = this.translate.instant('addon.mod_assign.warningsubmissionmodified');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
submission.plugins.forEach((plugin) => {
|
||||
promises.push(this.submissionDelegate.preparePluginSyncData(assign, submission, plugin, offlineData, pluginData,
|
||||
siteId));
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
// Now save the submission.
|
||||
let promise;
|
||||
|
||||
if (!Object.keys(pluginData).length) {
|
||||
// Nothing to save.
|
||||
promise = Promise.resolve();
|
||||
} else {
|
||||
promise = this.assignProvider.saveSubmissionOnline(assign.id, pluginData, siteId);
|
||||
}
|
||||
|
||||
return promise.then(() => {
|
||||
if (assign.submissiondrafts && offlineData.submitted) {
|
||||
// The user submitted the assign manually. Submit it for grading.
|
||||
return this.assignProvider.submitForGradingOnline(assign.id, offlineData.submissionStatement, siteId);
|
||||
}
|
||||
}).then(() => {
|
||||
// Submission data sent, update cached data. No need to block the user for this.
|
||||
this.assignProvider.getSubmissionStatus(assign.id, userId, false, true, true, siteId);
|
||||
});
|
||||
}).catch((error) => {
|
||||
if (error && this.utils.isWebServiceError(error)) {
|
||||
// A WebService has thrown an error, this means it cannot be submitted. Discard the submission.
|
||||
discardError = error.message || error.error || error.content || error.body;
|
||||
} else {
|
||||
// Couldn't connect to server, reject.
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
// Delete the offline data.
|
||||
return this.assignOfflineProvider.deleteSubmission(assign.id, userId, siteId).then(() => {
|
||||
const promises = [];
|
||||
|
||||
submission.plugins.forEach((plugin) => {
|
||||
promises.push(this.submissionDelegate.deletePluginOfflineData(assign, submission, plugin, offlineData, siteId));
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
});
|
||||
}).then(() => {
|
||||
if (discardError) {
|
||||
// Submission was discarded, add a warning.
|
||||
const message = this.translate.instant('core.warningofflinedatadeleted', {
|
||||
component: this.componentTranslate,
|
||||
name: assign.name,
|
||||
error: discardError
|
||||
});
|
||||
|
||||
if (warnings.indexOf(message) == -1) {
|
||||
warnings.push(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize a submission grade.
|
||||
*
|
||||
* @param {any} assign Assignment.
|
||||
* @param {any} offlineData Submission grade offline data.
|
||||
* @param {string[]} warnings List of warnings.
|
||||
* @param {number} courseId Course Id.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved if success, rejected otherwise.
|
||||
*/
|
||||
protected syncSubmissionGrade(assign: any, offlineData: any, warnings: string[], courseId: number, siteId?: string)
|
||||
: Promise<any> {
|
||||
|
||||
const userId = offlineData.userId;
|
||||
let discardError;
|
||||
|
||||
return this.assignProvider.getSubmissionStatus(assign.id, userId, false, true, true, siteId).then((status) => {
|
||||
const timemodified = status.feedback && (status.feedback.gradeddate || status.feedback.grade.timemodified);
|
||||
|
||||
if (timemodified > offlineData.timemodified) {
|
||||
// The submission grade was modified in Moodle, discard it.
|
||||
discardError = this.translate.instant('addon.mod_assign.warningsubmissiongrademodified');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If grade has been modified from gradebook, do not use offline.
|
||||
return this.gradesHelper.getGradeModuleItems(courseId, assign.cmid, userId, undefined, siteId, true).then((grades) => {
|
||||
return this.courseProvider.getModuleBasicGradeInfo(assign.cmid, siteId).then((gradeInfo) => {
|
||||
|
||||
// Override offline grade and outcomes based on the gradebook data.
|
||||
grades.forEach((grade) => {
|
||||
if (grade.gradedategraded >= offlineData.timemodified) {
|
||||
if (!grade.outcomeid && !grade.scaleid) {
|
||||
if (gradeInfo && gradeInfo.scale) {
|
||||
offlineData.grade = this.getSelectedScaleId(gradeInfo.scale, grade.gradeformatted);
|
||||
} else {
|
||||
offlineData.grade = parseFloat(grade.gradeformatted) || null;
|
||||
}
|
||||
} else if (grade.outcomeid && this.assignProvider.isOutcomesEditEnabled() && gradeInfo.outcomes) {
|
||||
gradeInfo.outcomes.forEach((outcome, index) => {
|
||||
if (outcome.scale && grade.itemnumber == index) {
|
||||
offlineData.outcomes[grade.itemnumber] = this.getSelectedScaleId(outcome.scale,
|
||||
outcome.selected);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}).then(() => {
|
||||
// Now submit the grade.
|
||||
return this.assignProvider.submitGradingFormOnline(assign.id, userId, offlineData.grade, offlineData.attemptNumber,
|
||||
offlineData.addAttempt, offlineData.workflowState, offlineData.applyToAll, offlineData.outcomes,
|
||||
offlineData.pluginData, siteId).then(() => {
|
||||
|
||||
// Grades sent, update cached data. No need to block the user for this.
|
||||
this.assignProvider.getSubmissionStatus(assign.id, userId, false, true, true, siteId);
|
||||
}).catch((error) => {
|
||||
if (error && this.utils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means it cannot be submitted. Discard the offline data.
|
||||
discardError = error.message || error.error || error.content || error.body;
|
||||
} else {
|
||||
// Couldn't connect to server, reject.
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}).then(() => {
|
||||
// Delete the offline data.
|
||||
return this.assignOfflineProvider.deleteSubmissionGrade(assign.id, userId, siteId);
|
||||
}).then(() => {
|
||||
if (discardError) {
|
||||
// Submission grade was discarded, add a warning.
|
||||
const message = this.translate.instant('core.warningofflinedatadeleted', {
|
||||
component: this.componentTranslate,
|
||||
name: assign.name,
|
||||
error: discardError
|
||||
});
|
||||
|
||||
if (warnings.indexOf(message) == -1) {
|
||||
warnings.push(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,98 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { TranslateService } from '@ngx-translate/core';
|
||||
import { AddonModAssignFeedbackHandler } from './feedback-delegate';
|
||||
|
||||
/**
|
||||
* Default handler used when a feedback plugin doesn't have a specific implementation.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModAssignDefaultFeedbackHandler implements AddonModAssignFeedbackHandler {
|
||||
name = 'AddonModAssignDefaultFeedbackHandler';
|
||||
type = 'default';
|
||||
|
||||
constructor(private translate: TranslateService) { }
|
||||
|
||||
/**
|
||||
* Get files used by this plugin.
|
||||
* The files returned by this function will be prefetched when the user prefetches the assign.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {any[]|Promise<any[]>} The files (or promise resolved with the files).
|
||||
*/
|
||||
getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise<any[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a readable name to use for the plugin.
|
||||
*
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {string} The plugin name.
|
||||
*/
|
||||
getPluginName(plugin: any): string {
|
||||
// Check if there's a translated string for the plugin.
|
||||
const translationId = 'addon.mod_assign_feedback_' + plugin.type + '.pluginname',
|
||||
translation = this.translate.instant(translationId);
|
||||
|
||||
if (translationId != translation) {
|
||||
// Translation found, use it.
|
||||
return translation;
|
||||
}
|
||||
|
||||
// Fallback to WS string.
|
||||
if (plugin.name) {
|
||||
return plugin.name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the feedback data has changed for this plugin.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the feedback.
|
||||
* @return {boolean|Promise<boolean>} Boolean (or promise resolved with boolean): whether the data has changed.
|
||||
*/
|
||||
hasDataChanged(assign: any, submission: any, plugin: any, inputData: any): boolean | Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the plugin has draft data stored.
|
||||
*
|
||||
* @param {number} assignId The assignment ID.
|
||||
* @param {number} userId User ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {boolean|Promise<boolean>} Boolean or promise resolved with boolean: whether the plugin has draft data.
|
||||
*/
|
||||
hasDraftData(assignId: number, userId: number, siteId?: string): boolean | Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { TranslateService } from '@ngx-translate/core';
|
||||
import { AddonModAssignSubmissionHandler } from './submission-delegate';
|
||||
|
||||
/**
|
||||
* Default handler used when a submission plugin doesn't have a specific implementation.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModAssignDefaultSubmissionHandler implements AddonModAssignSubmissionHandler {
|
||||
name = 'AddonModAssignDefaultSubmissionHandler';
|
||||
type = 'default';
|
||||
|
||||
constructor(private translate: TranslateService) { }
|
||||
|
||||
/**
|
||||
* 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 {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {boolean|Promise<boolean>} Boolean or promise resolved with boolean: whether it can be edited in offline.
|
||||
*/
|
||||
canEditOffline(assign: any, submission: any, plugin: any): boolean | Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files used by this plugin.
|
||||
* The files returned by this function will be prefetched when the user prefetches the assign.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {any[]|Promise<any[]>} The files (or promise resolved with the files).
|
||||
*/
|
||||
getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise<any[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a readable name to use for the plugin.
|
||||
*
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {string} The plugin name.
|
||||
*/
|
||||
getPluginName(plugin: any): string {
|
||||
// Check if there's a translated string for the plugin.
|
||||
const translationId = 'addon.mod_assign_submission_' + plugin.type + '.pluginname',
|
||||
translation = this.translate.instant(translationId);
|
||||
|
||||
if (translationId != translation) {
|
||||
// Translation found, use it.
|
||||
return translation;
|
||||
}
|
||||
|
||||
// Fallback to WS string.
|
||||
if (plugin.name) {
|
||||
return plugin.name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of data (in bytes) this plugin will send to copy a previous submission.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {number|Promise<number>} The size (or promise resolved with size).
|
||||
*/
|
||||
getSizeForCopy(assign: any, plugin: any): number | Promise<number> {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of data (in bytes) this plugin will send to add or edit a submission.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {number|Promise<number>} The size (or promise resolved with size).
|
||||
*/
|
||||
getSizeForEdit(assign: any, plugin: any): number | Promise<number> {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the submission data has changed for this plugin.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the submission.
|
||||
* @return {boolean|Promise<boolean>} Boolean (or promise resolved with boolean): whether the data has changed.
|
||||
*/
|
||||
hasDataChanged(assign: any, submission: any, plugin: any, inputData: any): boolean | Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled for edit on a site level.
|
||||
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled for edit on a site level.
|
||||
*/
|
||||
isEnabledForEdit(): boolean | Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,297 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, Injector } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
||||
import { AddonModAssignDefaultFeedbackHandler } from './default-feedback-handler';
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Discard the draft data of the feedback plugin.
|
||||
*
|
||||
* @param {number} assignId The assignment ID.
|
||||
* @param {number} userId User ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} 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 {Injector} injector Injector.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||
*/
|
||||
getComponent?(injector: Injector, plugin: any): any | Promise<any>;
|
||||
|
||||
/**
|
||||
* Return the draft saved data of the feedback plugin.
|
||||
*
|
||||
* @param {number} assignId The assignment ID.
|
||||
* @param {number} userId User ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {any|Promise<any>} 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 {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {any[]|Promise<any[]>} The files (or promise resolved with the files).
|
||||
*/
|
||||
getPluginFiles?(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise<any[]>;
|
||||
|
||||
/**
|
||||
* Get a readable name to use for the plugin.
|
||||
*
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {string} The plugin name.
|
||||
*/
|
||||
getPluginName?(plugin: any): string;
|
||||
|
||||
/**
|
||||
* Check if the feedback data has changed for this plugin.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the feedback.
|
||||
* @param {number} userId User ID of the submission.
|
||||
* @return {boolean|Promise<boolean>} Boolean (or promise resolved with boolean): whether the data has changed.
|
||||
*/
|
||||
hasDataChanged?(assign: any, submission: any, plugin: any, inputData: any, userId: number): boolean | Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Check whether the plugin has draft data stored.
|
||||
*
|
||||
* @param {number} assignId The assignment ID.
|
||||
* @param {number} userId User ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {boolean|Promise<boolean>} 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 {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
prefetch?(assign: any, submission: any, plugin: any, siteId?: string): Promise<any>;
|
||||
|
||||
/**
|
||||
* Prepare and add to pluginData the data to send to the server based on the draft data saved.
|
||||
*
|
||||
* @param {number} assignId The assignment ID.
|
||||
* @param {number} userId User ID.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} pluginData Object where to store the data to send.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} If the function is async, it should return a Promise resolved when done.
|
||||
*/
|
||||
prepareFeedbackData?(assignId: number, userId: number, plugin: any, pluginData: any, siteId?: string): void | Promise<any>;
|
||||
|
||||
/**
|
||||
* Save draft data of the feedback plugin.
|
||||
*
|
||||
* @param {number} assignId The assignment ID.
|
||||
* @param {number} userId User ID.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} data The data to save.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} If the function is async, it should return a Promise resolved when done.
|
||||
*/
|
||||
saveDraft?(assignId: number, userId: number, plugin: any, data: any, siteId?: string): void | Promise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate to register plugins for assign feedback.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModAssignFeedbackDelegate extends CoreDelegate {
|
||||
|
||||
protected handlerNameProperty = 'type';
|
||||
|
||||
constructor(logger: CoreLoggerProvider, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider,
|
||||
protected defaultHandler: AddonModAssignDefaultFeedbackHandler) {
|
||||
super('AddonModAssignFeedbackDelegate', logger, sitesProvider, eventsProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discard the draft data of the feedback plugin.
|
||||
*
|
||||
* @param {number} assignId The assignment ID.
|
||||
* @param {number} userId User ID.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
discardPluginFeedbackData(assignId: number, userId: number, plugin: any, siteId?: string): Promise<any> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'discardDraft', [assignId, userId, siteId]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component to use for a certain feedback plugin.
|
||||
*
|
||||
* @param {Injector} injector Injector.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {Promise<any>} Promise resolved with the component to use, undefined if not found.
|
||||
*/
|
||||
getComponentForPlugin(injector: Injector, plugin: any): Promise<any> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'getComponent', [injector, plugin]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the draft saved data of the feedback plugin.
|
||||
*
|
||||
* @param {number} assignId The assignment ID.
|
||||
* @param {number} userId User ID.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved with the draft data.
|
||||
*/
|
||||
getPluginDraftData(assignId: number, userId: number, plugin: any, siteId?: string): Promise<any> {
|
||||
return Promise.resolve(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 {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any[]>} Promise resolved with the files.
|
||||
*/
|
||||
getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): Promise<any[]> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'getPluginFiles', [assign, submission, plugin, siteId]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a readable name to use for a certain feedback plugin.
|
||||
*
|
||||
* @param {any} plugin Plugin to get the name for.
|
||||
* @return {string} Human readable name.
|
||||
*/
|
||||
getPluginName(plugin: any): string {
|
||||
return this.executeFunctionOnEnabled(plugin.type, 'getPluginName', [plugin]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the feedback data has changed for a certain plugin.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the feedback.
|
||||
* @param {number} userId User ID of the submission.
|
||||
* @return {Promise<boolean>} Promise resolved with true if data has changed, resolved with false otherwise.
|
||||
*/
|
||||
hasPluginDataChanged(assign: any, submission: any, plugin: any, inputData: any, userId: number): Promise<boolean> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'hasDataChanged',
|
||||
[assign, submission, plugin, inputData, userId]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the plugin has draft data stored.
|
||||
*
|
||||
* @param {number} assignId The assignment ID.
|
||||
* @param {number} userId User ID.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<boolean>} Promise resolved with true if it has draft data.
|
||||
*/
|
||||
hasPluginDraftData(assignId: number, userId: number, plugin: any, siteId?: string): Promise<boolean> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'hasDraftData', [assignId, userId, siteId]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a feedback plugin is supported.
|
||||
*
|
||||
* @param {string} pluginType Type of the plugin.
|
||||
* @return {boolean} Whether it's supported.
|
||||
*/
|
||||
isPluginSupported(pluginType: string): boolean {
|
||||
return this.hasHandler(pluginType, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch any required data for a feedback plugin.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
prefetch(assign: any, submission: any, plugin: any, siteId?: string): Promise<any> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'prefetch', [assign, submission, plugin, siteId]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare and add to pluginData the data to submit for a certain feedback plugin.
|
||||
*
|
||||
* @param {number} assignId The assignment ID.
|
||||
* @param {number} userId User ID.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} pluginData Object where to store the data to send.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when data has been gathered.
|
||||
*/
|
||||
preparePluginFeedbackData(assignId: number, userId: number, plugin: any, pluginData: any, siteId?: string): Promise<any> {
|
||||
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'prepareFeedbackData',
|
||||
[assignId, userId, plugin, pluginData, siteId]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save draft data of the feedback plugin.
|
||||
*
|
||||
* @param {number} assignId The assignment ID.
|
||||
* @param {number} userId User ID.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data to save.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when data has been saved.
|
||||
*/
|
||||
saveFeedbackDraft(assignId: number, userId: number, plugin: any, inputData: any, siteId?: string): Promise<any> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'saveDraft',
|
||||
[assignId, userId, plugin, inputData, siteId]));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,454 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { CoreFileProvider } from '@providers/file';
|
||||
import { CoreGroupsProvider } from '@providers/groups';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
|
||||
import { AddonModAssignFeedbackDelegate } from './feedback-delegate';
|
||||
import { AddonModAssignSubmissionDelegate } from './submission-delegate';
|
||||
import { AddonModAssignProvider } from './assign';
|
||||
import { AddonModAssignOfflineProvider } from './assign-offline';
|
||||
|
||||
/**
|
||||
* Service that provides some helper functions for assign.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModAssignHelperProvider {
|
||||
protected logger;
|
||||
|
||||
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private fileProvider: CoreFileProvider,
|
||||
private assignProvider: AddonModAssignProvider, private utils: CoreUtilsProvider,
|
||||
private assignOffline: AddonModAssignOfflineProvider, private feedbackDelegate: AddonModAssignFeedbackDelegate,
|
||||
private submissionDelegate: AddonModAssignSubmissionDelegate, private fileUploaderProvider: CoreFileUploaderProvider,
|
||||
private groupsProvider: CoreGroupsProvider) {
|
||||
this.logger = logger.getInstance('AddonModAssignHelperProvider');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a submission can be edited in offline.
|
||||
*
|
||||
* @param {any} assign Assignment.
|
||||
* @param {any} submission Submission.
|
||||
* @return {boolean} Whether it can be edited offline.
|
||||
*/
|
||||
canEditSubmissionOffline(assign: any, submission: any): Promise<boolean> {
|
||||
if (!submission) {
|
||||
return Promise.resolve(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 Promise.resolve(true);
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
let canEdit = true;
|
||||
|
||||
for (let i = 0; i < submission.plugins.length; i++) {
|
||||
const plugin = submission.plugins[i];
|
||||
promises.push(this.submissionDelegate.canPluginEditOffline(assign, submission, plugin).then((canEditPlugin) => {
|
||||
if (!canEditPlugin) {
|
||||
canEdit = false;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return canEdit;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear plugins temporary data because a submission was cancelled.
|
||||
*
|
||||
* @param {any} assign Assignment.
|
||||
* @param {any} submission Submission to clear the data for.
|
||||
* @param {any} inputData Data entered in the submission form.
|
||||
*/
|
||||
clearSubmissionPluginTmpData(assign: any, submission: any, inputData: any): void {
|
||||
submission.plugins.forEach((plugin) => {
|
||||
this.submissionDelegate.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 {any} assign Assignment.
|
||||
* @param {any} previousSubmission Submission to copy.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
copyPreviousAttempt(assign: any, previousSubmission: any): Promise<any> {
|
||||
const pluginData = {},
|
||||
promises = [];
|
||||
|
||||
previousSubmission.plugins.forEach((plugin) => {
|
||||
promises.push(this.submissionDelegate.copyPluginSubmissionData(assign, plugin, pluginData));
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
// We got the plugin data. Now we need to submit it.
|
||||
if (Object.keys(pluginData).length) {
|
||||
// There's something to save.
|
||||
return this.assignProvider.saveSubmissionOnline(assign.id, pluginData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete stored submission files for a plugin. See storeSubmissionFiles.
|
||||
*
|
||||
* @param {number} assignId Assignment ID.
|
||||
* @param {string} folderName Name of the plugin folder. Must be unique (both in submission and feedback plugins).
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
deleteStoredSubmissionFiles(assignId: number, folderName: string, userId?: number, siteId?: string): Promise<any> {
|
||||
return this.assignOffline.getSubmissionPluginFolder(assignId, folderName, userId, siteId).then((folderPath) => {
|
||||
return this.fileProvider.removeDir(folderPath);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all drafts of the feedback plugin data.
|
||||
*
|
||||
* @param {number} assignId Assignment Id.
|
||||
* @param {number} userId User Id.
|
||||
* @param {any} feedback Feedback data.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
discardFeedbackPluginData(assignId: number, userId: number, feedback: any, siteId?: string): Promise<any> {
|
||||
const promises = [];
|
||||
|
||||
feedback.plugins.forEach((plugin) => {
|
||||
promises.push(this.feedbackDelegate.discardPluginFeedbackData(assignId, userId, plugin, siteId));
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* List the participants for a single assignment, with some summary info about their submissions.
|
||||
*
|
||||
* @param {any} assign Assignment object
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any[]} Promise resolved with the list of participants and summary of submissions.
|
||||
*/
|
||||
getParticipants(assign: any, siteId?: string): Promise<any[]> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
// Get the participants without specifying a group.
|
||||
return this.assignProvider.listParticipants(assign.id, undefined, siteId).then((participants) => {
|
||||
if (participants && participants.length > 0) {
|
||||
return participants;
|
||||
}
|
||||
|
||||
// If no participants returned, get participants by groups.
|
||||
return this.groupsProvider.getActivityAllowedGroupsIfEnabled(assign.cmid, undefined, siteId).then((userGroups) => {
|
||||
const promises = [],
|
||||
participants = {};
|
||||
|
||||
userGroups.forEach((userGroup) => {
|
||||
promises.push(this.assignProvider.listParticipants(assign.id, userGroup.id, siteId).then((parts) => {
|
||||
// Do not get repeated users.
|
||||
parts.forEach((participant) => {
|
||||
participants[participant.id] = participant;
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return this.utils.objectToArray(participants);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin config from assignment config.
|
||||
*
|
||||
* @param {any} assign Assignment object including all config.
|
||||
* @param {string} subtype Subtype name (assignsubmission or assignfeedback)
|
||||
* @param {string} type Name of the subplugin.
|
||||
* @return {any} Object containing all configurations of the subplugin selected.
|
||||
*/
|
||||
getPluginConfig(assign: any, subtype: string, type: string): any {
|
||||
const configs = {};
|
||||
|
||||
assign.configs.forEach((config) => {
|
||||
if (config.subtype == subtype && config.plugin == type) {
|
||||
configs[config.name] = config.value;
|
||||
}
|
||||
});
|
||||
|
||||
return configs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled subplugins.
|
||||
*
|
||||
* @param {any} assign Assignment object including all config.
|
||||
* @param {string} subtype Subtype name (assignsubmission or assignfeedback)
|
||||
* @return {any} List of enabled plugins for the assign.
|
||||
*/
|
||||
getPluginsEnabled(assign: any, subtype: string): any[] {
|
||||
const enabled = [];
|
||||
|
||||
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 {number} assignId Assignment ID.
|
||||
* @param {string} folderName Name of the plugin folder. Must be unique (both in submission and feedback plugins).
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any[]>} Promise resolved with the files.
|
||||
*/
|
||||
getStoredSubmissionFiles(assignId: number, folderName: string, userId?: number, siteId?: string): Promise<any[]> {
|
||||
return this.assignOffline.getSubmissionPluginFolder(assignId, folderName, userId, siteId).then((folderPath) => {
|
||||
return this.fileProvider.getDirectoryContents(folderPath);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size that will be uploaded to perform an attempt copy.
|
||||
*
|
||||
* @param {any} assign Assignment.
|
||||
* @param {any} previousSubmission Submission to copy.
|
||||
* @return {Promise<number>} Promise resolved with the size.
|
||||
*/
|
||||
getSubmissionSizeForCopy(assign: any, previousSubmission: any): Promise<number> {
|
||||
const promises = [];
|
||||
let totalSize = 0;
|
||||
|
||||
previousSubmission.plugins.forEach((plugin) => {
|
||||
promises.push(this.submissionDelegate.getPluginSizeForCopy(assign, plugin).then((size) => {
|
||||
totalSize += size;
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return totalSize;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size that will be uploaded to save a submission.
|
||||
*
|
||||
* @param {any} assign Assignment.
|
||||
* @param {any} submission Submission to check data.
|
||||
* @param {any} inputData Data entered in the submission form.
|
||||
* @return {Promise<number>} Promise resolved with the size.
|
||||
*/
|
||||
getSubmissionSizeForEdit(assign: any, submission: any, inputData: any): Promise<number> {
|
||||
const promises = [];
|
||||
let totalSize = 0;
|
||||
|
||||
submission.plugins.forEach((plugin) => {
|
||||
promises.push(this.submissionDelegate.getPluginSizeForEdit(assign, submission, plugin, inputData).then((size) => {
|
||||
totalSize += size;
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return totalSize;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the feedback data has changed for a certain submission and assign.
|
||||
*
|
||||
* @param {any} assign Assignment.
|
||||
* @param {number} userId User Id.
|
||||
* @param {any} feedback Feedback data.
|
||||
* @return {Promise<boolean>} Promise resolved with true if data has changed, resolved with false otherwise.
|
||||
*/
|
||||
hasFeedbackDataChanged(assign: any, userId: number, feedback: any): Promise<boolean> {
|
||||
const promises = [];
|
||||
let hasChanged = false;
|
||||
|
||||
feedback.plugins.forEach((plugin) => {
|
||||
promises.push(this.prepareFeedbackPluginData(assign.id, userId, feedback).then((inputData) => {
|
||||
return this.feedbackDelegate.hasPluginDataChanged(assign, userId, plugin, inputData, userId).then((changed) => {
|
||||
if (changed) {
|
||||
hasChanged = true;
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
// Ignore errors.
|
||||
}));
|
||||
});
|
||||
|
||||
return this.utils.allPromises(promises).then(() => {
|
||||
return hasChanged;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the submission data has changed for a certain submission and assign.
|
||||
*
|
||||
* @param {any} assign Assignment.
|
||||
* @param {any} submission Submission to check data.
|
||||
* @param {any} inputData Data entered in the submission form.
|
||||
* @return {Promise<boolean>} Promise resolved with true if data has changed, resolved with false otherwise.
|
||||
*/
|
||||
hasSubmissionDataChanged(assign: any, submission: any, inputData: any): Promise<boolean> {
|
||||
const promises = [];
|
||||
let hasChanged = false;
|
||||
|
||||
submission.plugins.forEach((plugin) => {
|
||||
promises.push(this.submissionDelegate.hasPluginDataChanged(assign, submission, plugin, inputData).then((changed) => {
|
||||
if (changed) {
|
||||
hasChanged = true;
|
||||
}
|
||||
}).catch(() => {
|
||||
// Ignore errors.
|
||||
}));
|
||||
});
|
||||
|
||||
return this.utils.allPromises(promises).then(() => {
|
||||
return hasChanged;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare and return the plugin data to send for a certain feedback and assign.
|
||||
*
|
||||
* @param {number} assignId Assignment Id.
|
||||
* @param {number} userId User Id.
|
||||
* @param {any} feedback Feedback data.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved with plugin data to send to server.
|
||||
*/
|
||||
prepareFeedbackPluginData(assignId: number, userId: number, feedback: any, siteId?: string): Promise<any> {
|
||||
const pluginData = {},
|
||||
promises = [];
|
||||
|
||||
feedback.plugins.forEach((plugin) => {
|
||||
promises.push(this.feedbackDelegate.preparePluginFeedbackData(assignId, userId, plugin, pluginData, siteId));
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return pluginData;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare and return the plugin data to send for a certain submission and assign.
|
||||
*
|
||||
* @param {any} assign Assignment.
|
||||
* @param {any} submission Submission to check data.
|
||||
* @param {any} inputData Data entered in the submission form.
|
||||
* @param {boolean} [offline] True to prepare the data for an offline submission, false otherwise.
|
||||
* @return {Promise<any>} Promise resolved with plugin data to send to server.
|
||||
*/
|
||||
prepareSubmissionPluginData(assign: any, submission: any, inputData: any, offline?: boolean): Promise<any> {
|
||||
const pluginData = {},
|
||||
promises = [];
|
||||
|
||||
submission.plugins.forEach((plugin) => {
|
||||
promises.push(this.submissionDelegate.preparePluginSubmissionData(assign, submission, plugin, inputData, pluginData,
|
||||
offline));
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
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 {number} assignId Assignment ID.
|
||||
* @param {string} folderName Name of the plugin folder. Must be unique (both in submission and feedback plugins).
|
||||
* @param {any[]} files List of files.
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved if success, rejected otherwise.
|
||||
*/
|
||||
storeSubmissionFiles(assignId: number, folderName: string, files: any[], userId?: number, siteId?: string): Promise<any> {
|
||||
// Get the folder where to store the files.
|
||||
return this.assignOffline.getSubmissionPluginFolder(assignId, folderName, userId, siteId).then((folderPath) => {
|
||||
return this.fileUploaderProvider.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 {number} assignId Assignment ID.
|
||||
* @param {any} file Online file or local FileEntry.
|
||||
* @param {number} [itemId] Draft ID to use. Undefined or 0 to create a new draft ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<number>} Promise resolved with the itemId.
|
||||
*/
|
||||
uploadFile(assignId: number, file: any, itemId?: number, siteId?: string): Promise<number> {
|
||||
return this.fileUploaderProvider.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 {number} assignId Assignment ID.
|
||||
* @param {any[]} files List of files.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<number>} Promise resolved with the itemId.
|
||||
*/
|
||||
uploadFiles(assignId: number, files: any[], siteId?: string): Promise<number> {
|
||||
return this.fileUploaderProvider.uploadOrReuploadFiles(files, AddonModAssignProvider.COMPONENT, assignId, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload or store some files, depending if the user is offline or not.
|
||||
*
|
||||
* @param {number} assignId Assignment ID.
|
||||
* @param {string} folderName Name of the plugin folder. Must be unique (both in submission and feedback plugins).
|
||||
* @param {any[]} files List of files.
|
||||
* @param {boolean} offline True if files sould be stored for offline, false to upload them.
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
uploadOrStoreFiles(assignId: number, folderName: string, files: any[], offline?: boolean, userId?: number, siteId?: string)
|
||||
: Promise<any> {
|
||||
|
||||
if (offline) {
|
||||
return this.storeSubmissionFiles(assignId, folderName, files, userId, siteId);
|
||||
} else {
|
||||
return this.uploadFiles(assignId, files, siteId);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 '@core/contentlinks/classes/module-index-handler';
|
||||
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
|
||||
|
||||
/**
|
||||
* Handler to treat links to assign index page.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModAssignIndexLinkHandler extends CoreContentLinksModuleIndexHandler {
|
||||
name = 'AddonModAssignIndexLinkHandler';
|
||||
|
||||
constructor(courseHelper: CoreCourseHelperProvider) {
|
||||
super(courseHelper, 'AddonModAssign', 'assign');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { NavController, NavOptions } from 'ionic-angular';
|
||||
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
import { AddonModAssignProvider } from './assign';
|
||||
import { AddonModAssignIndexComponent } from '../components/index/index';
|
||||
|
||||
/**
|
||||
* Handler to support assign modules.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModAssignModuleHandler implements CoreCourseModuleHandler {
|
||||
name = 'AddonModAssign';
|
||||
modName = 'assign';
|
||||
|
||||
constructor(private courseProvider: CoreCourseProvider, private assignProvider: AddonModAssignProvider) { }
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean} Whether or not the handler is enabled on a site level.
|
||||
*/
|
||||
isEnabled(): boolean {
|
||||
return this.assignProvider.isPluginEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data required to display the module in the course contents view.
|
||||
*
|
||||
* @param {any} module The module object.
|
||||
* @param {number} courseId The course ID.
|
||||
* @param {number} sectionId The section ID.
|
||||
* @return {CoreCourseModuleHandlerData} Data to render the module.
|
||||
*/
|
||||
getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData {
|
||||
return {
|
||||
icon: this.courseProvider.getModuleIconSrc('assign'),
|
||||
title: module.name,
|
||||
class: 'addon-mod_assign-handler',
|
||||
showDownloadButton: true,
|
||||
action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void {
|
||||
navCtrl.push('AddonModAssignIndexPage', {module: module, courseId: courseId}, options);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component to render the module. This is needed to support singleactivity course format.
|
||||
* The component returned must implement CoreCourseModuleMainComponent.
|
||||
*
|
||||
* @param {any} course The course object.
|
||||
* @param {any} module The module object.
|
||||
* @return {any} The component to use, undefined if not found.
|
||||
*/
|
||||
getMainComponent(course: any, module: any): any {
|
||||
return AddonModAssignIndexComponent;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,384 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, Injector } from '@angular/core';
|
||||
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||
import { CoreGroupsProvider } from '@providers/groups';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
|
||||
import { CoreGradesHelperProvider } from '@core/grades/providers/helper';
|
||||
import { CoreUserProvider } from '@core/user/providers/user';
|
||||
import { AddonModAssignProvider } from './assign';
|
||||
import { AddonModAssignHelperProvider } from './helper';
|
||||
import { AddonModAssignFeedbackDelegate } from './feedback-delegate';
|
||||
import { AddonModAssignSubmissionDelegate } from './submission-delegate';
|
||||
|
||||
/**
|
||||
* Handler to prefetch assigns.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModAssignPrefetchHandler extends CoreCourseModulePrefetchHandlerBase {
|
||||
name = 'AddonModAssign';
|
||||
modName = 'assign';
|
||||
component = AddonModAssignProvider.COMPONENT;
|
||||
updatesNames = /^configuration$|^.*files$|^submissions$|^grades$|^gradeitems$|^outcomes$|^comments$/;
|
||||
|
||||
constructor(protected injector: Injector, protected assignProvider: AddonModAssignProvider,
|
||||
protected textUtils: CoreTextUtilsProvider, protected feedbackDelegate: AddonModAssignFeedbackDelegate,
|
||||
protected submissionDelegate: AddonModAssignSubmissionDelegate, protected courseProvider: CoreCourseProvider,
|
||||
protected courseHelper: CoreCourseHelperProvider, protected filepoolProvider: CoreFilepoolProvider,
|
||||
protected groupsProvider: CoreGroupsProvider, protected gradesHelper: CoreGradesHelperProvider,
|
||||
protected userProvider: CoreUserProvider, protected assignHelper: AddonModAssignHelperProvider) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {any} module Module.
|
||||
* @param {number} courseId Course ID the module belongs to.
|
||||
* @return {boolean|Promise<boolean>} Whether the module can use check_updates. The promise should never be rejected.
|
||||
*/
|
||||
canUseCheckUpdates(module: any, courseId: number): boolean | Promise<boolean> {
|
||||
// Teachers cannot use the WS because it doesn't check student submissions.
|
||||
return this.assignProvider.getAssignment(courseId, module.id).then((assign) => {
|
||||
return this.assignProvider.getSubmissions(assign.id);
|
||||
}).then((data) => {
|
||||
return !data.canviewsubmissions;
|
||||
}).catch(() => {
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the module.
|
||||
*
|
||||
* @param {any} module The module object returned by WS.
|
||||
* @param {number} courseId Course ID.
|
||||
* @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch.
|
||||
* @return {Promise<any>} Promise resolved when all content is downloaded.
|
||||
*/
|
||||
download(module: any, courseId: number, dirPath?: string): Promise<any> {
|
||||
// Same implementation for download or prefetch.
|
||||
return this.prefetch(module, courseId, false, dirPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of files. If not defined, we'll assume they're in module.contents.
|
||||
*
|
||||
* @param {any} module Module.
|
||||
* @param {Number} courseId Course ID the module belongs to.
|
||||
* @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any[]>} Promise resolved with the list of files.
|
||||
*/
|
||||
getFiles(module: any, courseId: number, single?: boolean, siteId?: string): Promise<any[]> {
|
||||
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
return this.assignProvider.getAssignment(courseId, module.id, siteId).then((assign) => {
|
||||
// Get intro files and attachments.
|
||||
let files = assign.introattachments || [];
|
||||
files = files.concat(this.getIntroFilesFromInstance(module, assign));
|
||||
|
||||
// Now get the files in the submissions.
|
||||
return this.assignProvider.getSubmissions(assign.id, siteId).then((data) => {
|
||||
const blindMarking = assign.blindmarking && !assign.revealidentities;
|
||||
|
||||
if (data.canviewsubmissions) {
|
||||
// Teacher, get all submissions.
|
||||
return this.assignProvider.getSubmissionsUserData(data.submissions, courseId, assign.id, blindMarking,
|
||||
undefined, siteId).then((submissions) => {
|
||||
|
||||
const promises = [];
|
||||
|
||||
// Get all the files in the submissions.
|
||||
submissions.forEach((submission) => {
|
||||
promises.push(this.getSubmissionFiles(assign, submission.submitid, !!submission.blindid, siteId)
|
||||
.then((submissionFiles) => {
|
||||
files = files.concat(submissionFiles);
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return files;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Student, get only his/her submissions.
|
||||
const userId = this.sitesProvider.getCurrentSiteUserId();
|
||||
|
||||
return this.getSubmissionFiles(assign, userId, blindMarking, siteId).then((submissionFiles) => {
|
||||
files = files.concat(submissionFiles);
|
||||
|
||||
return files;
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
// Error getting data, return empty list.
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get submission files.
|
||||
*
|
||||
* @param {any} assign Assign.
|
||||
* @param {number} submitId User ID of the submission to get.
|
||||
* @param {boolean} blindMarking True if blind marking, false otherwise.
|
||||
* @param {string} siteId Site ID. If not defined, current site.
|
||||
* @return {Promise<any[]>} Promise resolved with array of files.
|
||||
*/
|
||||
protected getSubmissionFiles(assign: any, submitId: number, blindMarking: boolean, siteId?: string)
|
||||
: Promise<any[]> {
|
||||
|
||||
return this.assignProvider.getSubmissionStatus(assign.id, submitId, blindMarking, true, false, siteId).then((response) => {
|
||||
const promises = [];
|
||||
|
||||
if (response.lastattempt) {
|
||||
const userSubmission = this.assignProvider.getSubmissionObjectFromAttempt(assign, response.lastattempt);
|
||||
if (userSubmission && userSubmission.plugins) {
|
||||
// Add submission plugin files.
|
||||
userSubmission.plugins.forEach((plugin) => {
|
||||
promises.push(this.submissionDelegate.getPluginFiles(assign, userSubmission, plugin, siteId));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (response.feedback && response.feedback.plugins) {
|
||||
// Add feedback plugin files.
|
||||
response.feedback.plugins.forEach((plugin) => {
|
||||
promises.push(this.feedbackDelegate.getPluginFiles(assign, response, plugin, siteId));
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
|
||||
}).then((filesLists) => {
|
||||
let files = [];
|
||||
|
||||
filesLists.forEach((filesList) => {
|
||||
files = files.concat(filesList);
|
||||
});
|
||||
|
||||
return files;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the prefetched content.
|
||||
*
|
||||
* @param {number} moduleId The module ID.
|
||||
* @param {number} courseId The course ID the module belongs to.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateContent(moduleId: number, courseId: number): Promise<any> {
|
||||
return this.assignProvider.invalidateContent(moduleId, courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return this.assignProvider.isPluginEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch a module.
|
||||
*
|
||||
* @param {any} module Module.
|
||||
* @param {number} courseId Course ID the module belongs to.
|
||||
* @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section.
|
||||
* @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise<any> {
|
||||
return this.prefetchPackage(module, courseId, single, this.prefetchAssign.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch an assignment.
|
||||
*
|
||||
* @param {any} module Module.
|
||||
* @param {number} courseId Course ID the module belongs to.
|
||||
* @param {boolean} single True if we're downloading a single module, false if we're downloading a whole section.
|
||||
* @param {String} siteId Site ID.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected prefetchAssign(module: any, courseId: number, single: boolean, siteId: string): Promise<any> {
|
||||
const userId = this.sitesProvider.getCurrentSiteUserId(),
|
||||
promises = [];
|
||||
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
promises.push(this.courseProvider.getModuleBasicInfo(module.id, siteId));
|
||||
|
||||
// Get assignment to retrieve all its submissions.
|
||||
promises.push(this.assignProvider.getAssignment(courseId, module.id, siteId).then((assign) => {
|
||||
const subPromises = [],
|
||||
blindMarking = assign.blindmarking && !assign.revealidentities;
|
||||
|
||||
if (blindMarking) {
|
||||
subPromises.push(this.assignProvider.getAssignmentUserMappings(assign.id, undefined, siteId).catch(() => {
|
||||
// Ignore errors.
|
||||
}));
|
||||
}
|
||||
|
||||
subPromises.push(this.prefetchSubmissions(assign, courseId, module.id, userId, siteId));
|
||||
|
||||
subPromises.push(this.courseHelper.getModuleCourseIdByInstance(assign.id, 'assign', siteId));
|
||||
|
||||
// Get all files and fetch them.
|
||||
subPromises.push(this.getFiles(module, courseId, single, siteId).then((files) => {
|
||||
return this.filepoolProvider.addFilesToQueue(siteId, files, this.component, module.id);
|
||||
}));
|
||||
|
||||
return Promise.all(subPromises);
|
||||
}));
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch assign submissions.
|
||||
*
|
||||
* @param {any} assign Assign.
|
||||
* @param {number} courseId Course ID.
|
||||
* @param {number} moduleId Module ID.
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when prefetched, rejected otherwise.
|
||||
*/
|
||||
protected prefetchSubmissions(assign: any, courseId: number, moduleId: number, userId?: number, siteId?: string): Promise<any> {
|
||||
|
||||
// Get submissions.
|
||||
return this.assignProvider.getSubmissions(assign.id, siteId).then((data) => {
|
||||
const promises = [],
|
||||
blindMarking = assign.blindmarking && !assign.revealidentities;
|
||||
|
||||
if (data.canviewsubmissions) {
|
||||
// Teacher. Do not send participants to getSubmissionsUserData to retrieve user profiles.
|
||||
promises.push(this.assignProvider.getSubmissionsUserData(data.submissions, courseId, assign.id, blindMarking,
|
||||
undefined, siteId).then((submissions) => {
|
||||
|
||||
const subPromises = [];
|
||||
|
||||
submissions.forEach((submission) => {
|
||||
subPromises.push(this.assignProvider.getSubmissionStatus(assign.id, submission.submitid,
|
||||
!!submission.blindid, true, false, siteId).then((subm) => {
|
||||
return this.prefetchSubmission(assign, courseId, moduleId, subm, submission.submitid, siteId);
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(subPromises);
|
||||
}));
|
||||
|
||||
// Get list participants.
|
||||
promises.push(this.assignHelper.getParticipants(assign, siteId).then((participants) => {
|
||||
participants.forEach((participant) => {
|
||||
if (participant.profileimageurl) {
|
||||
this.filepoolProvider.addToQueueByUrl(siteId, participant.profileimageurl);
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
// Fail silently (Moodle < 3.2).
|
||||
}));
|
||||
} else {
|
||||
// Student.
|
||||
promises.push(this.assignProvider.getSubmissionStatus(assign.id, userId, false, true, false, siteId)
|
||||
.then((subm) => {
|
||||
return this.prefetchSubmission(assign, courseId, moduleId, subm, userId, siteId);
|
||||
}));
|
||||
}
|
||||
|
||||
promises.push(this.groupsProvider.activityHasGroups(assign.cmid));
|
||||
promises.push(this.groupsProvider.getActivityAllowedGroups(assign.cmid, undefined, siteId));
|
||||
|
||||
return Promise.all(promises);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch a submission.
|
||||
*
|
||||
* @param {any} assign Assign.
|
||||
* @param {number} courseId Course ID.
|
||||
* @param {number} moduleId Module ID.
|
||||
* @param {any} submission Data returned by AddonModAssignProvider.getSubmissionStatus.
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when prefetched, rejected otherwise.
|
||||
*/
|
||||
protected prefetchSubmission(assign: any, courseId: number, moduleId: number, submission: any, userId?: number,
|
||||
siteId?: string): Promise<any> {
|
||||
|
||||
const promises = [],
|
||||
blindMarking = assign.blindmarking && !assign.revealidentities;
|
||||
let userIds = [];
|
||||
|
||||
if (submission.lastattempt) {
|
||||
const userSubmission = this.assignProvider.getSubmissionObjectFromAttempt(assign, 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) => {
|
||||
promises.push(this.submissionDelegate.prefetch(assign, userSubmission, plugin, siteId));
|
||||
});
|
||||
}
|
||||
|
||||
// Get ID of the user who did the submission.
|
||||
if (userSubmission.userid) {
|
||||
userIds.push(userSubmission.userid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prefetch feedback.
|
||||
if (submission.feedback) {
|
||||
// Get profile and image of the grader.
|
||||
if (submission.feedback.grade && submission.feedback.grade.grader) {
|
||||
userIds.push(submission.feedback.grade.grader);
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
promises.push(this.gradesHelper.getGradeModuleItems(courseId, moduleId, userId, undefined, siteId));
|
||||
}
|
||||
|
||||
// Prefetch feedback plugins data.
|
||||
if (submission.feedback.plugins) {
|
||||
submission.feedback.plugins.forEach((plugin) => {
|
||||
promises.push(this.feedbackDelegate.prefetch(assign, submission, plugin, siteId));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Prefetch user profiles.
|
||||
promises.push(this.userProvider.prefetchProfiles(userIds, courseId, siteId));
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,405 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, Injector } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
||||
import { AddonModAssignDefaultSubmissionHandler } from './default-submission-handler';
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
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 {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {boolean|Promise<boolean>} Boolean or promise resolved with boolean: whether it can be edited in offline.
|
||||
*/
|
||||
canEditOffline?(assign: any, submission: any, plugin: any): boolean | Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Should clear temporary data for a cancelled submission.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the submission.
|
||||
*/
|
||||
clearTmpData?(assign: any, submission: any, plugin: any, 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 {any} assign The assignment.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} pluginData Object where to store the data to send.
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} If the function is async, it should return a Promise resolved when done.
|
||||
*/
|
||||
copySubmissionData?(assign: any, plugin: any, pluginData: any, userId?: number, siteId?: string): void | Promise<any>;
|
||||
|
||||
/**
|
||||
* Delete any stored data for the plugin and submission.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} offlineData Offline data stored.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} If the function is async, it should return a Promise resolved when done.
|
||||
*/
|
||||
deleteOfflineData?(assign: any, submission: any, plugin: any, 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 {Injector} injector Injector.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {boolean} [edit] Whether the user is editing.
|
||||
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||
*/
|
||||
getComponent?(injector: Injector, plugin: any, 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 {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {any[]|Promise<any[]>} The files (or promise resolved with the files).
|
||||
*/
|
||||
getPluginFiles?(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise<any[]>;
|
||||
|
||||
/**
|
||||
* Get a readable name to use for the plugin.
|
||||
*
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {string} The plugin name.
|
||||
*/
|
||||
getPluginName?(plugin: any): string;
|
||||
|
||||
/**
|
||||
* Get the size of data (in bytes) this plugin will send to copy a previous submission.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {number|Promise<number>} The size (or promise resolved with size).
|
||||
*/
|
||||
getSizeForCopy?(assign: any, plugin: any): number | Promise<number>;
|
||||
|
||||
/**
|
||||
* Get the size of data (in bytes) this plugin will send to add or edit a submission.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the submission.
|
||||
* @return {number|Promise<number>} The size (or promise resolved with size).
|
||||
*/
|
||||
getSizeForEdit?(assign: any, submission: any, plugin: any, inputData: any): number | Promise<number>;
|
||||
|
||||
/**
|
||||
* Check if the submission data has changed for this plugin.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the submission.
|
||||
* @return {boolean|Promise<boolean>} Boolean (or promise resolved with boolean): whether the data has changed.
|
||||
*/
|
||||
hasDataChanged?(assign: any, submission: any, plugin: any, inputData: any): boolean | Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled for edit on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} 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 {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
prefetch?(assign: any, submission: any, plugin: any, siteId?: string): Promise<any>;
|
||||
|
||||
/**
|
||||
* Prepare and add to pluginData the data to send to the server based on the input data.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the submission.
|
||||
* @param {any} pluginData Object where to store the data to send.
|
||||
* @param {boolean} [offline] Whether the user is editing in offline.
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} If the function is async, it should return a Promise resolved when done.
|
||||
*/
|
||||
prepareSubmissionData?(assign: any, submission: any, plugin: any, 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 {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} offlineData Offline data stored.
|
||||
* @param {any} pluginData Object where to store the data to send.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} If the function is async, it should return a Promise resolved when done.
|
||||
*/
|
||||
prepareSyncData?(assign: any, submission: any, plugin: any, offlineData: any, pluginData: any, siteId?: string)
|
||||
: void | Promise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate to register plugins for assign submission.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModAssignSubmissionDelegate extends CoreDelegate {
|
||||
|
||||
protected handlerNameProperty = 'type';
|
||||
|
||||
constructor(logger: CoreLoggerProvider, sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider,
|
||||
protected defaultHandler: AddonModAssignDefaultSubmissionHandler) {
|
||||
super('AddonModAssignSubmissionDelegate', logger, sitesProvider, eventsProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the plugin can be edited in offline for existing submissions.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {boolean|Promise<boolean>} Promise resolved with boolean: whether it can be edited in offline.
|
||||
*/
|
||||
canPluginEditOffline(assign: any, submission: any, plugin: any): Promise<boolean> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'canEditOffline', [assign, submission, plugin]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear some temporary data for a certain plugin because a submission was cancelled.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the submission.
|
||||
*/
|
||||
clearTmpData(assign: any, submission: any, plugin: any, 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 {any} assign The assignment.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} pluginData Object where to store the data to send.
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the data has been copied.
|
||||
*/
|
||||
copyPluginSubmissionData(assign: any, plugin: any, pluginData: any, userId?: number, siteId?: string): void | Promise<any> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'copySubmissionData',
|
||||
[assign, plugin, pluginData, userId, siteId]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete offline data stored for a certain submission and plugin.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} offlineData Offline data stored.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
deletePluginOfflineData(assign: any, submission: any, plugin: any, offlineData: any, siteId?: string): Promise<any> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'deleteOfflineData',
|
||||
[assign, submission, plugin, offlineData, siteId]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component to use for a certain submission plugin.
|
||||
*
|
||||
* @param {Injector} injector Injector.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {boolean} [edit] Whether the user is editing.
|
||||
* @return {Promise<any>} Promise resolved with the component to use, undefined if not found.
|
||||
*/
|
||||
getComponentForPlugin(injector: Injector, plugin: any, edit?: boolean): Promise<any> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'getComponent', [injector, plugin, edit]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files used by this plugin.
|
||||
* The files returned by this function will be prefetched when the user prefetches the assign.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any[]>} Promise resolved with the files.
|
||||
*/
|
||||
getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): Promise<any[]> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'getPluginFiles', [assign, submission, plugin, siteId]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a readable name to use for a certain submission plugin.
|
||||
*
|
||||
* @param {any} plugin Plugin to get the name for.
|
||||
* @return {string} Human readable name.
|
||||
*/
|
||||
getPluginName(plugin: any): string {
|
||||
return this.executeFunctionOnEnabled(plugin.type, 'getPluginName', [plugin]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of data (in bytes) this plugin will send to copy a previous submission.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {Promise<number>} Promise resolved with size.
|
||||
*/
|
||||
getPluginSizeForCopy(assign: any, plugin: any): Promise<number> {
|
||||
return Promise.resolve(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 {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the submission.
|
||||
* @return {Promise<number>} Promise resolved with size.
|
||||
*/
|
||||
getPluginSizeForEdit(assign: any, submission: any, plugin: any, inputData: any): Promise<number> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'getSizeForEdit',
|
||||
[assign, submission, plugin, inputData]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the submission data has changed for a certain plugin.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the submission.
|
||||
* @return {Promise<boolean>} Promise resolved with true if data has changed, resolved with false otherwise.
|
||||
*/
|
||||
hasPluginDataChanged(assign: any, submission: any, plugin: any, inputData: any): Promise<boolean> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'hasDataChanged',
|
||||
[assign, submission, plugin, inputData]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a submission plugin is supported.
|
||||
*
|
||||
* @param {string} pluginType Type of the plugin.
|
||||
* @return {boolean} Whether it's supported.
|
||||
*/
|
||||
isPluginSupported(pluginType: string): boolean {
|
||||
return this.hasHandler(pluginType, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a submission plugin is supported for edit.
|
||||
*
|
||||
* @param {string} pluginType Type of the plugin.
|
||||
* @return {Promise<boolean>} Whether it's supported for edit.
|
||||
*/
|
||||
isPluginSupportedForEdit(pluginType: string): Promise<boolean> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(pluginType, 'isEnabledForEdit'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch any required data for a submission plugin.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
prefetch(assign: any, submission: any, plugin: any, siteId?: string): Promise<any> {
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'prefetch', [assign, submission, plugin, siteId]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare and add to pluginData the data to submit for a certain submission plugin.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the submission.
|
||||
* @param {any} pluginData Object where to store the data to send.
|
||||
* @param {boolean} [offline] Whether the user is editing in offline.
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when data has been gathered.
|
||||
*/
|
||||
preparePluginSubmissionData(assign: any, submission: any, plugin: any, inputData: any, pluginData: any, offline?: boolean,
|
||||
userId?: number, siteId?: string): Promise<any> {
|
||||
|
||||
return Promise.resolve(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 {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} offlineData Offline data stored.
|
||||
* @param {any} pluginData Object where to store the data to send.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when data has been gathered.
|
||||
*/
|
||||
preparePluginSyncData(assign: any, submission: any, plugin: any, offlineData: any, pluginData: any, siteId?: string)
|
||||
: Promise<any> {
|
||||
|
||||
return Promise.resolve(this.executeFunctionOnEnabled(plugin.type, 'prepareSyncData',
|
||||
[assign, submission, plugin, offlineData, pluginData, siteId]));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 '@providers/cron';
|
||||
import { AddonModAssignSyncProvider } from './assign-sync';
|
||||
|
||||
/**
|
||||
* Synchronization cron handler.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModAssignSyncCronHandler implements CoreCronHandler {
|
||||
name = 'AddonModAssignSyncCronHandler';
|
||||
|
||||
constructor(private assignSync: AddonModAssignSyncProvider) {}
|
||||
|
||||
/**
|
||||
* Execute the process.
|
||||
* Receives the ID of the site affected, undefined for all sites.
|
||||
*
|
||||
* @param {string} [siteId] ID of the site affected, undefined for all sites.
|
||||
* @return {Promise<any>} Promise resolved when done, rejected if failure.
|
||||
*/
|
||||
execute(siteId?: string): Promise<any> {
|
||||
return this.assignSync.syncAllAssignments(siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time between consecutive executions.
|
||||
*
|
||||
* @return {number} Time between consecutive executions (in ms).
|
||||
*/
|
||||
getInterval(): number {
|
||||
return 600000; // 10 minutes.
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { AddonModAssignSubmissionCommentsHandler } from './providers/handler';
|
||||
import { AddonModAssignSubmissionCommentsComponent } from './component/comments';
|
||||
import { AddonModAssignSubmissionDelegate } from '../../providers/submission-delegate';
|
||||
import { CoreCommentsComponentsModule } from '@core/comments/components/components.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModAssignSubmissionCommentsComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreCommentsComponentsModule
|
||||
],
|
||||
providers: [
|
||||
AddonModAssignSubmissionCommentsHandler
|
||||
],
|
||||
exports: [
|
||||
AddonModAssignSubmissionCommentsComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModAssignSubmissionCommentsComponent
|
||||
]
|
||||
})
|
||||
export class AddonModAssignSubmissionCommentsModule {
|
||||
constructor(submissionDelegate: AddonModAssignSubmissionDelegate, handler: AddonModAssignSubmissionCommentsHandler) {
|
||||
submissionDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<ion-item text-wrap (click)="showComments()">
|
||||
<h2>{{plugin.name}}</h2>
|
||||
<core-comments contextLevel="module" [instanceId]="assign.cmid" component="assignsubmission_comments" [itemId]="submission.id" area="submission_comments" [title]="plugin.name"></core-comments>
|
||||
</ion-item>
|
|
@ -0,0 +1,50 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, ViewChild } from '@angular/core';
|
||||
import { CoreCommentsProvider } from '@core/comments/providers/comments';
|
||||
import { CoreCommentsCommentsComponent } from '@core/comments/components/comments/comments';
|
||||
import { AddonModAssignSubmissionPluginComponent } from '../../../classes/submission-plugin-component';
|
||||
|
||||
/**
|
||||
* Component to render a comments submission plugin.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-assign-submission-comments',
|
||||
templateUrl: 'comments.html'
|
||||
})
|
||||
export class AddonModAssignSubmissionCommentsComponent extends AddonModAssignSubmissionPluginComponent {
|
||||
@ViewChild(CoreCommentsCommentsComponent) commentsComponent: CoreCommentsCommentsComponent;
|
||||
|
||||
constructor(protected commentsProvider: CoreCommentsProvider) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the data.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
invalidate(): Promise<any> {
|
||||
return this.commentsProvider.invalidateCommentsData('module', this.assign.cmid, 'assignsubmission_comments',
|
||||
this.submission.id, 'submission_comments');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the comments.
|
||||
*/
|
||||
showComments(): void {
|
||||
this.commentsComponent && this.commentsComponent.openComments();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"pluginname": "Submission comments"
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
|
||||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, Injector } from '@angular/core';
|
||||
import { CoreCommentsProvider } from '@core/comments/providers/comments';
|
||||
import { AddonModAssignSubmissionHandler } from '../../../providers/submission-delegate';
|
||||
import { AddonModAssignSubmissionCommentsComponent } from '../component/comments';
|
||||
|
||||
/**
|
||||
* Handler for comments submission plugin.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModAssignSubmissionCommentsHandler implements AddonModAssignSubmissionHandler {
|
||||
name = 'AddonModAssignSubmissionCommentsHandler';
|
||||
type = 'comments';
|
||||
|
||||
constructor(private commentsProvider: CoreCommentsProvider) { }
|
||||
|
||||
/**
|
||||
* 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 {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {boolean|Promise<boolean>} Boolean or promise resolved with boolean: whether it can be edited in offline.
|
||||
*/
|
||||
canEditOffline(assign: any, submission: any, plugin: any): boolean | Promise<boolean> {
|
||||
// This plugin is read only, but return true to prevent blocking the edition.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {Injector} injector Injector.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {boolean} [edit] Whether the user is editing.
|
||||
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||
*/
|
||||
getComponent(injector: Injector, plugin: any, edit?: boolean): any | Promise<any> {
|
||||
return edit ? undefined : AddonModAssignSubmissionCommentsComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled for edit on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled for edit on a site level.
|
||||
*/
|
||||
isEnabledForEdit(): boolean | 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.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
prefetch(assign: any, submission: any, plugin: any, siteId?: string): Promise<any> {
|
||||
return this.commentsProvider.getComments('module', assign.cmid, 'assignsubmission_comments', submission.id,
|
||||
'submission_comments', 0, siteId).catch(() => {
|
||||
// Fail silently (Moodle < 3.1.1, 3.2)
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<!-- Read only. -->
|
||||
<ion-item text-wrap *ngIf="files && files.length && !edit">
|
||||
<h2>{{plugin.name}}</h2>
|
||||
<div *ngFor="let file of files" no-lines>
|
||||
<!-- Files already attached to the submission. -->
|
||||
<core-file *ngIf="!file.name" [file]="file" [component]="component" [componentId]="assign.cmid" [alwaysDownload]="true"></core-file>
|
||||
|
||||
<!-- Files stored in offline to be sent later. -->
|
||||
<core-local-file *ngIf="file.name" [file]="file"></core-local-file>
|
||||
</div>
|
||||
</ion-item>
|
||||
|
||||
<!-- Edit -->
|
||||
<div *ngIf="edit">
|
||||
<ion-item-divider text-wrap color="light">{{plugin.name}}</ion-item-divider>
|
||||
<core-attachments [files]="files" [maxSize]="configs.maxsubmissionsizebytes" [maxSubmissions]="configs.maxfilesubmissions" [component]="component" [componentId]="assign.cmid" [acceptedTypes]="configs.filetypeslist" [allowOffline]="allowOffline"></core-attachments>
|
||||
</div>
|
|
@ -0,0 +1,74 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 } from '@angular/core';
|
||||
import { CoreFileSessionProvider } from '@providers/file-session';
|
||||
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
|
||||
import { AddonModAssignProvider } from '../../../providers/assign';
|
||||
import { AddonModAssignHelperProvider } from '../../../providers/helper';
|
||||
import { AddonModAssignOfflineProvider } from '../../../providers/assign-offline';
|
||||
import { AddonModAssignSubmissionFileHandler } from '../providers/handler';
|
||||
import { AddonModAssignSubmissionPluginComponent } from '../../../classes/submission-plugin-component';
|
||||
|
||||
/**
|
||||
* Component to render a file submission plugin.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-assign-submission-file',
|
||||
templateUrl: 'file.html'
|
||||
})
|
||||
export class AddonModAssignSubmissionFileComponent extends AddonModAssignSubmissionPluginComponent implements OnInit {
|
||||
|
||||
component = AddonModAssignProvider.COMPONENT;
|
||||
files: any[];
|
||||
|
||||
constructor(protected fileSessionprovider: CoreFileSessionProvider, protected assignProvider: AddonModAssignProvider,
|
||||
protected assignOfflineProvider: AddonModAssignOfflineProvider, protected assignHelper: AddonModAssignHelperProvider,
|
||||
protected fileUploaderProvider: CoreFileUploaderProvider) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Get the offline data.
|
||||
this.assignOfflineProvider.getSubmission(this.assign.id).catch(() => {
|
||||
// Error getting data, assume there's no offline submission.
|
||||
}).then((offlineData) => {
|
||||
if (offlineData && offlineData.pluginData && offlineData.pluginData.files_filemanager) {
|
||||
// It has offline data.
|
||||
let promise;
|
||||
if (offlineData.pluginData.files_filemanager.offline) {
|
||||
promise = this.assignHelper.getStoredSubmissionFiles(this.assign.id,
|
||||
AddonModAssignSubmissionFileHandler.FOLDER_NAME);
|
||||
} else {
|
||||
promise = Promise.resolve([]);
|
||||
}
|
||||
|
||||
return promise.then((offlineFiles) => {
|
||||
const onlineFiles = offlineData.pluginData.files_filemanager.online || [];
|
||||
offlineFiles = this.fileUploaderProvider.markOfflineFiles(offlineFiles);
|
||||
|
||||
this.files = onlineFiles.concat(offlineFiles);
|
||||
});
|
||||
} else {
|
||||
// No offline data, get the online files.
|
||||
this.files = this.assignProvider.getSubmissionPluginAttachments(this.plugin);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.fileSessionprovider.setFiles(this.component, this.assign.id, this.files);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { AddonModAssignSubmissionFileHandler } from './providers/handler';
|
||||
import { AddonModAssignSubmissionFileComponent } from './component/file';
|
||||
import { AddonModAssignSubmissionDelegate } from '../../providers/submission-delegate';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModAssignSubmissionFileComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule
|
||||
],
|
||||
providers: [
|
||||
AddonModAssignSubmissionFileHandler
|
||||
],
|
||||
exports: [
|
||||
AddonModAssignSubmissionFileComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModAssignSubmissionFileComponent
|
||||
]
|
||||
})
|
||||
export class AddonModAssignSubmissionFileModule {
|
||||
constructor(submissionDelegate: AddonModAssignSubmissionDelegate, handler: AddonModAssignSubmissionFileHandler) {
|
||||
submissionDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"pluginname": "File submissions"
|
||||
}
|
|
@ -0,0 +1,361 @@
|
|||
|
||||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, Injector } from '@angular/core';
|
||||
import { CoreFileProvider } from '@providers/file';
|
||||
import { CoreFileSessionProvider } from '@providers/file-session';
|
||||
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreWSProvider } from '@providers/ws';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
|
||||
import { AddonModAssignProvider } from '../../../providers/assign';
|
||||
import { AddonModAssignOfflineProvider } from '../../../providers/assign-offline';
|
||||
import { AddonModAssignHelperProvider } from '../../../providers/helper';
|
||||
import { AddonModAssignSubmissionHandler } from '../../../providers/submission-delegate';
|
||||
import { AddonModAssignSubmissionFileComponent } from '../component/file';
|
||||
|
||||
/**
|
||||
* Handler for file submission plugin.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModAssignSubmissionFileHandler implements AddonModAssignSubmissionHandler {
|
||||
static FOLDER_NAME = 'submission_file';
|
||||
|
||||
name = 'AddonModAssignSubmissionFileHandler';
|
||||
type = 'file';
|
||||
|
||||
constructor(private sitesProvider: CoreSitesProvider, private wsProvider: CoreWSProvider,
|
||||
private assignProvider: AddonModAssignProvider, private assignOfflineProvider: AddonModAssignOfflineProvider,
|
||||
private assignHelper: AddonModAssignHelperProvider, private fileSessionProvider: CoreFileSessionProvider,
|
||||
private fileUploaderProvider: CoreFileUploaderProvider, private filepoolProvider: CoreFilepoolProvider,
|
||||
private fileProvider: CoreFileProvider, private utils: CoreUtilsProvider) { }
|
||||
|
||||
/**
|
||||
* 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 {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {boolean|Promise<boolean>} Boolean or promise resolved with boolean: whether it can be edited in offline.
|
||||
*/
|
||||
canEditOffline(assign: any, submission: any, plugin: any): boolean | Promise<boolean> {
|
||||
// This plugin doesn't use Moodle filters, it can be edited in offline.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should clear temporary data for a cancelled submission.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the submission.
|
||||
*/
|
||||
clearTmpData(assign: any, submission: any, plugin: any, inputData: any): void {
|
||||
const files = this.fileSessionProvider.getFiles(AddonModAssignProvider.COMPONENT, assign.id);
|
||||
|
||||
// Clear the files in session for this assign.
|
||||
this.fileSessionProvider.clearFiles(AddonModAssignProvider.COMPONENT, assign.id);
|
||||
|
||||
// Now delete the local files from the tmp folder.
|
||||
this.fileUploaderProvider.clearTmpFiles(files);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {any} assign The assignment.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} pluginData Object where to store the data to send.
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} If the function is async, it should return a Promise resolved when done.
|
||||
*/
|
||||
copySubmissionData(assign: any, plugin: any, pluginData: any, userId?: number, siteId?: string): void | Promise<any> {
|
||||
// We need to re-upload all the existing files.
|
||||
const files = this.assignProvider.getSubmissionPluginAttachments(plugin);
|
||||
|
||||
return this.assignHelper.uploadFiles(assign.id, files).then((itemId) => {
|
||||
pluginData.files_filemanager = itemId;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {Injector} injector Injector.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {boolean} [edit] Whether the user is editing.
|
||||
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||
*/
|
||||
getComponent(injector: Injector, plugin: any, edit?: boolean): any | Promise<any> {
|
||||
return AddonModAssignSubmissionFileComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete any stored data for the plugin and submission.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} offlineData Offline data stored.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} If the function is async, it should return a Promise resolved when done.
|
||||
*/
|
||||
deleteOfflineData(assign: any, submission: any, plugin: any, offlineData: any, siteId?: string): void | Promise<any> {
|
||||
return this.assignHelper.deleteStoredSubmissionFiles(assign.id, AddonModAssignSubmissionFileHandler.FOLDER_NAME,
|
||||
submission.userid, siteId).catch(() => {
|
||||
// Ignore errors, maybe the folder doesn't exist.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files used by this plugin.
|
||||
* The files returned by this function will be prefetched when the user prefetches the assign.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {any[]|Promise<any[]>} The files (or promise resolved with the files).
|
||||
*/
|
||||
getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise<any[]> {
|
||||
return this.assignProvider.getSubmissionPluginAttachments(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of data (in bytes) this plugin will send to copy a previous submission.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {number|Promise<number>} The size (or promise resolved with size).
|
||||
*/
|
||||
getSizeForCopy(assign: any, plugin: any): number | Promise<number> {
|
||||
const files = this.assignProvider.getSubmissionPluginAttachments(plugin),
|
||||
promises = [];
|
||||
let totalSize = 0;
|
||||
|
||||
files.forEach((file) => {
|
||||
promises.push(this.wsProvider.getRemoteFileSize(file.fileurl).then((size) => {
|
||||
if (size == -1) {
|
||||
// Couldn't determine the size, reject.
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
totalSize += size;
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return totalSize;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of data (in bytes) this plugin will send to add or edit a submission.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the submission.
|
||||
* @return {number|Promise<number>} The size (or promise resolved with size).
|
||||
*/
|
||||
getSizeForEdit(assign: any, submission: any, plugin: any, inputData: any): number | Promise<number> {
|
||||
const siteId = this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
// Check if there's any change.
|
||||
if (this.hasDataChanged(assign, submission, plugin, inputData)) {
|
||||
const files = this.fileSessionProvider.getFiles(AddonModAssignProvider.COMPONENT, assign.id),
|
||||
promises = [];
|
||||
let totalSize = 0;
|
||||
|
||||
files.forEach((file) => {
|
||||
if (file.filename && !file.name) {
|
||||
// It's a remote file. First check if we have the file downloaded since it's more reliable.
|
||||
promises.push(this.filepoolProvider.getFilePathByUrl(siteId, file.fileurl).then((path) => {
|
||||
return this.fileProvider.getFile(path).then((fileEntry) => {
|
||||
return this.fileProvider.getFileObjectFromFileEntry(fileEntry);
|
||||
}).then((file) => {
|
||||
totalSize += file.size;
|
||||
});
|
||||
}).catch(() => {
|
||||
// Error getting the file, maybe it's not downloaded. Get remote size.
|
||||
return this.wsProvider.getRemoteFileSize(file.fileurl).then((size) => {
|
||||
if (size == -1) {
|
||||
// Couldn't determine the size, reject.
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
totalSize += size;
|
||||
});
|
||||
}));
|
||||
} else if (file.name) {
|
||||
// It's a local file, get its size.
|
||||
promises.push(this.fileProvider.getFileObjectFromFileEntry(file).then((file) => {
|
||||
totalSize += file.size;
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return totalSize;
|
||||
});
|
||||
} else {
|
||||
// Nothing has changed, we won't upload any file.
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the submission data has changed for this plugin.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the submission.
|
||||
* @return {boolean|Promise<boolean>} Boolean (or promise resolved with boolean): whether the data has changed.
|
||||
*/
|
||||
hasDataChanged(assign: any, submission: any, plugin: any, inputData: any): boolean | Promise<boolean> {
|
||||
// Check if there's any offline data.
|
||||
return this.assignOfflineProvider.getSubmission(assign.id, submission.userid).catch(() => {
|
||||
// No offline data found.
|
||||
}).then((offlineData) => {
|
||||
if (offlineData && offlineData.pluginData && offlineData.pluginData.files_filemanager) {
|
||||
// Has offline data, return the number of files.
|
||||
return offlineData.pluginData.files_filemanager.offline + offlineData.pluginData.files_filemanager.online.length;
|
||||
}
|
||||
|
||||
// No offline data, return the number of online files.
|
||||
const pluginFiles = this.assignProvider.getSubmissionPluginAttachments(plugin);
|
||||
|
||||
return pluginFiles && pluginFiles.length;
|
||||
}).then((numFiles) => {
|
||||
const currentFiles = this.fileSessionProvider.getFiles(AddonModAssignProvider.COMPONENT, assign.id);
|
||||
|
||||
if (currentFiles.length != numFiles) {
|
||||
// Number of files has changed.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Search if there is any local file added.
|
||||
for (let i = 0; i < currentFiles.length; i++) {
|
||||
const file = currentFiles[i];
|
||||
if (!file.filename && typeof file.name != 'undefined' && !file.offline) {
|
||||
// There's a local file added, list has changed.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// No local files and list length is the same, this means the list hasn't changed.
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled for edit on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled for edit on a site level.
|
||||
*/
|
||||
isEnabledForEdit(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare and add to pluginData the data to send to the server based on the input data.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the submission.
|
||||
* @param {any} pluginData Object where to store the data to send.
|
||||
* @param {boolean} [offline] Whether the user is editing in offline.
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} If the function is async, it should return a Promise resolved when done.
|
||||
*/
|
||||
prepareSubmissionData(assign: any, submission: any, plugin: any, inputData: any, pluginData: any, offline?: boolean,
|
||||
userId?: number, siteId?: string): void | Promise<any> {
|
||||
|
||||
if (this.hasDataChanged(assign, submission, plugin, inputData)) {
|
||||
// Data has changed, we need to upload new files and re-upload all the existing files.
|
||||
const currentFiles = this.fileSessionProvider.getFiles(AddonModAssignProvider.COMPONENT, assign.id),
|
||||
error = this.utils.hasRepeatedFilenames(currentFiles);
|
||||
|
||||
if (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
return this.assignHelper.uploadOrStoreFiles(assign.id, AddonModAssignSubmissionFileHandler.FOLDER_NAME,
|
||||
currentFiles, offline, userId, siteId).then((result) => {
|
||||
pluginData.files_filemanager = result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} offlineData Offline data stored.
|
||||
* @param {any} pluginData Object where to store the data to send.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} If the function is async, it should return a Promise resolved when done.
|
||||
*/
|
||||
prepareSyncData(assign: any, submission: any, plugin: any, offlineData: any, pluginData: any, siteId?: string)
|
||||
: void | Promise<any> {
|
||||
|
||||
const filesData = offlineData && offlineData.pluginData && offlineData.pluginData.files_filemanager;
|
||||
if (filesData) {
|
||||
// Has some data to sync.
|
||||
let files = filesData.online || [],
|
||||
promise;
|
||||
|
||||
if (filesData.offline) {
|
||||
// Has offline files, get them and add them to the list.
|
||||
promise = this.assignHelper.getStoredSubmissionFiles(assign.id, AddonModAssignSubmissionFileHandler.FOLDER_NAME,
|
||||
submission.userid, siteId).then((result) => {
|
||||
files = files.concat(result);
|
||||
}).catch(() => {
|
||||
// Folder not found, no files to add.
|
||||
});
|
||||
} else {
|
||||
promise = Promise.resolve();
|
||||
}
|
||||
|
||||
return promise.then(() => {
|
||||
return this.assignHelper.uploadFiles(assign.id, files, siteId).then((itemId) => {
|
||||
pluginData.files_filemanager = itemId;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<!-- Read only -->
|
||||
<ion-item text-wrap *ngIf="!edit && text">
|
||||
<h2>{{ plugin.name }}</h2>
|
||||
<p *ngIf="words">{{ 'addon.mod_assign.numwords' | translate: {'$a': words} }}</p>
|
||||
<p>
|
||||
<core-format-text [component]="component" [componentId]="assign.cmid" [maxHeight]="80" [fullOnClick]="true" [fullTitle]="plugin.name" [text]="text"></core-format-text>
|
||||
</p>
|
||||
</ion-item>
|
||||
|
||||
<!-- Edit -->
|
||||
<div *ngIf="edit && loaded">
|
||||
<ion-item-divider text-wrap color="light">{{ plugin.name }}</ion-item-divider>
|
||||
<ion-item text-wrap *ngIf="configs.wordlimitenabled && words >= 0">
|
||||
<h2>{{ 'addon.mod_assign.wordlimit' | translate }}</h2>
|
||||
<p>{{ 'core.numwords' | translate: {'$a': words + ' / ' + configs.wordlimit} }}</p>
|
||||
</ion-item>
|
||||
<ion-item text-wrap>
|
||||
<!-- @todo: [component]="component" [componentId]="assign.cmid" -->
|
||||
<core-rich-text-editor item-content [control]="control" [placeholder]="plugin.name" name="onlinetext_editor_text" (contentChanged)="onChange($event)"></core-rich-text-editor>
|
||||
</ion-item>
|
||||
</div>
|
|
@ -0,0 +1,129 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, ElementRef } from '@angular/core';
|
||||
import { FormBuilder, FormControl } from '@angular/forms';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { AddonModAssignProvider } from '../../../providers/assign';
|
||||
import { AddonModAssignOfflineProvider } from '../../../providers/assign-offline';
|
||||
import { AddonModAssignSubmissionPluginComponent } from '../../../classes/submission-plugin-component';
|
||||
|
||||
/**
|
||||
* Component to render an onlinetext submission plugin.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-assign-submission-online-text',
|
||||
templateUrl: 'onlinetext.html'
|
||||
})
|
||||
export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignSubmissionPluginComponent implements OnInit {
|
||||
|
||||
control: FormControl;
|
||||
words: number;
|
||||
component = AddonModAssignProvider.COMPONENT;
|
||||
text: string;
|
||||
loaded: boolean;
|
||||
|
||||
protected wordCountTimeout: any;
|
||||
protected element: HTMLElement;
|
||||
|
||||
constructor(protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected textUtils: CoreTextUtilsProvider,
|
||||
protected assignProvider: AddonModAssignProvider, protected assignOfflineProvider: AddonModAssignOfflineProvider,
|
||||
element: ElementRef) {
|
||||
|
||||
super();
|
||||
this.element = element.nativeElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
let promise,
|
||||
rteEnabled;
|
||||
|
||||
// Check if rich text editor is enabled.
|
||||
if (this.edit) {
|
||||
promise = this.domUtils.isRichTextEditorEnabled();
|
||||
} else {
|
||||
// We aren't editing, so no rich text editor.
|
||||
promise = Promise.resolve(false);
|
||||
}
|
||||
|
||||
promise.then((enabled) => {
|
||||
rteEnabled = enabled;
|
||||
|
||||
// Get the text. Check if we have anything offline.
|
||||
return this.assignOfflineProvider.getSubmission(this.assign.id).catch(() => {
|
||||
// No offline data found.
|
||||
}).then((offlineData) => {
|
||||
if (offlineData && offlineData.pluginData && offlineData.pluginData.onlinetext_editor) {
|
||||
return offlineData.pluginData.onlinetext_editor.text;
|
||||
}
|
||||
|
||||
// No offline data found, return online text.
|
||||
return this.assignProvider.getSubmissionPluginText(this.plugin, this.edit && !rteEnabled);
|
||||
});
|
||||
}).then((text) => {
|
||||
// We receive them as strings, convert to int.
|
||||
this.configs.wordlimit = parseInt(this.configs.wordlimit, 10);
|
||||
this.configs.wordlimitenabled = parseInt(this.configs.wordlimitenabled, 10);
|
||||
|
||||
// Set the text.
|
||||
this.text = text;
|
||||
|
||||
if (!this.edit) {
|
||||
// Not editing, see full text when clicked.
|
||||
this.element.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (text) {
|
||||
// Open a new state with the interpolated contents.
|
||||
this.textUtils.expandText(this.plugin.name, text, this.component, this.assign.cmid);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Create and add the control.
|
||||
this.control = this.fb.control(text);
|
||||
}
|
||||
|
||||
// Calculate initial words.
|
||||
if (this.configs.wordlimitenabled) {
|
||||
this.words = this.textUtils.countWords(text);
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Text changed.
|
||||
*
|
||||
* @param {string} text The new text.
|
||||
*/
|
||||
onChange(text: string): void {
|
||||
// Count words if needed.
|
||||
if (this.configs.wordlimitenabled) {
|
||||
// Cancel previous wait.
|
||||
clearTimeout(this.wordCountTimeout);
|
||||
|
||||
// Wait before calculating, if the user keeps inputing we won't calculate.
|
||||
// This is to prevent slowing down devices, this calculation can be slow if the text is long.
|
||||
this.wordCountTimeout = setTimeout(() => {
|
||||
this.words = this.textUtils.countWords(text);
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"pluginname": "Online text submissions"
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { AddonModAssignSubmissionOnlineTextHandler } from './providers/handler';
|
||||
import { AddonModAssignSubmissionOnlineTextComponent } from './component/onlinetext';
|
||||
import { AddonModAssignSubmissionDelegate } from '../../providers/submission-delegate';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModAssignSubmissionOnlineTextComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule
|
||||
],
|
||||
providers: [
|
||||
AddonModAssignSubmissionOnlineTextHandler
|
||||
],
|
||||
exports: [
|
||||
AddonModAssignSubmissionOnlineTextComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModAssignSubmissionOnlineTextComponent
|
||||
]
|
||||
})
|
||||
export class AddonModAssignSubmissionOnlineTextModule {
|
||||
constructor(submissionDelegate: AddonModAssignSubmissionDelegate, handler: AddonModAssignSubmissionOnlineTextHandler) {
|
||||
submissionDelegate.registerHandler(handler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,277 @@
|
|||
|
||||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, Injector } from '@angular/core';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreWSProvider } from '@providers/ws';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { AddonModAssignProvider } from '../../../providers/assign';
|
||||
import { AddonModAssignOfflineProvider } from '../../../providers/assign-offline';
|
||||
import { AddonModAssignHelperProvider } from '../../../providers/helper';
|
||||
import { AddonModAssignSubmissionHandler } from '../../../providers/submission-delegate';
|
||||
import { AddonModAssignSubmissionOnlineTextComponent } from '../component/onlinetext';
|
||||
|
||||
/**
|
||||
* Handler for online text submission plugin.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModAssignSubmissionOnlineTextHandler implements AddonModAssignSubmissionHandler {
|
||||
name = 'AddonModAssignSubmissionOnlineTextHandler';
|
||||
type = 'onlinetext';
|
||||
|
||||
constructor(private sitesProvider: CoreSitesProvider, private wsProvider: CoreWSProvider,
|
||||
private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider,
|
||||
private assignProvider: AddonModAssignProvider, private assignOfflineProvider: AddonModAssignOfflineProvider,
|
||||
private assignHelper: AddonModAssignHelperProvider) { }
|
||||
|
||||
/**
|
||||
* 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 {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {boolean|Promise<boolean>} Boolean or promise resolved with boolean: whether it can be edited in offline.
|
||||
*/
|
||||
canEditOffline(assign: any, submission: any, plugin: any): boolean | Promise<boolean> {
|
||||
// This plugin uses Moodle filters, it cannot be edited in offline.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {any} assign The assignment.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} pluginData Object where to store the data to send.
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} If the function is async, it should return a Promise resolved when done.
|
||||
*/
|
||||
copySubmissionData(assign: any, plugin: any, pluginData: any, userId?: number, siteId?: string): void | Promise<any> {
|
||||
const text = this.assignProvider.getSubmissionPluginText(plugin, true),
|
||||
files = this.assignProvider.getSubmissionPluginAttachments(plugin);
|
||||
let promise;
|
||||
|
||||
if (!files.length) {
|
||||
// No files to copy, no item ID.
|
||||
promise = Promise.resolve(0);
|
||||
} else {
|
||||
// Re-upload the files.
|
||||
promise = this.assignHelper.uploadFiles(assign.id, files, siteId);
|
||||
}
|
||||
|
||||
return promise.then((itemId) => {
|
||||
pluginData.onlinetext_editor = {
|
||||
text: text,
|
||||
format: 1,
|
||||
itemid: itemId
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {Injector} injector Injector.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {boolean} [edit] Whether the user is editing.
|
||||
* @return {any|Promise<any>} The component (or promise resolved with component) to use, undefined if not found.
|
||||
*/
|
||||
getComponent(injector: Injector, plugin: any, edit?: boolean): any | Promise<any> {
|
||||
return AddonModAssignSubmissionOnlineTextComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files used by this plugin.
|
||||
* The files returned by this function will be prefetched when the user prefetches the assign.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {any[]|Promise<any[]>} The files (or promise resolved with the files).
|
||||
*/
|
||||
getPluginFiles(assign: any, submission: any, plugin: any, siteId?: string): any[] | Promise<any[]> {
|
||||
return this.assignProvider.getSubmissionPluginAttachments(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of data (in bytes) this plugin will send to copy a previous submission.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @return {number|Promise<number>} The size (or promise resolved with size).
|
||||
*/
|
||||
getSizeForCopy(assign: any, plugin: any): number | Promise<number> {
|
||||
const text = this.assignProvider.getSubmissionPluginText(plugin, true),
|
||||
files = this.assignProvider.getSubmissionPluginAttachments(plugin),
|
||||
promises = [];
|
||||
let totalSize = text.length;
|
||||
|
||||
if (!files.length) {
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
files.forEach((file) => {
|
||||
promises.push(this.wsProvider.getRemoteFileSize(file.fileurl).then((size) => {
|
||||
if (size == -1) {
|
||||
// Couldn't determine the size, reject.
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
totalSize += size;
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return totalSize;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of data (in bytes) this plugin will send to add or edit a submission.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the submission.
|
||||
* @return {number|Promise<number>} The size (or promise resolved with size).
|
||||
*/
|
||||
getSizeForEdit(assign: any, submission: any, plugin: any, inputData: any): number | Promise<number> {
|
||||
const text = this.assignProvider.getSubmissionPluginText(plugin, true);
|
||||
|
||||
return text.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text to submit.
|
||||
*
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the submission.
|
||||
* @return {string} Text to submit.
|
||||
*/
|
||||
protected getTextToSubmit(plugin: any, inputData: any): string {
|
||||
const text = inputData.onlinetext_editor_text,
|
||||
files = plugin.fileareas && plugin.fileareas[0] ? plugin.fileareas[0].files : [];
|
||||
|
||||
return this.textUtils.restorePluginfileUrls(text, files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the submission data has changed for this plugin.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the submission.
|
||||
* @return {boolean|Promise<boolean>} Boolean (or promise resolved with boolean): whether the data has changed.
|
||||
*/
|
||||
hasDataChanged(assign: any, submission: any, plugin: any, inputData: any): boolean | Promise<boolean> {
|
||||
// Get the original text from plugin or offline.
|
||||
return this.assignOfflineProvider.getSubmission(assign.id, submission.userid).catch(() => {
|
||||
// No offline data found.
|
||||
}).then((data) => {
|
||||
if (data && data.pluginData && data.pluginData.onlinetext_editor) {
|
||||
return data.pluginData.onlinetext_editor.text;
|
||||
}
|
||||
|
||||
// No offline data found, get text from plugin.
|
||||
return plugin.editorfields && plugin.editorfields[0] ? plugin.editorfields[0].text : '';
|
||||
}).then((initialText) => {
|
||||
// Check if text has changed.
|
||||
return initialText != this.getTextToSubmit(plugin, inputData);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled for edit on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} Whether or not the handler is enabled for edit on a site level.
|
||||
*/
|
||||
isEnabledForEdit(): boolean | Promise<boolean> {
|
||||
// There's a bug in Moodle 3.1.0 that doesn't allow submitting HTML, so we'll disable this plugin in that case.
|
||||
// Bug was fixed in 3.1.1 minor release and in 3.2.
|
||||
const currentSite = this.sitesProvider.getCurrentSite();
|
||||
|
||||
return currentSite.isVersionGreaterEqualThan('3.1.1') || currentSite.checkIfAppUsesLocalMobile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare and add to pluginData the data to send to the server based on the input data.
|
||||
*
|
||||
* @param {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} inputData Data entered by the user for the submission.
|
||||
* @param {any} pluginData Object where to store the data to send.
|
||||
* @param {boolean} [offline] Whether the user is editing in offline.
|
||||
* @param {number} [userId] User ID. If not defined, site's current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} If the function is async, it should return a Promise resolved when done.
|
||||
*/
|
||||
prepareSubmissionData(assign: any, submission: any, plugin: any, inputData: any, pluginData: any, offline?: boolean,
|
||||
userId?: number, siteId?: string): void | Promise<any> {
|
||||
|
||||
return this.domUtils.isRichTextEditorEnabled().then((enabled) => {
|
||||
let text = this.getTextToSubmit(plugin, inputData);
|
||||
if (!enabled) {
|
||||
// Rich text editor not enabled, add some HTML to the text if needed.
|
||||
text = this.textUtils.formatHtmlLines(text);
|
||||
}
|
||||
|
||||
pluginData.onlinetext_editor = {
|
||||
text: text,
|
||||
format: 1,
|
||||
itemid: 0 // Can't add new files yet, so we use a fake itemid.
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {any} assign The assignment.
|
||||
* @param {any} submission The submission.
|
||||
* @param {any} plugin The plugin object.
|
||||
* @param {any} offlineData Offline data stored.
|
||||
* @param {any} pluginData Object where to store the data to send.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {void|Promise<any>} If the function is async, it should return a Promise resolved when done.
|
||||
*/
|
||||
prepareSyncData(assign: any, submission: any, plugin: any, offlineData: any, pluginData: any, siteId?: string)
|
||||
: void | Promise<any> {
|
||||
|
||||
const textData = offlineData && offlineData.plugindata && offlineData.plugindata.onlinetext_editor;
|
||||
if (textData) {
|
||||
// Has some data to sync.
|
||||
pluginData.onlinetext_editor = textData;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { AddonModAssignSubmissionCommentsModule } from './comments/comments.module';
|
||||
import { AddonModAssignSubmissionFileModule } from './file/file.module';
|
||||
import { AddonModAssignSubmissionOnlineTextModule } from './onlinetext/onlinetext.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
AddonModAssignSubmissionCommentsModule,
|
||||
AddonModAssignSubmissionFileModule,
|
||||
AddonModAssignSubmissionOnlineTextModule
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
exports: []
|
||||
})
|
||||
export class AddonModAssignSubmissionModule { }
|
|
@ -48,9 +48,9 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
protected hasAnsweredOnline = false;
|
||||
protected now: number;
|
||||
|
||||
constructor(injector: Injector, private choiceProvider: AddonModChoiceProvider, @Optional() private content: Content,
|
||||
constructor(injector: Injector, private choiceProvider: AddonModChoiceProvider, @Optional() content: Content,
|
||||
private choiceOffline: AddonModChoiceOfflineProvider, private choiceSync: AddonModChoiceSyncProvider) {
|
||||
super(injector);
|
||||
super(injector, content);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -65,11 +65,11 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity
|
|||
|
||||
protected submitObserver: any;
|
||||
|
||||
constructor(injector: Injector, private feedbackProvider: AddonModFeedbackProvider, @Optional() private content: Content,
|
||||
constructor(injector: Injector, private feedbackProvider: AddonModFeedbackProvider, @Optional() content: Content,
|
||||
private feedbackOffline: AddonModFeedbackOfflineProvider, private groupsProvider: CoreGroupsProvider,
|
||||
private feedbackSync: AddonModFeedbackSyncProvider, private navCtrl: NavController,
|
||||
private feedbackHelper: AddonModFeedbackHelperProvider) {
|
||||
super(injector);
|
||||
super(injector, content);
|
||||
|
||||
// Listen for form submit events.
|
||||
this.submitObserver = this.eventsProvider.on(AddonModFeedbackProvider.FORM_SUBMITTED, (data) => {
|
||||
|
|
|
@ -68,12 +68,12 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
|
|||
protected finishedObserver: any; // It will observe attempt finished events.
|
||||
protected hasPlayed = false; // Whether the user has gone to the quiz player (attempted).
|
||||
|
||||
constructor(injector: Injector, protected quizProvider: AddonModQuizProvider, @Optional() protected content: Content,
|
||||
constructor(injector: Injector, protected quizProvider: AddonModQuizProvider, @Optional() content: Content,
|
||||
protected quizHelper: AddonModQuizHelperProvider, protected quizOffline: AddonModQuizOfflineProvider,
|
||||
protected quizSync: AddonModQuizSyncProvider, protected behaviourDelegate: CoreQuestionBehaviourDelegate,
|
||||
protected prefetchHandler: AddonModQuizPrefetchHandler, protected navCtrl: NavController,
|
||||
protected prefetchDelegate: CoreCourseModulePrefetchDelegate) {
|
||||
super(injector);
|
||||
super(injector, content);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -38,10 +38,10 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
protected userId: number;
|
||||
protected syncEventName = AddonModSurveySyncProvider.AUTO_SYNCED;
|
||||
|
||||
constructor(injector: Injector, private surveyProvider: AddonModSurveyProvider, @Optional() private content: Content,
|
||||
constructor(injector: Injector, private surveyProvider: AddonModSurveyProvider, @Optional() content: Content,
|
||||
private surveyHelper: AddonModSurveyHelperProvider, private surveyOffline: AddonModSurveyOfflineProvider,
|
||||
private surveySync: AddonModSurveySyncProvider) {
|
||||
super(injector);
|
||||
super(injector, content);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,8 +83,6 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
*/
|
||||
protected isRefreshSyncNeeded(syncEventData: any): boolean {
|
||||
if (this.survey && syncEventData.surveyId == this.survey.id && syncEventData.userId == this.userId) {
|
||||
this.content.scrollToTop();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -189,9 +187,7 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
|
|||
}
|
||||
|
||||
return this.surveyProvider.submitAnswers(this.survey.id, this.survey.name, this.courseId, answers).then(() => {
|
||||
this.content.scrollToTop();
|
||||
|
||||
return this.refreshContent(false);
|
||||
return this.showLoadingAndRefresh(false);
|
||||
}).finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
|
|
|
@ -50,6 +50,7 @@ export class AddonModSurveyModuleHandler implements CoreCourseModuleHandler {
|
|||
icon: this.courseProvider.getModuleIconSrc('survey'),
|
||||
title: module.name,
|
||||
class: 'addon-mod_survey-handler',
|
||||
showDownloadButton: true,
|
||||
action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void {
|
||||
navCtrl.push('AddonModSurveyIndexPage', {module: module, courseId: courseId}, options);
|
||||
}
|
||||
|
|
|
@ -76,6 +76,7 @@ import { AddonCalendarModule } from '@addon/calendar/calendar.module';
|
|||
import { AddonCompetencyModule } from '@addon/competency/competency.module';
|
||||
import { AddonUserProfileFieldModule } from '@addon/userprofilefield/userprofilefield.module';
|
||||
import { AddonFilesModule } from '@addon/files/files.module';
|
||||
import { AddonModAssignModule } from '@addon/mod/assign/assign.module';
|
||||
import { AddonModBookModule } from '@addon/mod/book/book.module';
|
||||
import { AddonModChatModule } from '@addon/mod/chat/chat.module';
|
||||
import { AddonModChoiceModule } from '@addon/mod/choice/choice.module';
|
||||
|
@ -174,6 +175,7 @@ export const CORE_PROVIDERS: any[] = [
|
|||
AddonCompetencyModule,
|
||||
AddonUserProfileFieldModule,
|
||||
AddonFilesModule,
|
||||
AddonModAssignModule,
|
||||
AddonModBookModule,
|
||||
AddonModChatModule,
|
||||
AddonModChoiceModule,
|
||||
|
@ -210,7 +212,7 @@ export const CORE_PROVIDERS: any[] = [
|
|||
})
|
||||
export class AppModule {
|
||||
constructor(platform: Platform, initDelegate: CoreInitDelegate, updateManager: CoreUpdateManagerProvider,
|
||||
sitesProvider: CoreSitesProvider) {
|
||||
sitesProvider: CoreSitesProvider, fileProvider: CoreFileProvider) {
|
||||
// Register a handler for platform ready.
|
||||
initDelegate.registerProcess({
|
||||
name: 'CorePlatformReady',
|
||||
|
@ -230,6 +232,14 @@ export class AppModule {
|
|||
load: sitesProvider.restoreSession.bind(sitesProvider)
|
||||
});
|
||||
|
||||
// Register clear app tmp folder.
|
||||
initDelegate.registerProcess({
|
||||
name: 'CoreClearTmpFolder',
|
||||
priority: CoreInitDelegate.MAX_RECOMMENDED_PRIORITY + 150,
|
||||
blocking: false,
|
||||
load: fileProvider.clearTmpFolder.bind(fileProvider)
|
||||
});
|
||||
|
||||
// Execute the init processes.
|
||||
initDelegate.executeInitProcesses();
|
||||
}
|
||||
|
|
|
@ -81,6 +81,12 @@
|
|||
background-color: $gray-lighter;
|
||||
}
|
||||
|
||||
// Make no-lines work in any element, not just ion-item and ion-list.
|
||||
.item *[no-lines] .item-inner,
|
||||
*[no-lines] .item .item-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.core-oauth-icon, .item.core-oauth-icon, .list .item.core-oauth-icon {
|
||||
min-height: 32px;
|
||||
img, .label {
|
||||
|
@ -636,6 +642,11 @@ canvas[core-chart] {
|
|||
background-image: url("data:image/svg+xml;charset=utf-8,<svg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2012%2020'><path%20d='M2,20l-2-2l8-8L0,2l2-2l10,10L2,20z'%20fill='%23FFFFFF'/></svg>") !important;
|
||||
}
|
||||
|
||||
// For list where some items have detail icon and some others don't.
|
||||
.core-list-align-detail-right .item .item-inner {
|
||||
@include padding-horizontal(null, 32px);
|
||||
}
|
||||
|
||||
[ion-fixed] {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
<ion-item text-wrap>
|
||||
{{ 'core.maxsizeandattachments' | translate:{$a: {size: maxSizeReadable, attachments: maxSubmissionsReadable} } }}
|
||||
</ion-item>
|
||||
<ion-item text-wrap *ngIf="filetypes && filetypes.mimetypes && filetypes.mimetypes.length">
|
||||
<p>{{ 'core.fileuploader.filesofthesetypes' | translate }}</p>
|
||||
<ul class="list-with-style">
|
||||
<li *ngFor="let typeInfo of filetypes.info">
|
||||
<strong *ngIf="typeInfo.name">{{typeInfo.name}} </strong>{{typeInfo.extlist}}
|
||||
</li>
|
||||
</ul>
|
||||
</ion-item>
|
||||
<div *ngFor="let file of files; let index=index">
|
||||
<!-- Files already attached to the submission, either in online or in offline. -->
|
||||
<core-file *ngIf="!file.name || file.offline" [file]="file" [component]="component" [componentId]="componentId" [canDelete]="true" (onDelete)="delete(index, true)" [canDownload]="!file.offline"></core-file>
|
||||
|
||||
<!-- Files added to draft but not attached to submission yet. -->
|
||||
<core-local-file *ngIf="file.name && !file.offline" [file]="file" [manage]="true" (onDelete)="delete(index, false)" (onRename)="renamed(index, $event)"></core-local-file>
|
||||
</div>
|
||||
<!-- Button to add more files. -->
|
||||
<ion-item text-wrap *ngIf="unlimitedFiles || (maxSubmissions >= 0 && files && files.length < maxSubmissions)">
|
||||
<a ion-button block icon-start (click)="add()">
|
||||
<ion-icon name="add"></ion-icon>
|
||||
{{ 'core.fileuploader.addfiletext' | translate }}
|
||||
</a>
|
||||
</ion-item>
|
|
@ -0,0 +1,135 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, Input, OnInit } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
|
||||
import { CoreFileUploaderHelperProvider } from '@core/fileuploader/providers/helper';
|
||||
|
||||
/**
|
||||
* Component to render attachments, allow adding more and delete the current ones.
|
||||
*
|
||||
* All the changes done will be applied to the "files" input array, no file will be uploaded. The component using this
|
||||
* component should be the one uploading and moving the files.
|
||||
*
|
||||
* All the files added will be copied to the app temporary folder, so they should be deleted after uploading them
|
||||
* or if the user cancels the action.
|
||||
*
|
||||
* <core-attachments [files]="files" [maxSize]="configs.maxsubmissionsizebytes" [maxSubmissions]="configs.maxfilesubmissions"
|
||||
* [component]="component" [componentId]="assign.cmid" [acceptedTypes]="configs.filetypeslist" [allowOffline]="allowOffline">
|
||||
* </core-attachments>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-attachments',
|
||||
templateUrl: 'attachments.html'
|
||||
})
|
||||
export class CoreAttachmentsComponent implements OnInit {
|
||||
@Input() files: any[]; // List of attachments. New attachments will be added to this array.
|
||||
@Input() maxSize: number; // Max size for attachments. If not defined, 0 or -1, unknown size.
|
||||
@Input() maxSubmissions: number; // Max number of attachments. If -1 or not defined, unknown limit.
|
||||
@Input() component: string; // Component the downloaded files will be linked to.
|
||||
@Input() componentId: string | number; // Component ID.
|
||||
@Input() allowOffline: boolean | string; // Whether to allow selecting files in offline.
|
||||
@Input() acceptedTypes: string; // List of supported filetypes. If undefined, all types supported.
|
||||
|
||||
maxSizeReadable: string;
|
||||
maxSubmissionsReadable: string;
|
||||
unlimitedFiles: boolean;
|
||||
|
||||
protected fileTypes: { info: any[], mimetypes: string[] };
|
||||
|
||||
constructor(protected appProvider: CoreAppProvider, protected domUtils: CoreDomUtilsProvider,
|
||||
protected textUtils: CoreTextUtilsProvider, protected fileUploaderProvider: CoreFileUploaderProvider,
|
||||
protected translate: TranslateService, protected fileUploaderHelper: CoreFileUploaderHelperProvider) { }
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.maxSize = Number(this.maxSize); // Make sure it's defined and it's a number.
|
||||
this.maxSize = !isNaN(this.maxSize) && this.maxSize > 0 ? this.maxSize : -1;
|
||||
|
||||
if (this.maxSize == -1) {
|
||||
this.maxSizeReadable = this.translate.instant('core.unknown');
|
||||
} else {
|
||||
this.maxSizeReadable = this.textUtils.bytesToSize(this.maxSize, 2);
|
||||
}
|
||||
|
||||
if (typeof this.maxSubmissions == 'undefined' || this.maxSubmissions < 0) {
|
||||
this.maxSubmissionsReadable = this.translate.instant('core.unknown');
|
||||
this.unlimitedFiles = true;
|
||||
} else {
|
||||
this.maxSubmissionsReadable = String(this.maxSubmissions);
|
||||
}
|
||||
|
||||
if (this.acceptedTypes && this.acceptedTypes.trim()) {
|
||||
this.fileTypes = this.fileUploaderProvider.prepareFiletypeList(this.acceptedTypes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new attachment.
|
||||
*/
|
||||
add(): void {
|
||||
const allowOffline = this.allowOffline && this.allowOffline !== 'false';
|
||||
|
||||
if (!allowOffline && !this.appProvider.isOnline()) {
|
||||
this.domUtils.showErrorModal('core.fileuploader.errormustbeonlinetoupload', true);
|
||||
} else {
|
||||
const mimetypes = this.fileTypes && this.fileTypes.mimetypes;
|
||||
|
||||
this.fileUploaderHelper.selectFile(this.maxSize, allowOffline, undefined, mimetypes).then((result) => {
|
||||
this.files.push(result);
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'Error selecting file.');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file from the list.
|
||||
*
|
||||
* @param {number} index The index of the file.
|
||||
* @param {boolean} [askConfirm] Whether to ask confirm.
|
||||
*/
|
||||
delete(index: number, askConfirm?: boolean): void {
|
||||
let promise;
|
||||
|
||||
if (askConfirm) {
|
||||
promise = this.domUtils.showConfirm(this.translate.instant('core.confirmdeletefile'));
|
||||
} else {
|
||||
promise = Promise.resolve();
|
||||
}
|
||||
|
||||
promise.then(() => {
|
||||
// Remove the file from the list.
|
||||
this.files.splice(index, 1);
|
||||
}).catch(() => {
|
||||
// User cancelled.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A file was renamed.
|
||||
*
|
||||
* @param {number} index Index of the file.
|
||||
* @param {any} data The data received.
|
||||
*/
|
||||
renamed(index: number, data: any): void {
|
||||
this.files[index] = data.file;
|
||||
}
|
||||
}
|
|
@ -41,8 +41,10 @@ import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
|
|||
import { CoreDynamicComponent } from './dynamic-component/dynamic-component';
|
||||
import { CoreSendMessageFormComponent } from './send-message-form/send-message-form';
|
||||
import { CoreTimerComponent } from './timer/timer';
|
||||
import { CoreRecaptchaComponent, CoreRecaptchaModalComponent } from './recaptcha/recaptcha';
|
||||
import { CoreRecaptchaComponent } from './recaptcha/recaptcha';
|
||||
import { CoreRecaptchaModalComponent } from './recaptcha/recaptchamodal';
|
||||
import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar';
|
||||
import { CoreAttachmentsComponent } from './attachments/attachments';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -72,7 +74,8 @@ import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar';
|
|||
CoreTimerComponent,
|
||||
CoreRecaptchaComponent,
|
||||
CoreRecaptchaModalComponent,
|
||||
CoreNavigationBarComponent
|
||||
CoreNavigationBarComponent,
|
||||
CoreAttachmentsComponent
|
||||
],
|
||||
entryComponents: [
|
||||
CoreContextMenuPopoverComponent,
|
||||
|
@ -109,7 +112,8 @@ import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar';
|
|||
CoreSendMessageFormComponent,
|
||||
CoreTimerComponent,
|
||||
CoreRecaptchaComponent,
|
||||
CoreNavigationBarComponent
|
||||
CoreNavigationBarComponent,
|
||||
CoreAttachmentsComponent
|
||||
]
|
||||
})
|
||||
export class CoreComponentsModule {}
|
||||
|
|
|
@ -1,2 +1,11 @@
|
|||
core-file {
|
||||
.card-md core-file + core-file > .item-md.item-block > .item-inner {
|
||||
border-top: 1px solid $list-md-border-color;
|
||||
}
|
||||
|
||||
.card-ios core-file + core-file > .item-ios.item-block > .item-inner {
|
||||
border-top: $hairlines-width solid $list-ios-border-color;
|
||||
}
|
||||
|
||||
.card-wp core-file + core-file > .item-wp.item-block > .item-inner {
|
||||
border-top: 1px solid $list-wp-border-color;
|
||||
}
|
|
@ -39,7 +39,7 @@ export class CoreFileComponent implements OnInit, OnDestroy {
|
|||
@Input() alwaysDownload?: boolean | string; // Whether it should always display the refresh button when the file is downloaded.
|
||||
// Use it for files that you cannot determine if they're outdated or not.
|
||||
@Input() canDownload?: boolean | string = true; // Whether file can be downloaded.
|
||||
@Output() onDelete?: EventEmitter<string>; // Will notify when the delete button is clicked.
|
||||
@Output() onDelete?: EventEmitter<void>; // Will notify when the delete button is clicked.
|
||||
|
||||
isDownloaded: boolean;
|
||||
isDownloading: boolean;
|
||||
|
@ -178,7 +178,7 @@ export class CoreFileComponent implements OnInit, OnDestroy {
|
|||
*
|
||||
* @param {Event} e Click event.
|
||||
*/
|
||||
deleteFile(e: Event): void {
|
||||
delete(e: Event): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
|
|
@ -4,5 +4,7 @@
|
|||
<p class="core-loading-message" *ngIf="message">{{message}}</p>
|
||||
</span>
|
||||
</div>
|
||||
<ng-content [@coreShowHideAnimation] class="core-loading-content" *ngIf="hideUntil">
|
||||
</ng-content>
|
||||
<div #content>
|
||||
<ng-content [@coreShowHideAnimation] *ngIf="hideUntil">
|
||||
</ng-content>
|
||||
</div>
|
|
@ -1,4 +1,6 @@
|
|||
core-loading {
|
||||
@include core-transition(height, 200ms);
|
||||
|
||||
.core-loading-container {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
@ -7,34 +9,45 @@ core-loading {
|
|||
}
|
||||
|
||||
.core-loading-content {
|
||||
display: unset;
|
||||
padding-bottom: 1px; /* This makes height be real */
|
||||
}
|
||||
|
||||
&.core-loading-noheight .core-loading-content {
|
||||
height: auto;
|
||||
}
|
||||
@include core-transition(core-show-animation);
|
||||
}
|
||||
|
||||
.scroll-content > core-loading > .core-loading-container,
|
||||
ion-content > .scroll-content > core-loading > .core-loading-container,
|
||||
.core-loading-center .core-loading-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: table;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
clear: both;
|
||||
.scroll-content > core-loading,
|
||||
ion-content > .scroll-content > core-loading,
|
||||
.core-loading-center {
|
||||
position: unset !important;
|
||||
}
|
||||
|
||||
.core-loading-spinner {
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
.scroll-content > core-loading,
|
||||
ion-content > .scroll-content > core-loading,
|
||||
.core-loading-center,
|
||||
core-loading.core-loading-loaded {
|
||||
position: relative;
|
||||
|
||||
> .core-loading-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: table;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
clear: both;
|
||||
|
||||
.core-loading-spinner {
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Component, Input, OnInit, OnChanges, SimpleChange, ViewChild, ElementRef } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { coreShowHideAnimation } from '@classes/animations';
|
||||
|
||||
|
@ -41,11 +41,15 @@ import { coreShowHideAnimation } from '@classes/animations';
|
|||
templateUrl: 'loading.html',
|
||||
animations: [coreShowHideAnimation]
|
||||
})
|
||||
export class CoreLoadingComponent implements OnInit {
|
||||
export class CoreLoadingComponent implements OnInit, OnChanges {
|
||||
@Input() hideUntil: boolean; // Determine when should the contents be shown.
|
||||
@Input() message?: string; // Message to show while loading.
|
||||
@ViewChild('content') content: ElementRef;
|
||||
protected element: HTMLElement; // Current element.
|
||||
|
||||
constructor(private translate: TranslateService) { }
|
||||
constructor(private translate: TranslateService, element: ElementRef) {
|
||||
this.element = element.nativeElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
|
@ -57,4 +61,17 @@ export class CoreLoadingComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||
if (changes.hideUntil.currentValue === true) {
|
||||
setTimeout(() => {
|
||||
// Content is loaded so, center the spinner on the content itself.
|
||||
this.element.classList.add('core-loading-loaded');
|
||||
setTimeout(() => {
|
||||
// Change CSS to force calculate height.
|
||||
this.content.nativeElement.classList.add('core-loading-content');
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,29 +1,28 @@
|
|||
<a ion-item text-wrap class="item-media" (click)="fileClicked($event)" detail-none>
|
||||
<img [src]="fileIcon" alt="{{fileExtension}}" role="presentation" item-start />
|
||||
<form (ngSubmit)="changeName(newFileName, $event)">
|
||||
<a ion-item text-wrap stacked class="item-media" [class.item-input]="editMode" (click)="fileClicked($event)" detail-none>
|
||||
<img [src]="fileIcon" alt="{{fileExtension}}" role="presentation" item-start />
|
||||
|
||||
<!-- File name and edit button (if editable). -->
|
||||
<p *ngIf="!editMode" class="core-text-with-icon-right">
|
||||
{{fileName}}
|
||||
<a ion-button icon-only clear *ngIf="manage" (click)="activateEdit($event)" [attr.aria-label]="'core.edit' | translate">
|
||||
<ion-icon name="create" ios="md-create"></ion-icon>
|
||||
</a>
|
||||
</p>
|
||||
<!-- File name and edit button (if editable). -->
|
||||
<h2 *ngIf="!editMode">{{fileName}}</h2>
|
||||
<!-- More data about the file. -->
|
||||
<p *ngIf="size && !editMode">{{ size }}</p>
|
||||
<p *ngIf="timemodified && !editMode">{{ timemodified }}</p>
|
||||
|
||||
<!-- Form to edit the file's name. -->
|
||||
<form *ngIf="editMode" (ngSubmit)="changeName(newFileName)">
|
||||
<ion-input type="text" name="filename" [(ngModel)]="newFileName" [placeholder]="'core.filename' | translate" autocapitalize="none" autocorrect="off" (click)="$event.stopPropagation()" [core-auto-focus]></ion-input>
|
||||
<button type="submit" ion-button icon-only clear class="core-button-icon-small" [attr.aria-label]="'core.save' | translate">
|
||||
<ion-icon name="checkmark"></ion-icon>
|
||||
</button>
|
||||
</form>
|
||||
<!-- Form to edit the file's name. -->
|
||||
<ion-input type="text" name="filename" [placeholder]="'core.filename' | translate" autocapitalize="none" autocorrect="off" (click)="$event.stopPropagation()" [core-auto-focus] [(ngModel)]="newFileName" *ngIf="editMode"></ion-input>
|
||||
|
||||
<!-- More data about the file. -->
|
||||
<p *ngIf="size">{{ size }}</p>
|
||||
<p *ngIf="timemodified">{{ timemodified }}</p>
|
||||
<div class="buttons" item-end *ngIf="manage">
|
||||
<button *ngIf="!editMode" ion-button icon-only clear (click)="activateEdit($event)" [attr.aria-label]="'core.edit' | translate" color="dark">
|
||||
<ion-icon name="create" ios="md-create"></ion-icon>
|
||||
</button>
|
||||
|
||||
<div class="buttons" item-end *ngIf="manage">
|
||||
<button ion-button clear icon-only (click)="deleteFile($event)" [attr.aria-label]="'core.delete' | translate" color="danger">
|
||||
<ion-icon name="trash"></ion-icon>
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
<button *ngIf="editMode" ion-button icon-only clear [attr.aria-label]="'core.save' | translate" color="success" type="submit">
|
||||
<ion-icon name="checkmark"></ion-icon>
|
||||
</button>
|
||||
|
||||
<button ion-button clear icon-only (click)="deleteFile($event)" [attr.aria-label]="'core.delete' | translate" color="danger">
|
||||
<ion-icon name="trash"></ion-icon>
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
</form>
|
||||
|
|
|
@ -96,6 +96,10 @@ export class CoreLocalFileComponent implements OnInit {
|
|||
* @param {Event} e Click event.
|
||||
*/
|
||||
fileClicked(e: Event): void {
|
||||
if (this.editMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
@ -116,19 +120,18 @@ export class CoreLocalFileComponent implements OnInit {
|
|||
e.stopPropagation();
|
||||
this.editMode = true;
|
||||
this.newFileName = this.file.name;
|
||||
|
||||
// @todo For some reason core-auto-focus isn't working right. Focus the input manually.
|
||||
// $timeout(function() {
|
||||
// $mmUtil.focusElement(element[0].querySelector('input'));
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename the file.
|
||||
*
|
||||
* @param {string} newName New name.
|
||||
* @param {Event} e Click event.
|
||||
*/
|
||||
changeName(newName: string): void {
|
||||
changeName(newName: string, e: Event): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (newName == this.file.name) {
|
||||
// Name hasn't changed, stop.
|
||||
this.editMode = false;
|
||||
|
@ -151,8 +154,8 @@ export class CoreLocalFileComponent implements OnInit {
|
|||
this.file = fileEntry;
|
||||
this.loadFileBasicData();
|
||||
this.onRename.emit({ file: this.file });
|
||||
}).catch(() => {
|
||||
this.domUtils.showErrorModal('core.errorrenamefile', true);
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'core.errorrenamefile', true);
|
||||
});
|
||||
}).finally(() => {
|
||||
modal.dismiss();
|
||||
|
@ -177,8 +180,8 @@ export class CoreLocalFileComponent implements OnInit {
|
|||
}).finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
}).catch(() => {
|
||||
this.domUtils.showErrorModal('core.errordeletefile', true);
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'core.errordeletefile', true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,34 +13,29 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ModalController, ViewController, NavParams } from 'ionic-angular';
|
||||
import { ModalController } from 'ionic-angular';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreLangProvider } from '@providers/lang';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { CoreRecaptchaModalComponent } from './recaptchamodal';
|
||||
|
||||
/**
|
||||
* Directive to display a reCaptcha.
|
||||
*
|
||||
* Accepts the following attributes:
|
||||
* @param {any} model The model where to store the recaptcha response.
|
||||
* @param {string} publicKey The site public key.
|
||||
* @param {string} [modelValueName] Name of the model property where to store the response. Defaults to 'recaptcharesponse'.
|
||||
* @param {string} [siteUrl] The site URL. If not defined, current site.
|
||||
* Component that allows answering a recaptcha.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-recaptcha',
|
||||
templateUrl: 'recaptcha.html'
|
||||
})
|
||||
export class CoreRecaptchaComponent {
|
||||
@Input() model: any; // The model where to store the recaptcha response.
|
||||
@Input() publicKey: string; // The site public key.
|
||||
@Input() modelValueName = 'recaptcharesponse'; // Name of the model property where to store the response.
|
||||
@Input() siteUrl?: string; // The site URL. If not defined, current site.
|
||||
|
||||
expired = false;
|
||||
|
||||
protected lang: string;
|
||||
|
||||
@Input() model: any;
|
||||
@Input() publicKey: string;
|
||||
@Input() modelValueName = 'recaptcharesponse';
|
||||
@Input() siteUrl?: string;
|
||||
|
||||
constructor(private sitesProvider: CoreSitesProvider, langProvider: CoreLangProvider,
|
||||
private textUtils: CoreTextUtilsProvider, private modalCtrl: ModalController) {
|
||||
|
||||
|
@ -75,53 +70,3 @@ export class CoreRecaptchaComponent {
|
|||
modal.present();
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'core-recaptcha-modal',
|
||||
templateUrl: 'recaptchamodal.html'
|
||||
})
|
||||
export class CoreRecaptchaModalComponent {
|
||||
|
||||
expired = false;
|
||||
value = '';
|
||||
src: string;
|
||||
|
||||
constructor(protected viewCtrl: ViewController, params: NavParams) {
|
||||
this.src = params.get('src');
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal.
|
||||
*/
|
||||
closeModal(): void {
|
||||
this.viewCtrl.dismiss({
|
||||
expired: this.expired,
|
||||
value: this.value
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The iframe with the recaptcha was loaded.
|
||||
*
|
||||
* @param {HTMLIFrameElement} iframe Iframe element.
|
||||
*/
|
||||
loaded(iframe: HTMLIFrameElement): void {
|
||||
// Search the iframe content.
|
||||
const contentWindow = iframe && iframe.contentWindow;
|
||||
|
||||
if (contentWindow) {
|
||||
// Set the callbacks we're interested in.
|
||||
contentWindow['recaptchacallback'] = (value): void => {
|
||||
this.expired = false;
|
||||
this.value = value;
|
||||
this.closeModal();
|
||||
};
|
||||
|
||||
contentWindow['recaptchaexpiredcallback'] = (): void => {
|
||||
// Verification expired. Check the checkbox again.
|
||||
this.expired = true;
|
||||
this.value = '';
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 } from '@angular/core';
|
||||
import { ViewController, NavParams } from 'ionic-angular';
|
||||
|
||||
/**
|
||||
* Component to display a the recaptcha in a modal.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-recaptcha-modal',
|
||||
templateUrl: 'recaptchamodal.html'
|
||||
})
|
||||
export class CoreRecaptchaModalComponent {
|
||||
expired = false;
|
||||
value = '';
|
||||
src: string;
|
||||
|
||||
constructor(protected viewCtrl: ViewController, params: NavParams) {
|
||||
this.src = params.get('src');
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal.
|
||||
*/
|
||||
closeModal(): void {
|
||||
this.viewCtrl.dismiss({
|
||||
expired: this.expired,
|
||||
value: this.value
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The iframe with the recaptcha was loaded.
|
||||
*
|
||||
* @param {HTMLIFrameElement} iframe Iframe element.
|
||||
*/
|
||||
loaded(iframe: HTMLIFrameElement): void {
|
||||
// Search the iframe content.
|
||||
const contentWindow = iframe && iframe.contentWindow;
|
||||
|
||||
if (contentWindow) {
|
||||
// Set the callbacks we're interested in.
|
||||
contentWindow['recaptchacallback'] = (value): void => {
|
||||
this.expired = false;
|
||||
this.value = value;
|
||||
this.closeModal();
|
||||
};
|
||||
|
||||
contentWindow['recaptchaexpiredcallback'] = (): void => {
|
||||
// Verification expired. Check the checkbox again.
|
||||
this.expired = true;
|
||||
this.value = '';
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<core-loading [hideUntil]="hideUntil" class="core-loading-center">
|
||||
<div class="core-tabs-bar" #topTabs>
|
||||
<div class="core-tabs-bar" #topTabs [hidden]="!tabs || tabs.length < 2">
|
||||
<ng-container *ngFor="let tab of tabs; let idx = index">
|
||||
<a *ngIf="tab.show" [attr.aria-selected]="selected == idx" (click)="selectTab(idx)">
|
||||
<ion-icon *ngIf="tab.icon" [name]="tab.icon"></ion-icon>
|
||||
|
|
|
@ -25,6 +25,17 @@ core-tabs {
|
|||
|
||||
.core-tabs-content-container {
|
||||
height: 100%;
|
||||
|
||||
&.no-scroll {
|
||||
height: auto;
|
||||
padding-bottom: 0 !important;
|
||||
|
||||
.scroll-content {
|
||||
overflow: hidden !important;
|
||||
contain: initial;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tabs-hidden {
|
||||
|
|
|
@ -44,6 +44,7 @@ import { Content } from 'ionic-angular';
|
|||
export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges {
|
||||
@Input() selectedIndex = 0; // Index of the tab to select.
|
||||
@Input() hideUntil = true; // Determine when should the contents be shown.
|
||||
@Input() parentScrollable = false; // Determine if the scroll should be in the parent content or the tab itself.
|
||||
@Output() ionChange: EventEmitter<CoreTabComponent> = new EventEmitter<CoreTabComponent>(); // Emitted when the tab changes.
|
||||
@ViewChild('originalTabs') originalTabsRef: ElementRef;
|
||||
@ViewChild('topTabs') topTabs: ElementRef;
|
||||
|
@ -58,7 +59,6 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges {
|
|||
protected tabBarHeight;
|
||||
protected tabBarElement: HTMLElement; // Host element.
|
||||
protected tabsShown = true;
|
||||
protected scroll: HTMLElement; // Parent scroll element (if core-tabs is inside a ion-content).
|
||||
|
||||
constructor(element: ElementRef, protected content: Content) {
|
||||
this.tabBarElement = element.nativeElement;
|
||||
|
@ -164,9 +164,14 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges {
|
|||
this.tabBarHeight = this.topTabsElement.offsetHeight;
|
||||
this.originalTabsContainer.style.paddingBottom = this.tabBarHeight + 'px';
|
||||
if (this.content) {
|
||||
this.scroll = this.content.getScrollElement();
|
||||
if (this.scroll) {
|
||||
this.scroll.classList.add('no-scroll');
|
||||
if (!this.parentScrollable) {
|
||||
// Parent scroll element (if core-tabs is inside a ion-content).
|
||||
const scroll = this.content.getScrollElement();
|
||||
if (scroll) {
|
||||
scroll.classList.add('no-scroll');
|
||||
}
|
||||
} else {
|
||||
this.originalTabsContainer.classList.add('no-scroll');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Injector } from '@angular/core';
|
||||
import { Content } from 'ionic-angular';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
|
||||
|
@ -47,7 +48,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
|||
protected eventsProvider: CoreEventsProvider;
|
||||
protected modulePrefetchProvider: CoreCourseModulePrefetchDelegate;
|
||||
|
||||
constructor(injector: Injector) {
|
||||
constructor(injector: Injector, protected content?: Content) {
|
||||
super(injector);
|
||||
|
||||
this.sitesProvider = injector.get(CoreSitesProvider);
|
||||
|
@ -118,10 +119,8 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
|||
*/
|
||||
protected autoSyncEventReceived(syncEventData: any): void {
|
||||
if (this.isRefreshSyncNeeded(syncEventData)) {
|
||||
this.loaded = false;
|
||||
|
||||
// Refresh the data.
|
||||
this.refreshContent(false);
|
||||
this.showLoadingAndRefresh(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,6 +145,22 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show loading and perform the refresh content function.
|
||||
*
|
||||
* @param {boolean} [sync=false] If the refresh needs syncing.
|
||||
* @param {boolean} [showErrors=false] Wether to show errors to the user or hide them.
|
||||
* @return {Promise<any>} Resolved when done.
|
||||
*/
|
||||
protected showLoadingAndRefresh(sync: boolean = false, showErrors: boolean = false): Promise<any> {
|
||||
this.refreshIcon = 'spinner';
|
||||
this.syncIcon = 'spinner';
|
||||
this.loaded = false;
|
||||
this.content && this.content.scrollToTop();
|
||||
|
||||
return this.refreshContent(true, showErrors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the component contents.
|
||||
*
|
||||
|
|
|
@ -128,12 +128,12 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
|
|||
this.spinner = true;
|
||||
|
||||
// Get download size to ask for confirm if it's high.
|
||||
this.prefetchHandler.getDownloadSize(module, this.courseId).then((size) => {
|
||||
this.prefetchHandler.getDownloadSize(this.module, this.courseId).then((size) => {
|
||||
return this.courseHelper.prefetchModule(this.prefetchHandler, this.module, size, this.courseId, refresh);
|
||||
}).catch((error) => {
|
||||
// Error, hide spinner.
|
||||
this.spinner = false;
|
||||
if (!this.isDestroyed && error) {
|
||||
if (!this.isDestroyed) {
|
||||
this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -74,6 +74,8 @@ export class FileMock extends File {
|
|||
*/
|
||||
private copyMock(srce: Entry, destDir: DirectoryEntry, newName: string): Promise<Entry> {
|
||||
return new Promise<Entry>((resolve, reject): void => {
|
||||
newName = newName.replace(/%20/g, ' '); // Replace all %20 with spaces.
|
||||
|
||||
srce.copyTo(destDir, newName, (deste) => {
|
||||
resolve(deste);
|
||||
}, (err) => {
|
||||
|
@ -212,6 +214,8 @@ export class FileMock extends File {
|
|||
getDirectory(directoryEntry: DirectoryEntry, directoryName: string, flags: Flags): Promise<DirectoryEntry> {
|
||||
return new Promise<DirectoryEntry>((resolve, reject): void => {
|
||||
try {
|
||||
directoryName = directoryName.replace(/%20/g, ' '); // Replace all %20 with spaces.
|
||||
|
||||
directoryEntry.getDirectory(directoryName, flags, (de) => {
|
||||
resolve(de);
|
||||
}, (err) => {
|
||||
|
@ -235,6 +239,8 @@ export class FileMock extends File {
|
|||
getFile(directoryEntry: DirectoryEntry, fileName: string, flags: Flags): Promise<FileEntry> {
|
||||
return new Promise<FileEntry>((resolve, reject): void => {
|
||||
try {
|
||||
fileName = fileName.replace(/%20/g, ' '); // Replace all %20 with spaces.
|
||||
|
||||
directoryEntry.getFile(fileName, flags, resolve, (err) => {
|
||||
this.fillErrorMessageMock(err);
|
||||
reject(err);
|
||||
|
@ -375,6 +381,8 @@ export class FileMock extends File {
|
|||
*/
|
||||
private moveMock(srce: Entry, destDir: DirectoryEntry, newName: string): Promise<Entry> {
|
||||
return new Promise<Entry>((resolve, reject): void => {
|
||||
newName = newName.replace(/%20/g, ' '); // Replace all %20 with spaces.
|
||||
|
||||
srce.moveTo(destDir, newName, (deste) => {
|
||||
resolve(deste);
|
||||
}, (err) => {
|
||||
|
|
|
@ -487,7 +487,7 @@ export class CoreFileUploaderProvider {
|
|||
* @return {Promise<number>} Promise resolved with the itemId.
|
||||
*/
|
||||
uploadOrReuploadFiles(files: any[], component?: string, componentId?: string | number, siteId?: string): Promise<number> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
if (!files || !files.length) {
|
||||
// Return fake draft ID.
|
||||
|
|
|
@ -198,13 +198,9 @@ export class CoreFileUploaderHelperProvider {
|
|||
*/
|
||||
filePickerClosed(): void {
|
||||
if (this.filePickerDeferred) {
|
||||
this.filePickerDeferred.reject();
|
||||
this.filePickerDeferred.reject(this.domUtils.createCanceledError());
|
||||
this.filePickerDeferred = undefined;
|
||||
}
|
||||
// Close the action sheet if it's opened.
|
||||
if (this.actionSheet) {
|
||||
this.actionSheet.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue