MOBILE-2334 assign: Implement edit page

main
Dani Palou 2018-04-16 13:04:41 +02:00
parent 9d69c0f623
commit 847a9da41b
6 changed files with 426 additions and 7 deletions

View File

@ -0,0 +1,27 @@
<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"></ion-checkbox>
</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,335 @@
// (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.
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);
if (this.assignHelper.canEditSubmissionOffline(this.assign, userSubmission)) {
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) {
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

@ -198,13 +198,9 @@ export class CoreFileUploaderHelperProvider {
*/ */
filePickerClosed(): void { filePickerClosed(): void {
if (this.filePickerDeferred) { if (this.filePickerDeferred) {
this.filePickerDeferred.reject(); this.filePickerDeferred.reject(this.domUtils.createCanceledError());
this.filePickerDeferred = undefined; this.filePickerDeferred = undefined;
} }
// Close the action sheet if it's opened.
if (this.actionSheet) {
this.actionSheet.dismiss();
}
} }
/** /**

View File

@ -548,6 +548,32 @@ export class CoreFilepoolProvider {
}); });
} }
/**
* Adds a hash to a filename if needed.
*
* @param {string} url The URL of the file, already treated (decoded, without revision, etc.).
* @param {string} filename The filename.
* @return {string} The filename with the hash.
*/
protected addHashToFilename(url: string, filename: string): string {
// Check if the file already has a hash. If a file is downloaded and re-uploaded with the app it will have a hash already.
const matches = filename.match(/_[a-f0-9]{32}/g);
if (matches && matches.length) {
// There is at least 1 match. Get the last one.
const hash = matches[matches.length - 1],
treatedUrl = url.replace(hash, ''); // Remove the hash from the URL.
// Check that the hash is valid.
if ('_' + Md5.hashAsciiStr('url:' + treatedUrl) == hash) {
// The data found is a hash of the URL, don't need to add it again.
return filename;
}
}
return filename + '_' + Md5.hashAsciiStr('url:' + url);
}
/** /**
* Add a file to the queue. * Add a file to the queue.
* *
@ -1414,7 +1440,7 @@ export class CoreFilepoolProvider {
// We want to keep the original file name so people can easily identify the files after the download. // We want to keep the original file name so people can easily identify the files after the download.
filename = this.guessFilenameFromUrl(url); filename = this.guessFilenameFromUrl(url);
return filename + '_' + Md5.hashAsciiStr('url:' + url); return this.addHashToFilename(url, filename);
} }
/** /**

View File

@ -296,7 +296,7 @@ export class CoreDomUtilsProvider {
// Ignore submit inputs. // Ignore submit inputs.
if (!name || element.type == 'submit' || element.tagName == 'BUTTON') { if (!name || element.type == 'submit' || element.tagName == 'BUTTON') {
return; continue;
} }
// Get the value. // Get the value.