diff --git a/src/addon/mod/assign/pages/edit/edit.html b/src/addon/mod/assign/pages/edit/edit.html new file mode 100644 index 000000000..58dbf25aa --- /dev/null +++ b/src/addon/mod/assign/pages/edit/edit.html @@ -0,0 +1,27 @@ + + + + + + + + + + + + + +
+ + + + + + + +
+
+
+
diff --git a/src/addon/mod/assign/pages/edit/edit.module.ts b/src/addon/mod/assign/pages/edit/edit.module.ts new file mode 100644 index 000000000..8b02b1c96 --- /dev/null +++ b/src/addon/mod/assign/pages/edit/edit.module.ts @@ -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 {} diff --git a/src/addon/mod/assign/pages/edit/edit.ts b/src/addon/mod/assign/pages/edit/edit.ts new file mode 100644 index 000000000..9fcfc67cc --- /dev/null +++ b/src/addon/mod/assign/pages/edit/edit.ts @@ -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} Resolved if we can leave it, rejected if not. + */ + ionViewCanLeave(): boolean | Promise { + 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} Promise resolved when done. + */ + protected fetchAssignment(): Promise { + 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} Promise resolved with boolean: whether data has changed. + */ + protected hasDataChanged(): Promise { + // 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} Promise resolved with the data to submit. + */ + protected prepareSubmissionData(inputData: any): Promise { + // 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} Promise resolved when done. + */ + protected saveSubmission(): Promise { + 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); + } + } +} diff --git a/src/core/fileuploader/providers/helper.ts b/src/core/fileuploader/providers/helper.ts index 3518c3b54..168fee217 100644 --- a/src/core/fileuploader/providers/helper.ts +++ b/src/core/fileuploader/providers/helper.ts @@ -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(); - } } /** diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts index eb398387c..673606dad 100644 --- a/src/providers/filepool.ts +++ b/src/providers/filepool.ts @@ -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. * @@ -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. filename = this.guessFilenameFromUrl(url); - return filename + '_' + Md5.hashAsciiStr('url:' + url); + return this.addHashToFilename(url, filename); } /** diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index 494e2bd6f..e52e75121 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -296,7 +296,7 @@ export class CoreDomUtilsProvider { // Ignore submit inputs. if (!name || element.type == 'submit' || element.tagName == 'BUTTON') { - return; + continue; } // Get the value.