// (C) Copyright 2015 Moodle Pty Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin, AddonModAssignProvider, AddonModAssign, } from '@addons/mod/assign/services/assign'; import { AddonModAssignHelper } from '@addons/mod/assign/services/assign-helper'; import { AddonModAssignOffline, AddonModAssignSubmissionsDBRecordFormatted } from '@addons/mod/assign/services/assign-offline'; import { AddonModAssignSubmissionHandler } from '@addons/mod/assign/services/submission-delegate'; import { Injectable, Type } from '@angular/core'; import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; import { CoreFileEntry, CoreFileHelper } from '@services/file-helper'; import { CoreFileSession } from '@services/file-session'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonModAssignSubmissionFileComponent } from '../component/file'; import { FileEntry } from '@ionic-native/file/ngx'; /** * Handler for file submission plugin. */ @Injectable( { providedIn: 'root' }) export class AddonModAssignSubmissionFileHandlerService implements AddonModAssignSubmissionHandler { static readonly FOLDER_NAME = 'submission_file'; name = 'AddonModAssignSubmissionFileHandler'; type = 'file'; /** * Whether the plugin can be edited in offline for existing submissions. In general, this should return false if the * plugin uses Moodle filters. The reason is that the app only prefetches filtered data, and the user should edit * unfiltered data. * * @return Boolean or promise resolved with boolean: whether it can be edited in offline. */ canEditOffline(): boolean { // This plugin doesn't use Moodle filters, it can be edited in offline. return true; } /** * Check if a plugin has no data. * * @param assign The assignment. * @param plugin The plugin object. * @return Whether the plugin is empty. */ isEmpty(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): boolean { const files = AddonModAssign.getSubmissionPluginAttachments(plugin); return files.length === 0; } /** * Should clear temporary data for a cancelled submission. * * @param assign The assignment. */ clearTmpData(assign: AddonModAssignAssign): void { const files = CoreFileSession.getFiles(AddonModAssignProvider.COMPONENT, assign.id); // Clear the files in session for this assign. CoreFileSession.clearFiles(AddonModAssignProvider.COMPONENT, assign.id); // Now delete the local files from the tmp folder. CoreFileUploader.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 assign The assignment. * @param plugin The plugin object. * @param pluginData Object where to store the data to send. * @return If the function is async, it should return a Promise resolved when done. */ async copySubmissionData( assign: AddonModAssignAssign, plugin: AddonModAssignPlugin, pluginData: AddonModAssignSubmissionFilePluginData, ): Promise { // We need to re-upload all the existing files. const files = AddonModAssign.getSubmissionPluginAttachments(plugin); // Get the itemId. pluginData.files_filemanager = await AddonModAssignHelper.uploadFiles(assign.id, files); } /** * Return the Component to use to display the plugin data, either in read or in edit mode. * It's recommended to return the class of the component, but you can also return an instance of the component. * * @return The component (or promise resolved with component) to use, undefined if not found. */ getComponent(): Type { return AddonModAssignSubmissionFileComponent; } /** * Delete any stored data for the plugin and submission. * * @param assign The assignment. * @param submission The submission. * @param plugin The plugin object. * @param offlineData Offline data stored. * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ async deleteOfflineData( assign: AddonModAssignAssign, submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, offlineData: AddonModAssignSubmissionsDBRecordFormatted, siteId?: string, ): Promise { await CoreUtils.ignoreErrors( AddonModAssignHelper.deleteStoredSubmissionFiles( assign.id, AddonModAssignSubmissionFileHandlerService.FOLDER_NAME, submission.userid, siteId, ), ); } /** * Get files used by this plugin. * The files returned by this function will be prefetched when the user prefetches the assign. * * @param assign The assignment. * @param submission The submission. * @param plugin The plugin object. * @param siteId Site ID. If not defined, current site. * @return The files (or promise resolved with the files). */ getPluginFiles( assign: AddonModAssignAssign, submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, ): CoreWSFile[] { return AddonModAssign.getSubmissionPluginAttachments(plugin); } /** * Get the size of data (in bytes) this plugin will send to copy a previous submission. * * @param assign The assignment. * @param plugin The plugin object. * @return The size (or promise resolved with size). */ async getSizeForCopy(assign: AddonModAssignAssign, plugin: AddonModAssignPlugin): Promise { const files = AddonModAssign.getSubmissionPluginAttachments(plugin); return CoreFileHelper.getTotalFilesSize(files); } /** * Get the size of data (in bytes) this plugin will send to add or edit a submission. * * @param assign The assignment. * @param submission The submission. * @param plugin The plugin object. * @return The size (or promise resolved with size). */ async getSizeForEdit( assign: AddonModAssignAssign, submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, ): Promise { // Check if there's any change. if (this.hasDataChanged(assign, submission, plugin)) { const files = CoreFileSession.getFiles(AddonModAssignProvider.COMPONENT, assign.id); return CoreFileHelper.getTotalFilesSize(files); } else { // Nothing has changed, we won't upload any file. return 0; } } /** * Check if the submission data has changed for this plugin. * * @param assign The assignment. * @param submission The submission. * @param plugin The plugin object. * @return Boolean (or promise resolved with boolean): whether the data has changed. */ async hasDataChanged( assign: AddonModAssignAssign, submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, ): Promise { const offlineData = await CoreUtils.ignoreErrors( // Check if there's any offline data. AddonModAssignOffline.getSubmission(assign.id, submission.userid), undefined, ); let numFiles: number; if (offlineData && offlineData.plugindata && offlineData.plugindata.files_filemanager) { const offlineDataFiles = offlineData.plugindata.files_filemanager; // Has offline data, return the number of files. numFiles = offlineDataFiles.offline + offlineDataFiles.online.length; } else { // No offline data, return the number of online files. const pluginFiles = AddonModAssign.getSubmissionPluginAttachments(plugin); numFiles = pluginFiles && pluginFiles.length; } const currentFiles = CoreFileSession.getFiles(AddonModAssignProvider.COMPONENT, assign.id); if (currentFiles.length != numFiles) { // Number of files has changed. return true; } const files = await this.getSubmissionFilesToSync(assign, submission, offlineData); // Check if there is any local file added and list has changed. return CoreFileUploader.areFileListDifferent(currentFiles, files); } /** * Whether or not the handler is enabled on a site level. * * @return True or promise resolved with true if enabled. */ async isEnabled(): Promise { return true; } /** * Whether or not the handler is enabled for edit on a site level. * * @return Whether or not the handler is enabled for edit on a site level. */ isEnabledForEdit(): boolean { return true; } /** * Prepare and add to pluginData the data to send to the server based on the input data. * * @param assign The assignment. * @param submission The submission. * @param plugin The plugin object. * @param inputData Data entered by the user for the submission. * @param pluginData Object where to store the data to send. * @param offline Whether the user is editing in offline. * @param userId User ID. If not defined, site's current user. * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ async prepareSubmissionData( assign: AddonModAssignAssign, submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, inputData: AddonModAssignSubmissionFileData, pluginData: AddonModAssignSubmissionFilePluginData, offline = false, userId?: number, siteId?: string, ): Promise { const changed = await this.hasDataChanged(assign, submission, plugin); if (!changed) { return; } // Data has changed, we need to upload new files and re-upload all the existing files. const currentFiles = CoreFileSession.getFiles(AddonModAssignProvider.COMPONENT, assign.id); const error = CoreUtils.hasRepeatedFilenames(currentFiles); if (error) { throw error; } pluginData.files_filemanager = await AddonModAssignHelper.uploadOrStoreFiles( assign.id, AddonModAssignSubmissionFileHandlerService.FOLDER_NAME, currentFiles, offline, userId, siteId, ); } /** * Prepare and add to pluginData the data to send to the server based on the offline data stored. * This will be used when performing a synchronization. * * @param assign The assignment. * @param submission The submission. * @param plugin The plugin object. * @param offlineData Offline data stored. * @param pluginData Object where to store the data to send. * @param siteId Site ID. If not defined, current site. * @return If the function is async, it should return a Promise resolved when done. */ async prepareSyncData( assign: AddonModAssignAssign, submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, offlineData: AddonModAssignSubmissionsDBRecordFormatted, pluginData: AddonModAssignSubmissionFilePluginData, siteId?: string, ): Promise { const files = await this.getSubmissionFilesToSync(assign, submission, offlineData, siteId); if (files.length == 0) { return; } pluginData.files_filemanager = await AddonModAssignHelper.uploadFiles(assign.id, files, siteId); } /** * Get the file list to be synced. * * @param assign The assignment. * @param submission The submission. * @param offlineData Offline data stored. * @param siteId Site ID. If not defined, current site. * @return File entries when is all resolved. */ protected async getSubmissionFilesToSync( assign: AddonModAssignAssign, submission: AddonModAssignSubmission, offlineData?: AddonModAssignSubmissionsDBRecordFormatted, siteId?: string, ): Promise { const filesData = offlineData?.plugindata.files_filemanager; if (!filesData) { return []; } // Has some data to sync. let files: CoreFileEntry[] = filesData.online || []; if (filesData.offline) { // Has offline files, get them and add them to the list. const storedFiles = await CoreUtils.ignoreErrors( AddonModAssignHelper.getStoredSubmissionFiles( assign.id, AddonModAssignSubmissionFileHandlerService.FOLDER_NAME, submission.userid, siteId, ), [], ); files = files.concat(storedFiles); } return files; } } export const AddonModAssignSubmissionFileHandler = makeSingleton(AddonModAssignSubmissionFileHandlerService); // Define if ever used. export type AddonModAssignSubmissionFileData = Record; export type AddonModAssignSubmissionFilePluginData = { // The id of a draft area containing files for this submission. Or the offline file results. files_filemanager: number | CoreFileUploaderStoreFilesResult; // eslint-disable-line @typescript-eslint/naming-convention };