Merge pull request #1298 from dpalou/MOBILE-2334

Mobile 2334
main
Juan Leyva 2018-04-30 14:08:41 +02:00 committed by GitHub
commit f56861aa17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
114 changed files with 10091 additions and 181 deletions

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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 {}

View File

@ -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>

View File

@ -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', []));
}
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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', []));
}
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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);
});
}
});
}
}

View File

@ -0,0 +1,3 @@
{
"pluginname": "Feedback comments"
}

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,3 @@
{
"pluginname": "Annotate PDF"
}

View File

@ -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;
}
}

View File

@ -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 { }

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,3 @@
{
"pluginname": "File feedback"
}

View File

@ -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;
}
}

View File

@ -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"
}

View File

@ -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>

View File

@ -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 {}

View File

@ -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;
});
}
}

View File

@ -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>

View File

@ -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 {}

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -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 {}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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 {}

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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 {}

View File

@ -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();
}
});
}
}
}

View File

@ -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);
});
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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]));
}
}

View File

@ -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);
}
}
}

View File

@ -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');
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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]));
}
}

View File

@ -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.
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -0,0 +1,3 @@
{
"pluginname": "Submission comments"
}

View File

@ -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)
});
}
}

View File

@ -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>

View File

@ -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);
});
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,3 @@
{
"pluginname": "File submissions"
}

View File

@ -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;
});
});
}
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -0,0 +1,3 @@
{
"pluginname": "Online text submissions"
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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 { }

View File

@ -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);
}
/**

View File

@ -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) => {

View File

@ -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);
}
/**

View File

@ -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();
});

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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%;
}

View File

@ -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>

View File

@ -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;
}
}

View 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 {}

View File

@ -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;
}

View File

@ -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();

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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);
});
}
}
}

View File

@ -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>

View File

@ -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);
});
}
}

View File

@ -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 = '';
};
}
}
}

View File

@ -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 = '';
};
}
}
}

View File

@ -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>

View File

@ -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 {

View File

@ -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');
}
}

View File

@ -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.
*

View File

@ -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);
}
});

View File

@ -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) => {

View File

@ -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.

View File

@ -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