diff --git a/src/addons/blog/services/handlers/course-option.ts b/src/addons/blog/services/handlers/course-option.ts index 7b6f241e4..c15f4b4de 100644 --- a/src/addons/blog/services/handlers/course-option.ts +++ b/src/addons/blog/services/handlers/course-option.ts @@ -24,7 +24,7 @@ import { CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/service import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper'; import { CoreFilepool } from '@services/filepool'; import { CoreSites } from '@services/sites'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonBlog } from '../blog'; import { AddonBlogMainMenuHandlerService } from './mainmenu'; @@ -89,7 +89,7 @@ export class AddonBlogCourseOptionHandlerService implements CoreCourseOptionsHan const result = await AddonBlog.getEntries({ courseid: course.id }); await Promise.all(result.entries.map(async (entry) => { - let files: CoreWSExternalFile[] = []; + let files: CoreWSFile[] = []; if (entry.attachmentfiles && entry.attachmentfiles.length) { files = entry.attachmentfiles; diff --git a/src/addons/mod/assign/components/feedback-plugin/feedback-plugin.ts b/src/addons/mod/assign/components/feedback-plugin/feedback-plugin.ts index 39eae9e4f..ed4f46182 100644 --- a/src/addons/mod/assign/components/feedback-plugin/feedback-plugin.ts +++ b/src/addons/mod/assign/components/feedback-plugin/feedback-plugin.ts @@ -15,7 +15,7 @@ import { Component, Input, OnInit, ViewChild, Type } from '@angular/core'; import { CoreError } from '@classes/errors/error'; import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { ModalController } from '@singletons'; import { AddonModAssignFeedbackCommentsTextData } from '../../feedback/comments/services/handler'; import { @@ -53,7 +53,7 @@ export class AddonModAssignFeedbackPluginComponent implements OnInit { // Data to render the plugin if it isn't supported. component = AddonModAssignProvider.COMPONENT; text = ''; - files: CoreWSExternalFile[] = []; + files: CoreWSFile[] = []; notSupported = false; pluginLoaded = false; diff --git a/src/addons/mod/assign/components/submission-plugin/submission-plugin.ts b/src/addons/mod/assign/components/submission-plugin/submission-plugin.ts index c54e39cdb..7fda5c4ba 100644 --- a/src/addons/mod/assign/components/submission-plugin/submission-plugin.ts +++ b/src/addons/mod/assign/components/submission-plugin/submission-plugin.ts @@ -14,7 +14,6 @@ import { Component, Input, OnInit, Type, ViewChild } from '@angular/core'; import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; -import { CoreWSExternalFile } from '@services/ws'; import { AddonModAssignAssign, AddonModAssignSubmission, @@ -24,7 +23,7 @@ import { } from '../../services/assign'; import { AddonModAssignHelper, AddonModAssignPluginConfig } from '../../services/assign-helper'; import { AddonModAssignSubmissionDelegate } from '../../services/submission-delegate'; -import { FileEntry } from '@ionic-native/file/ngx'; +import { CoreFileEntry } from '@services/file-helper'; /** * Component that displays an assignment submission plugin. @@ -49,7 +48,7 @@ export class AddonModAssignSubmissionPluginComponent implements OnInit { // Data to render the plugin if it isn't supported. component = AddonModAssignProvider.COMPONENT; text = ''; - files: (FileEntry | CoreWSExternalFile)[] = []; + files: CoreFileEntry[] = []; notSupported = false; pluginLoaded = false; diff --git a/src/addons/mod/assign/feedback/comments/services/handler.ts b/src/addons/mod/assign/feedback/comments/services/handler.ts index c952f961c..0e0a3b4ce 100644 --- a/src/addons/mod/assign/feedback/comments/services/handler.ts +++ b/src/addons/mod/assign/feedback/comments/services/handler.ts @@ -25,7 +25,7 @@ import { Injectable, Type } from '@angular/core'; import { CoreSites } from '@services/sites'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonModAssignFeedbackCommentsComponent } from '../component/comments'; @@ -126,7 +126,7 @@ export class AddonModAssignFeedbackCommentsHandlerService implements AddonModAss assign: AddonModAssignAssign, submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, - ): CoreWSExternalFile[] { + ): CoreWSFile[] { return AddonModAssign.getSubmissionPluginAttachments(plugin); } diff --git a/src/addons/mod/assign/feedback/editpdf/component/editpdf.ts b/src/addons/mod/assign/feedback/editpdf/component/editpdf.ts index df1ff86ea..733aa8d47 100644 --- a/src/addons/mod/assign/feedback/editpdf/component/editpdf.ts +++ b/src/addons/mod/assign/feedback/editpdf/component/editpdf.ts @@ -15,7 +15,7 @@ import { AddonModAssignFeedbackPluginComponent } from '@addons/mod/assign/components/feedback-plugin/feedback-plugin'; import { AddonModAssignProvider, AddonModAssign } from '@addons/mod/assign/services/assign'; import { Component, OnInit } from '@angular/core'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; /** * Component to render a edit pdf feedback plugin. @@ -27,7 +27,7 @@ import { CoreWSExternalFile } from '@services/ws'; export class AddonModAssignFeedbackEditPdfComponent extends AddonModAssignFeedbackPluginComponent implements OnInit { component = AddonModAssignProvider.COMPONENT; - files: CoreWSExternalFile[] = []; + files: CoreWSFile[] = []; /** * Component being initialized. diff --git a/src/addons/mod/assign/feedback/editpdf/services/handler.ts b/src/addons/mod/assign/feedback/editpdf/services/handler.ts index dc9cb02d7..d3fac5247 100644 --- a/src/addons/mod/assign/feedback/editpdf/services/handler.ts +++ b/src/addons/mod/assign/feedback/editpdf/services/handler.ts @@ -20,7 +20,7 @@ import { } from '@addons/mod/assign/services/assign'; import { AddonModAssignFeedbackHandler } from '@addons/mod/assign/services/feedback-delegate'; import { Injectable, Type } from '@angular/core'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonModAssignFeedbackEditPdfComponent } from '../component/editpdf'; @@ -56,7 +56,7 @@ export class AddonModAssignFeedbackEditPdfHandlerService implements AddonModAssi assign: AddonModAssignAssign, submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, - ): CoreWSExternalFile[] { + ): CoreWSFile[] { return AddonModAssign.getSubmissionPluginAttachments(plugin); } diff --git a/src/addons/mod/assign/feedback/file/component/file.ts b/src/addons/mod/assign/feedback/file/component/file.ts index 64d2af54f..c407baf9d 100644 --- a/src/addons/mod/assign/feedback/file/component/file.ts +++ b/src/addons/mod/assign/feedback/file/component/file.ts @@ -15,7 +15,7 @@ import { AddonModAssignFeedbackPluginComponent } from '@addons/mod/assign/components/feedback-plugin/feedback-plugin'; import { AddonModAssign, AddonModAssignProvider } from '@addons/mod/assign/services/assign'; import { Component, OnInit } from '@angular/core'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; /** * Component to render a file feedback plugin. @@ -27,7 +27,7 @@ import { CoreWSExternalFile } from '@services/ws'; export class AddonModAssignFeedbackFileComponent extends AddonModAssignFeedbackPluginComponent implements OnInit { component = AddonModAssignProvider.COMPONENT; - files: CoreWSExternalFile[] = []; + files: CoreWSFile[] = []; /** * Component being initialized. diff --git a/src/addons/mod/assign/feedback/file/services/handler.ts b/src/addons/mod/assign/feedback/file/services/handler.ts index d8854bb13..d77d7991c 100644 --- a/src/addons/mod/assign/feedback/file/services/handler.ts +++ b/src/addons/mod/assign/feedback/file/services/handler.ts @@ -20,7 +20,7 @@ import { } from '@addons/mod/assign/services/assign'; import { AddonModAssignFeedbackHandler } from '@addons/mod/assign/services/feedback-delegate'; import { Injectable, Type } from '@angular/core'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonModAssignFeedbackFileComponent } from '../component/file'; @@ -56,7 +56,7 @@ export class AddonModAssignFeedbackFileHandlerService implements AddonModAssignF assign: AddonModAssignAssign, submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, - ): CoreWSExternalFile[] { + ): CoreWSFile[] { return AddonModAssign.getSubmissionPluginAttachments(plugin); } diff --git a/src/addons/mod/assign/services/assign-helper.ts b/src/addons/mod/assign/services/assign-helper.ts index b4bbb4eb7..2da29183c 100644 --- a/src/addons/mod/assign/services/assign-helper.ts +++ b/src/addons/mod/assign/services/assign-helper.ts @@ -15,7 +15,6 @@ import { Injectable } from '@angular/core'; import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; -import { CoreWSExternalFile } from '@services/ws'; import { FileEntry } from '@ionic-native/file/ngx'; import { AddonModAssignProvider, @@ -36,6 +35,7 @@ import { AddonModAssignSubmissionDelegate } from './submission-delegate'; import { AddonModAssignFeedbackDelegate } from './feedback-delegate'; import { makeSingleton } from '@singletons'; import { CoreFormFields } from '@singletons/form'; +import { CoreFileEntry } from '@services/file-helper'; /** * Service that provides some helper functions for assign. @@ -642,7 +642,7 @@ export class AddonModAssignHelperProvider { async storeSubmissionFiles( assignId: number, folderName: string, - files: (CoreWSExternalFile | FileEntry)[], + files: CoreFileEntry[], userId?: number, siteId?: string, ): Promise { @@ -661,7 +661,7 @@ export class AddonModAssignHelperProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the itemId. */ - uploadFile(assignId: number, file: CoreWSExternalFile | FileEntry, itemId?: number, siteId?: string): Promise { + uploadFile(assignId: number, file: CoreFileEntry, itemId?: number, siteId?: string): Promise { return CoreFileUploader.uploadOrReuploadFile(file, itemId, AddonModAssignProvider.COMPONENT, assignId, siteId); } @@ -675,7 +675,7 @@ export class AddonModAssignHelperProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the itemId. */ - uploadFiles(assignId: number, files: (CoreWSExternalFile | FileEntry)[], siteId?: string): Promise { + uploadFiles(assignId: number, files: CoreFileEntry[], siteId?: string): Promise { return CoreFileUploader.uploadOrReuploadFiles(files, AddonModAssignProvider.COMPONENT, assignId, siteId); } @@ -693,7 +693,7 @@ export class AddonModAssignHelperProvider { async uploadOrStoreFiles( assignId: number, folderName: string, - files: (CoreWSExternalFile | FileEntry)[], + files: CoreFileEntry[], offline = false, userId?: number, siteId?: string, diff --git a/src/addons/mod/assign/services/assign-sync.ts b/src/addons/mod/assign/services/assign-sync.ts index 2ea3fd466..60cad1218 100644 --- a/src/addons/mod/assign/services/assign-sync.ts +++ b/src/addons/mod/assign/services/assign-sync.ts @@ -53,7 +53,7 @@ export class AddonModAssignSyncProvider extends CoreCourseActivitySyncBaseProvid protected componentTranslatableString = 'assign'; constructor() { - super('AddonModLessonSyncProvider'); + super('AddonModAssignSyncProvider'); } /** diff --git a/src/addons/mod/assign/services/assign.ts b/src/addons/mod/assign/services/assign.ts index aa53f5f4f..dec89da07 100644 --- a/src/addons/mod/assign/services/assign.ts +++ b/src/addons/mod/assign/services/assign.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreInterceptor } from '@classes/interceptor'; -import { CoreWSExternalWarning, CoreWSExternalFile } from '@services/ws'; +import { CoreWSExternalWarning, CoreWSExternalFile, CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreTextUtils } from '@services/utils/text'; @@ -405,12 +405,12 @@ export class AddonModAssignProvider { * @param submissionPlugin Submission plugin. * @return Submission plugin attachments. */ - getSubmissionPluginAttachments(submissionPlugin: AddonModAssignPlugin): CoreWSExternalFile[] { + getSubmissionPluginAttachments(submissionPlugin: AddonModAssignPlugin): CoreWSFile[] { if (!submissionPlugin.fileareas) { return []; } - const files: CoreWSExternalFile[] = []; + const files: CoreWSFile[] = []; submissionPlugin.fileareas.forEach((filearea) => { if (!filearea || !filearea.files) { diff --git a/src/addons/mod/assign/services/feedback-delegate.ts b/src/addons/mod/assign/services/feedback-delegate.ts index 8c9951877..2fb96a013 100644 --- a/src/addons/mod/assign/services/feedback-delegate.ts +++ b/src/addons/mod/assign/services/feedback-delegate.ts @@ -17,7 +17,7 @@ import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { AddonModAssignDefaultFeedbackHandler } from './handlers/default-feedback'; import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin, AddonModAssignSavePluginData } from './assign'; import { makeSingleton } from '@singletons'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { AddonModAssignSubmissionFormatted } from './assign-helper'; import { CoreFormFields } from '@singletons/form'; @@ -79,7 +79,7 @@ export interface AddonModAssignFeedbackHandler extends CoreDelegateHandler { submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, siteId?: string, - ): CoreWSExternalFile[] | Promise; + ): CoreWSFile[] | Promise; /** * Get a readable name to use for the plugin. @@ -246,8 +246,8 @@ export class AddonModAssignFeedbackDelegateService extends CoreDelegate { - const files: CoreWSExternalFile[] | undefined = + ): Promise { + const files: CoreWSFile[] | undefined = await this.executeFunctionOnEnabled(plugin.type, 'getPluginFiles', [assign, submission, plugin, siteId]); return files || []; diff --git a/src/addons/mod/assign/services/handlers/default-feedback.ts b/src/addons/mod/assign/services/handlers/default-feedback.ts index 62c394bdb..ef19ffdd7 100644 --- a/src/addons/mod/assign/services/handlers/default-feedback.ts +++ b/src/addons/mod/assign/services/handlers/default-feedback.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { Translate } from '@singletons'; import { AddonModAssignPlugin } from '../assign'; import { AddonModAssignFeedbackHandler } from '../feedback-delegate'; @@ -52,7 +52,7 @@ export class AddonModAssignDefaultFeedbackHandler implements AddonModAssignFeedb * * @return The files (or promise resolved with the files). */ - getPluginFiles(): CoreWSExternalFile[] { + getPluginFiles(): CoreWSFile[] { return []; } diff --git a/src/addons/mod/assign/services/handlers/default-submission.ts b/src/addons/mod/assign/services/handlers/default-submission.ts index db3791205..2d2b42cf0 100644 --- a/src/addons/mod/assign/services/handlers/default-submission.ts +++ b/src/addons/mod/assign/services/handlers/default-submission.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { Translate } from '@singletons'; import { AddonModAssignPlugin } from '../assign'; import { AddonModAssignSubmissionHandler } from '../submission-delegate'; @@ -79,7 +79,7 @@ export class AddonModAssignDefaultSubmissionHandler implements AddonModAssignSub * * @return The files (or promise resolved with the files). */ - getPluginFiles(): CoreWSExternalFile[] { + getPluginFiles(): CoreWSFile[] { return []; } diff --git a/src/addons/mod/assign/services/handlers/prefetch.ts b/src/addons/mod/assign/services/handlers/prefetch.ts index f2f092d28..0ffc75535 100644 --- a/src/addons/mod/assign/services/handlers/prefetch.ts +++ b/src/addons/mod/assign/services/handlers/prefetch.ts @@ -26,7 +26,7 @@ import { AddonModAssignSubmissionDelegate } from '../submission-delegate'; import { AddonModAssignFeedbackDelegate } from '../feedback-delegate'; import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler'; import { CoreCourse, CoreCourseAnyModuleData, CoreCourseCommonModWSOptions } from '@features/course/services/course'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { AddonModAssignHelper, AddonModAssignSubmissionFormatted } from '../assign-helper'; import { CoreCourseHelper } from '@features/course/services/course-helper'; import { CoreUtils } from '@services/utils/utils'; @@ -82,13 +82,13 @@ export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPref * @param courseId Course ID the module belongs to. * @return Promise resolved with the list of files. */ - async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { + async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { const siteId = CoreSites.getCurrentSiteId(); try { const assign = await AddonModAssign.getAssignment(courseId, module.id, { siteId }); // Get intro files and attachments. - let files = assign.introattachments || []; + let files: CoreWSFile[] = assign.introattachments || []; files = files.concat(this.getIntroFilesFromInstance(module, assign)); // Now get the files in the submissions. @@ -145,7 +145,7 @@ export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPref submitId: number, blindMarking: boolean, siteId?: string, - ): Promise { + ): Promise { const submissionStatus = await AddonModAssign.getSubmissionStatusWithRetry(assign, { userId: submitId, @@ -158,7 +158,7 @@ export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPref return []; } - const promises: Promise[] = []; + const promises: Promise[] = []; if (userSubmission.plugins) { // Add submission plugin files. @@ -245,7 +245,7 @@ export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPref // Get assignment to retrieve all its submissions. const assign = await AddonModAssign.getAssignment(courseId, module.id, options); - const promises: Promise[] = []; + const promises: Promise[] = []; const blindMarking = assign.blindmarking && !assign.revealidentities; if (blindMarking) { @@ -259,7 +259,7 @@ export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPref promises.push(CoreCourseHelper.getModuleCourseIdByInstance(assign.id, 'assign', siteId)); // Download intro files and attachments. Do not call getFiles because it'd call some WS twice. - let files = assign.introattachments || []; + let files: CoreWSFile[] = assign.introattachments || []; files = files.concat(this.getIntroFilesFromInstance(module, assign)); promises.push(CoreFilepool.addFilesToQueue(siteId, files, this.component, module.id)); @@ -293,7 +293,7 @@ export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPref // Get submissions. const submissions = await AddonModAssign.getSubmissions(assign.id, modOptions); - const promises: Promise[] = []; + const promises: Promise[] = []; promises.push(this.prefetchParticipantSubmissions( assign, @@ -359,7 +359,7 @@ export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPref AddonModAssignHelper.getSubmissionsUserData(assign, submissions, group.id, options) .then((submissions: AddonModAssignSubmissionFormatted[]) => { - const subPromises: Promise[] = submissions.map((submission) => { + const subPromises: Promise[] = submissions.map((submission) => { const submissionOptions = { userId: submission.submitid, groupId: group.id, @@ -426,7 +426,7 @@ export class AddonModAssignPrefetchHandlerService extends CoreCourseActivityPref const userId = options.userId; try { - const promises: Promise[] = []; + const promises: Promise[] = []; const blindMarking = !!assign.blindmarking && !assign.revealidentities; let userIds: number[] = []; const userSubmission = AddonModAssign.getSubmissionObjectFromAttempt(assign, submission.lastattempt); diff --git a/src/addons/mod/assign/services/submission-delegate.ts b/src/addons/mod/assign/services/submission-delegate.ts index a58da6eac..136da9d78 100644 --- a/src/addons/mod/assign/services/submission-delegate.ts +++ b/src/addons/mod/assign/services/submission-delegate.ts @@ -17,7 +17,7 @@ import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { AddonModAssignDefaultSubmissionHandler } from './handlers/default-submission'; import { AddonModAssignAssign, AddonModAssignSubmission, AddonModAssignPlugin, AddonModAssignSavePluginData } from './assign'; import { makeSingleton } from '@singletons'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { AddonModAssignSubmissionsDBRecordFormatted } from './assign-offline'; import { CoreFormFields } from '@singletons/form'; @@ -139,7 +139,7 @@ export interface AddonModAssignSubmissionHandler extends CoreDelegateHandler { submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, siteId?: string, - ): CoreWSExternalFile[] | Promise; + ): CoreWSFile[] | Promise; /** * Get a readable name to use for the plugin. @@ -384,8 +384,8 @@ export class AddonModAssignSubmissionDelegateService extends CoreDelegate { - const files: CoreWSExternalFile[] | undefined = + ): Promise { + const files: CoreWSFile[] | undefined = await this.executeFunctionOnEnabled(plugin.type, 'getPluginFiles', [assign, submission, plugin, siteId]); return files || []; diff --git a/src/addons/mod/assign/submission/file/services/handler.ts b/src/addons/mod/assign/submission/file/services/handler.ts index 00333791c..72c93d843 100644 --- a/src/addons/mod/assign/submission/file/services/handler.ts +++ b/src/addons/mod/assign/submission/file/services/handler.ts @@ -24,10 +24,10 @@ import { AddonModAssignOffline, AddonModAssignSubmissionsDBRecordFormatted } fro import { AddonModAssignSubmissionHandler } from '@addons/mod/assign/services/submission-delegate'; import { Injectable, Type } from '@angular/core'; import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; -import { CoreFileHelper } from '@services/file-helper'; +import { CoreFileEntry, CoreFileHelper } from '@services/file-helper'; import { CoreFileSession } from '@services/file-session'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonModAssignSubmissionFileComponent } from '../component/file'; import { FileEntry } from '@ionic-native/file/ngx'; @@ -156,7 +156,7 @@ export class AddonModAssignSubmissionFileHandlerService implements AddonModAssig assign: AddonModAssignAssign, submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, - ): CoreWSExternalFile[] { + ): CoreWSFile[] { return AddonModAssign.getSubmissionPluginAttachments(plugin); } @@ -350,14 +350,14 @@ export class AddonModAssignSubmissionFileHandlerService implements AddonModAssig submission: AddonModAssignSubmission, offlineData?: AddonModAssignSubmissionsDBRecordFormatted, siteId?: string, - ): Promise<(FileEntry | CoreWSExternalFile)[]> { + ): Promise { const filesData = offlineData?.plugindata.files_filemanager; if (!filesData) { return []; } // Has some data to sync. - let files: (FileEntry | CoreWSExternalFile)[] = filesData.online || []; + let files: CoreFileEntry[] = filesData.online || []; if (filesData.offline) { // Has offline files, get them and add them to the list. diff --git a/src/addons/mod/assign/submission/onlinetext/services/handler.ts b/src/addons/mod/assign/submission/onlinetext/services/handler.ts index 36e4c04c5..56f86d63a 100644 --- a/src/addons/mod/assign/submission/onlinetext/services/handler.ts +++ b/src/addons/mod/assign/submission/onlinetext/services/handler.ts @@ -27,7 +27,7 @@ import { CoreFileHelper } from '@services/file-helper'; import { CoreSites } from '@services/sites'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { AddonModAssignSubmissionOnlineTextComponent } from '../component/onlinetext'; @@ -124,7 +124,7 @@ export class AddonModAssignSubmissionOnlineTextHandlerService implements AddonMo assign: AddonModAssignAssign, submission: AddonModAssignSubmission, plugin: AddonModAssignPlugin, - ): CoreWSExternalFile[] { + ): CoreWSFile[] { return AddonModAssign.getSubmissionPluginAttachments(plugin); } diff --git a/src/addons/mod/book/services/handlers/prefetch.ts b/src/addons/mod/book/services/handlers/prefetch.ts index 6dc9c26cf..75dff4a32 100644 --- a/src/addons/mod/book/services/handlers/prefetch.ts +++ b/src/addons/mod/book/services/handlers/prefetch.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreCourseResourcePrefetchHandlerBase } from '@features/course/classes/resource-prefetch-handler'; import { CoreCourseAnyModuleData, CoreCourseWSModule } from '@features/course/services/course'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonModBook, AddonModBookProvider } from '../book'; @@ -55,7 +55,7 @@ export class AddonModBookPrefetchHandlerService extends CoreCourseResourcePrefet * @param courseId Course ID. * @return Promise resolved with list of intro files. */ - async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { + async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { const book = await CoreUtils.ignoreErrors(AddonModBook.getBook(courseId, module.id)); return this.getIntroFilesFromInstance(module, book); diff --git a/src/addons/mod/choice/services/handlers/prefetch.ts b/src/addons/mod/choice/services/handlers/prefetch.ts index 0ead84f9a..d8e10a54a 100644 --- a/src/addons/mod/choice/services/handlers/prefetch.ts +++ b/src/addons/mod/choice/services/handlers/prefetch.ts @@ -19,7 +19,7 @@ import { CoreUser } from '@features/user/services/user'; import { CoreFilepool } from '@services/filepool'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonModChoice, AddonModChoiceProvider } from '../choice'; import { AddonModChoiceSync, AddonModChoiceSyncResult } from '../choice-sync'; @@ -116,7 +116,7 @@ export class AddonModChoicePrefetchHandlerService extends CoreCourseActivityPref /** * @inheritdoc */ - async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { + async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { const choice = await CoreUtils.ignoreErrors(AddonModChoice.getChoice(courseId, module.id)); return this.getIntroFilesFromInstance(module, choice); diff --git a/src/addons/mod/data/fields/file/component/file.ts b/src/addons/mod/data/fields/file/component/file.ts index 2f1680027..542858939 100644 --- a/src/addons/mod/data/fields/file/component/file.ts +++ b/src/addons/mod/data/fields/file/component/file.ts @@ -15,8 +15,7 @@ import { Component } from '@angular/core'; import { AddonModDataEntryField, AddonModDataProvider } from '@addons/mod/data/services/data'; import { AddonModDataFieldPluginComponent } from '@addons/mod/data/classes/field-plugin-component'; import { CoreFileSession } from '@services/file-session'; -import { CoreWSExternalFile } from '@services/ws'; -import { FileEntry } from '@ionic-native/file'; +import { CoreFileEntry } from '@services/file-helper'; /** * Component to render data file field. @@ -27,7 +26,7 @@ import { FileEntry } from '@ionic-native/file'; }) export class AddonModDataFieldFileComponent extends AddonModDataFieldPluginComponent { - files: (CoreWSExternalFile | FileEntry)[] = []; + files: CoreFileEntry[] = []; component?: string; componentId?: number; maxSizeBytes?: number; @@ -38,7 +37,7 @@ export class AddonModDataFieldFileComponent extends AddonModDataFieldPluginCompo * @param value Input value. * @return List of files. */ - protected getFiles(value?: Partial): (CoreWSExternalFile | FileEntry)[] { + protected getFiles(value?: Partial): CoreFileEntry[] { let files = value?.files || []; // Reduce to first element. diff --git a/src/addons/mod/data/fields/file/services/handler.ts b/src/addons/mod/data/fields/file/services/handler.ts index 77157c31e..2db49d5c3 100644 --- a/src/addons/mod/data/fields/file/services/handler.ts +++ b/src/addons/mod/data/fields/file/services/handler.ts @@ -22,12 +22,12 @@ import { import { AddonModDataFieldHandler } from '@addons/mod/data/services/data-fields-delegate'; import { Injectable, Type } from '@angular/core'; import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; -import { FileEntry } from '@ionic-native/file'; +import { FileEntry } from '@ionic-native/file/ngx'; import { CoreFileSession } from '@services/file-session'; import { CoreFormFields } from '@singletons/form'; -import { CoreWSExternalFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { AddonModDataFieldFileComponent } from '../component/file'; +import { CoreFileEntry } from '@services/file-helper'; /** * Handler for file data field plugin. @@ -77,7 +77,7 @@ export class AddonModDataFieldFileHandlerService implements AddonModDataFieldHan /** * @inheritdoc */ - getFieldEditFiles(field: AddonModDataField): (CoreWSExternalFile | FileEntry)[] { + getFieldEditFiles(field: AddonModDataField): CoreFileEntry[] { return CoreFileSession.getFiles(AddonModDataProvider.COMPONENT, field.dataid + '_' + field.id); } diff --git a/src/addons/mod/data/fields/picture/component/picture.ts b/src/addons/mod/data/fields/picture/component/picture.ts index 11d19be88..7c64674e1 100644 --- a/src/addons/mod/data/fields/picture/component/picture.ts +++ b/src/addons/mod/data/fields/picture/component/picture.ts @@ -13,10 +13,9 @@ // limitations under the License. import { AddonModDataEntryField, AddonModDataProvider } from '@addons/mod/data/services/data'; import { Component } from '@angular/core'; -import { FileEntry } from '@ionic-native/file'; +import { CoreFileEntry, CoreFileHelper } from '@services/file-helper'; import { CoreFileSession } from '@services/file-session'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreWSExternalFile } from '@services/ws'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; /** @@ -28,12 +27,12 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- }) export class AddonModDataFieldPictureComponent extends AddonModDataFieldPluginComponent { - files: (CoreWSExternalFile | FileEntry)[] = []; + files: CoreFileEntry[] = []; component?: string; componentId?: number; maxSizeBytes?: number; - image?: CoreWSExternalFile | FileEntry; + image?: CoreFileEntry; entryId?: number; imageUrl?: string; title?: string; @@ -46,7 +45,7 @@ export class AddonModDataFieldPictureComponent extends AddonModDataFieldPluginCo * @param value Input value. * @return List of files. */ - protected getFiles(value?: Partial): (CoreWSExternalFile | FileEntry)[] { + protected getFiles(value?: Partial): CoreFileEntry[] { let files = value?.files || []; // Reduce to first element. @@ -65,9 +64,9 @@ export class AddonModDataFieldPictureComponent extends AddonModDataFieldPluginCo * @return File found or false. */ protected findFile( - files: (CoreWSExternalFile | FileEntry)[], + files: CoreFileEntry[], filenameSeek: string, - ): CoreWSExternalFile | FileEntry | undefined { + ): CoreFileEntry | undefined { return files.find((file) => ('name' in file ? file.name : file.filename) == filenameSeek) || undefined; } @@ -130,7 +129,7 @@ export class AddonModDataFieldPictureComponent extends AddonModDataFieldPluginCo if (this.image) { this.imageUrl = 'name' in this.image ? this.image.toURL() // Is Offline. - : this.image.fileurl; + : CoreFileHelper.getFileUrl(this.image); } }, 1); diff --git a/src/addons/mod/data/fields/picture/services/handler.ts b/src/addons/mod/data/fields/picture/services/handler.ts index cd1c26dac..24937cdd9 100644 --- a/src/addons/mod/data/fields/picture/services/handler.ts +++ b/src/addons/mod/data/fields/picture/services/handler.ts @@ -22,12 +22,12 @@ import { import { AddonModDataFieldHandler } from '@addons/mod/data/services/data-fields-delegate'; import { Injectable, Type } from '@angular/core'; import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; -import { FileEntry } from '@ionic-native/file'; +import { FileEntry } from '@ionic-native/file/ngx'; import { CoreFileSession } from '@services/file-session'; import { CoreFormFields } from '@singletons/form'; -import { CoreWSExternalFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { AddonModDataFieldPictureComponent } from '../component/picture'; +import { CoreFileEntry } from '@services/file-helper'; /** * Handler for picture data field plugin. @@ -88,7 +88,7 @@ export class AddonModDataFieldPictureHandlerService implements AddonModDataField /** * @inheritdoc */ - getFieldEditFiles(field: AddonModDataField): (CoreWSExternalFile | FileEntry)[] { + getFieldEditFiles(field: AddonModDataField): CoreFileEntry[] { return CoreFileSession.getFiles(AddonModDataProvider.COMPONENT, field.dataid + '_' + field.id); } diff --git a/src/addons/mod/data/fields/textarea/component/textarea.ts b/src/addons/mod/data/fields/textarea/component/textarea.ts index 8b459b8ef..e8cc52d5d 100644 --- a/src/addons/mod/data/fields/textarea/component/textarea.ts +++ b/src/addons/mod/data/fields/textarea/component/textarea.ts @@ -16,7 +16,7 @@ import { Component } from '@angular/core'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; import { AddonModDataEntryField, AddonModDataProvider } from '@addons/mod/data/services/data'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; /** * Component to render data number field. @@ -37,7 +37,7 @@ export class AddonModDataFieldTextareaComponent extends AddonModDataFieldPluginC * @return Replaced string to be rendered. */ format(value?: Partial): string { - const files: CoreWSExternalFile[] = (value && value.files) || []; + const files: CoreWSFile[] = (value && value.files) || []; return value ? CoreTextUtils.replacePluginfileUrls(value.content || '', files) : ''; } diff --git a/src/addons/mod/data/fields/textarea/services/handler.ts b/src/addons/mod/data/fields/textarea/services/handler.ts index 2bfc9b85f..9eac09d39 100644 --- a/src/addons/mod/data/fields/textarea/services/handler.ts +++ b/src/addons/mod/data/fields/textarea/services/handler.ts @@ -14,13 +14,13 @@ import { AddonModDataEntryField, AddonModDataField, AddonModDataSubfieldData } from '@addons/mod/data/services/data'; import { Injectable, Type } from '@angular/core'; -import { FileEntry } from '@ionic-native/file'; import { CoreFormFields } from '@singletons/form'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { AddonModDataFieldTextHandlerService } from '../../text/services/handler'; import { AddonModDataFieldTextareaComponent } from '../component/textarea'; +import { CoreFileEntry } from '@services/file-helper'; /** * Handler for textarea data field plugin. @@ -49,7 +49,7 @@ export class AddonModDataFieldTextareaHandlerService extends AddonModDataFieldTe const fieldName = 'f_' + field.id; const files = this.getFieldEditFiles(field, inputData, originalFieldData); - let text = CoreTextUtils.restorePluginfileUrls(inputData[fieldName] || '', files); + let text = CoreTextUtils.restorePluginfileUrls(inputData[fieldName] || '', files); // Add some HTML to the text if needed. text = CoreTextUtils.formatHtmlLines(text); @@ -83,7 +83,7 @@ export class AddonModDataFieldTextareaHandlerService extends AddonModDataFieldTe field: AddonModDataField, inputData: CoreFormFields, originalFieldData: AddonModDataEntryField, - ): (CoreWSExternalFile | FileEntry)[] { + ): CoreFileEntry[] { return (originalFieldData && originalFieldData.files) || []; } @@ -116,7 +116,7 @@ export class AddonModDataFieldTextareaHandlerService extends AddonModDataFieldTe // Take the original files since we cannot edit them on the app. originalContent.content = CoreTextUtils.replacePluginfileUrls( originalContent.content, - originalContent.files, + originalContent.files, ); } diff --git a/src/addons/mod/data/services/data-fields-delegate.ts b/src/addons/mod/data/services/data-fields-delegate.ts index 0c79e29a0..e6509241b 100644 --- a/src/addons/mod/data/services/data-fields-delegate.ts +++ b/src/addons/mod/data/services/data-fields-delegate.ts @@ -22,8 +22,8 @@ import { AddonModDataEntryField, AddonModDataSubfieldData, } from './data'; import { CoreFormFields } from '@singletons/form'; -import { CoreWSExternalFile } from '@services/ws'; -import { FileEntry } from '@ionic-native/file'; +import { FileEntry } from '@ionic-native/file/ngx'; +import { CoreFileEntry } from '@services/file-helper'; /** * Interface that all fields handlers must implement. @@ -94,7 +94,7 @@ export interface AddonModDataFieldHandler extends CoreDelegateHandler { field: AddonModDataField, inputData: CoreFormFields, originalFieldData: AddonModDataEntryField, - ): (CoreWSExternalFile | FileEntry)[]; + ): CoreFileEntry[]; /** * Check and get field requeriments. @@ -183,7 +183,7 @@ export class AddonModDataFieldsDelegateService extends CoreDelegate { + ): Promise { if (!inputData) { return []; } @@ -745,7 +745,7 @@ export class AddonModDataHelperProvider { dataId: number, entryId: number, fieldId: number, - files: (CoreWSExternalFile | FileEntry)[], + files: CoreFileEntry[], siteId?: string, ): Promise { // Get the folder where to store the files. @@ -771,7 +771,7 @@ export class AddonModDataHelperProvider { itemId: number = 0, entryId: number, fieldId: number, - files: (CoreWSExternalFile | FileEntry)[], + files: CoreFileEntry[], offline: boolean, siteId?: string, ): Promise { diff --git a/src/addons/mod/data/services/data-sync.ts b/src/addons/mod/data/services/data-sync.ts index b89e4b2a6..c54158cfd 100644 --- a/src/addons/mod/data/services/data-sync.ts +++ b/src/addons/mod/data/services/data-sync.ts @@ -21,13 +21,12 @@ import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; import { CoreRatingSync } from '@features/rating/services/rating-sync'; -import { FileEntry } from '@ionic-native/file'; import { CoreApp } from '@services/app'; +import { CoreFileEntry } from '@services/file-helper'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreSync } from '@services/sync'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; import { Translate, makeSingleton } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { AddonModDataProvider, AddonModData, AddonModDataData, AddonModDataAction } from './data'; @@ -348,7 +347,7 @@ export class AddonModDataSyncProvider extends CoreCourseActivitySyncBaseProvider // Upload Files if asked. const value = CoreTextUtils.parseJSON(field.value || ''); if (value.online || value.offline) { - let files: (CoreWSExternalFile | FileEntry)[] = value.online || []; + let files: CoreFileEntry[] = value.online || []; const offlineFiles = value.offline ? await AddonModDataHelper.getStoredFiles(editAction.dataid, entryId, field.fieldid) diff --git a/src/addons/mod/data/services/data.ts b/src/addons/mod/data/services/data.ts index 4b3af40c1..44203896a 100644 --- a/src/addons/mod/data/services/data.ts +++ b/src/addons/mod/data/services/data.ts @@ -19,8 +19,8 @@ import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreRatingInfo } from '@features/rating/services/rating'; import { CoreTagItem } from '@features/tag/services/tag'; -import { FileEntry } from '@ionic-native/file'; import { CoreApp } from '@services/app'; +import { CoreFileEntry } from '@services/file-helper'; import { CoreFilepool } from '@services/filepool'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; @@ -1151,7 +1151,7 @@ export type AddonModDataEntryField = { content2: string; // Contents. content3: string; // Contents. content4: string; // Contents. - files: (CoreWSExternalFile | FileEntry)[]; + files: CoreFileEntry[]; }; /** @@ -1180,7 +1180,7 @@ export type AddonModDataSubfieldData = { fieldid: number; subfield?: string; value?: unknown; // Value encoded in JSON. - files?: (CoreWSExternalFile | FileEntry)[]; + files?: CoreFileEntry[]; }; /** diff --git a/src/addons/mod/data/services/handlers/default-field.ts b/src/addons/mod/data/services/handlers/default-field.ts index 3eec28e1a..a25fc5d7b 100644 --- a/src/addons/mod/data/services/handlers/default-field.ts +++ b/src/addons/mod/data/services/handlers/default-field.ts @@ -11,9 +11,9 @@ // 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 { FileEntry } from '@ionic-native/file'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreFileEntry } from '@services/file-helper'; import { AddonModDataEntryField, AddonModDataSearchEntriesAdvancedFieldFormatted, AddonModDataSubfieldData } from '../data'; import { AddonModDataFieldHandler } from '../data-fields-delegate'; @@ -50,7 +50,7 @@ export class AddonModDataDefaultFieldHandler implements AddonModDataFieldHandler /** * @inheritdoc */ - getFieldEditFiles(): (CoreWSExternalFile | FileEntry)[] { + getFieldEditFiles(): CoreFileEntry[] { return []; } diff --git a/src/addons/mod/data/services/handlers/prefetch.ts b/src/addons/mod/data/services/handlers/prefetch.ts index 7a8106596..e67b65a49 100644 --- a/src/addons/mod/data/services/handlers/prefetch.ts +++ b/src/addons/mod/data/services/handlers/prefetch.ts @@ -21,7 +21,7 @@ import { CoreGroup, CoreGroups } from '@services/groups'; import { CoreSitesCommonWSOptions, CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonModDataProvider, AddonModDataEntry, AddonModData, AddonModDataData } from '../data'; import { AddonModDataSync, AddonModDataSyncResult } from '../data-sync'; @@ -82,10 +82,10 @@ export class AddonModDataPrefetchHandlerService extends CoreCourseActivityPrefet courseId: number, omitFail: boolean, options: CoreCourseCommonModWSOptions = {}, - ): Promise<{ database: AddonModDataData; groups: CoreGroup[]; entries: AddonModDataEntry[]; files: CoreWSExternalFile[]}> { + ): Promise<{ database: AddonModDataData; groups: CoreGroup[]; entries: AddonModDataEntry[]; files: CoreWSFile[]}> { let groups: CoreGroup[] = []; let entries: AddonModDataEntry[] = []; - let files: CoreWSExternalFile[] = []; + let files: CoreWSFile[] = []; options.cmId = options.cmId || module.id; options.siteId = options.siteId || CoreSites.getCurrentSiteId(); @@ -131,12 +131,12 @@ export class AddonModDataPrefetchHandlerService extends CoreCourseActivityPrefet * @param entries List of entries to get files from. * @return List of files. */ - protected getEntriesFiles(entries: AddonModDataEntry[]): CoreWSExternalFile[] { - let files: CoreWSExternalFile[] = []; + protected getEntriesFiles(entries: AddonModDataEntry[]): CoreWSFile[] { + let files: CoreWSFile[] = []; entries.forEach((entry) => { CoreUtils.objectToArray(entry.contents).forEach((content) => { - files = files.concat(content.files); + files = files.concat(content.files); }); }); @@ -146,14 +146,14 @@ export class AddonModDataPrefetchHandlerService extends CoreCourseActivityPrefet /** * @inheritdoc */ - async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { + async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { return this.getDatabaseInfoHelper(module, courseId, true).then((info) => info.files); } /** * @inheritdoc */ - async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { + async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { const data = await CoreUtils.ignoreErrors(AddonModData.getDatabase(courseId, module.id)); return this.getIntroFilesFromInstance(module, data); diff --git a/src/addons/mod/feedback/components/components.module.ts b/src/addons/mod/feedback/components/components.module.ts new file mode 100644 index 000000000..ce2b48392 --- /dev/null +++ b/src/addons/mod/feedback/components/components.module.ts @@ -0,0 +1,34 @@ +// (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 { CoreSharedModule } from '@/core/shared.module'; +import { NgModule } from '@angular/core'; +import { CoreCourseComponentsModule } from '@features/course/components/components.module'; +import { AddonModFeedbackIndexComponent } from './index/index'; + +@NgModule({ + declarations: [ + AddonModFeedbackIndexComponent, + ], + imports: [ + CoreSharedModule, + CoreCourseComponentsModule, + ], + providers: [ + ], + exports: [ + AddonModFeedbackIndexComponent, + ], +}) +export class AddonModFeedbackComponentsModule {} diff --git a/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html b/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html new file mode 100644 index 000000000..e5c8a6962 --- /dev/null +++ b/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html @@ -0,0 +1,230 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{'core.groupsseparate' | translate }} + {{'core.groupsvisible' | translate }} + + + + {{groupOpt.name}} + + + + + +

{{ 'addon.mod_feedback.completed_feedbacks' | translate }}

+
+ {{completedCount}} +
+ + +

{{ 'addon.mod_feedback.show_nonrespondents' | translate }}

+
+
+ + +

{{ 'addon.mod_feedback.questions' | translate }}

+
+ {{itemsCount}} +
+
+
+ + + + + + + + + + + {{ 'core.hasdatatosync' | translate:{$a: moduleName} }} + + + + + + + {{ 'addon.mod_feedback.feedback_is_not_open' | translate }} + + + + + + + {{ 'addon.mod_feedback.this_feedback_is_already_submitted' | translate }} + + + + + + +

{{ 'addon.mod_feedback.feedbackopen' | translate }}

+

{{overview.openTimeReadable}}

+
+
+ + +

{{ 'addon.mod_feedback.feedbackclose' | translate }}

+

{{overview.closeTimeReadable}}

+
+
+ + +

{{ 'addon.mod_feedback.page_after_submit' | translate }}

+ + +
+
+ + + +

{{ 'addon.mod_feedback.mode' | translate }}

+

{{ 'addon.mod_feedback.anonymous' | translate }}

+

{{ 'addon.mod_feedback.non_anonymous' | translate }}

+
+
+ + + + + + {{ 'addon.mod_feedback.preview' | translate }} + + + + + + {{ 'addon.mod_feedback.complete_the_form' | translate }} + + + {{ 'addon.mod_feedback.continue_the_form' | translate }} + + + + + + + +
+
+
+
+ + + + + + + + + + + {{ warning }} + + + + + + +

+ {{item.num}}. + +

+

+ + +

+ + +
    +
  • {{ data }}
  • +
+

{{ 'addon.mod_feedback.average' | translate }}: {{item.average | number : '1.2-2'}}

+
+ +
    + +
  • {{ data }}
  • +
    +
+
+ + + +

+ {{ 'addon.mod_feedback.average' | translate }}: {{item.average | number : '1.2-2'}} +

+
+
+
+
+
+
+
+
diff --git a/src/addons/mod/feedback/components/index/index.ts b/src/addons/mod/feedback/components/index/index.ts new file mode 100644 index 000000000..cf2799220 --- /dev/null +++ b/src/addons/mod/feedback/components/index/index.ts @@ -0,0 +1,503 @@ +// (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 { Component, Input, Optional, ViewChild, OnInit, OnDestroy } from '@angular/core'; +import { CoreTabsComponent } from '@components/tabs/tabs'; +import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component'; +import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; +import { IonContent } from '@ionic/angular'; +import { CoreGroupInfo, CoreGroups } from '@services/groups'; +import { CoreNavigator } from '@services/navigator'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreTimeUtils } from '@services/utils/time'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { + AddonModFeedback, + AddonModFeedbackGetFeedbackAccessInformationWSResponse, + AddonModFeedbackProvider, + AddonModFeedbackWSFeedback, + AddonModFeedbackWSItem, +} from '../../services/feedback'; +import { AddonModFeedbackOffline } from '../../services/feedback-offline'; +import { + AddonModFeedbackAutoSyncData, + AddonModFeedbackSync, + AddonModFeedbackSyncProvider, + AddonModFeedbackSyncResult, +} from '../../services/feedback-sync'; +import { AddonModFeedbackModuleHandlerService } from '../../services/handlers/module'; +import { AddonModFeedbackPrefetchHandler } from '../../services/handlers/prefetch'; + +/** + * Component that displays a feedback index page. + */ +@Component({ + selector: 'addon-mod-feedback-index', + templateUrl: 'addon-mod-feedback-index.html', +}) +export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit, OnDestroy { + + @ViewChild(CoreTabsComponent) tabsComponent?: CoreTabsComponent; + + @Input() tab = 'overview'; + @Input() group = 0; + + component = AddonModFeedbackProvider.COMPONENT; + moduleName = 'feedback'; + feedback?: AddonModFeedbackWSFeedback; + goPage?: number; + items: AddonModFeedbackItem[] = []; + warning?: string; + showAnalysis = false; + tabsReady = false; + firstSelectedTab?: number; + access?: AddonModFeedbackGetFeedbackAccessInformationWSResponse; + completedCount = 0; + itemsCount = 0; + groupInfo?: CoreGroupInfo; + + overview = { + timeopen: 0, + openTimeReadable: '', + timeclose: 0, + closeTimeReadable: '', + }; + + tabsLoaded = { + overview: false, + analysis: false, + }; + + protected submitObserver: CoreEventObserver; + protected syncEventName = AddonModFeedbackSyncProvider.AUTO_SYNCED; + + constructor( + protected content?: IonContent, + @Optional() courseContentsPage?: CoreCourseContentsPage, + ) { + super('AddonModLessonIndexComponent', content, courseContentsPage); + + // Listen for form submit events. + this.submitObserver = CoreEvents.on(AddonModFeedbackProvider.FORM_SUBMITTED, async (data) => { + if (!this.feedback || data.feedbackId != this.feedback.id) { + return; + } + + this.tabsLoaded.analysis = false; + this.tabsLoaded.overview = false; + this.loaded = false; + + // Prefetch data if needed. + if (!data.offline && this.isPrefetched()) { + await CoreUtils.ignoreErrors(AddonModFeedbackSync.prefetchAfterUpdate( + AddonModFeedbackPrefetchHandler.instance, + this.module, + this.courseId, + )); + } + + // Load the right tab. + if (data.tab != this.tab) { + this.tabChanged(data.tab); + } else { + this.loadContent(true); + } + }, this.siteId); + } + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + super.ngOnInit(); + + try { + await this.loadContent(false, true); + + if (this.feedback) { + CoreUtils.ignoreErrors(AddonModFeedback.logView(this.feedback.id, this.feedback.name)); + } + } finally { + this.tabsReady = true; + } + } + + /** + * @inheritdoc + */ + protected async invalidateContent(): Promise { + const promises: Promise[] = []; + + promises.push(AddonModFeedback.invalidateFeedbackData(this.courseId)); + if (this.feedback) { + promises.push(AddonModFeedback.invalidateFeedbackAccessInformationData(this.feedback.id)); + promises.push(AddonModFeedback.invalidateAnalysisData(this.feedback.id)); + promises.push(CoreGroups.invalidateActivityAllowedGroups(this.feedback.coursemodule)); + promises.push(CoreGroups.invalidateActivityGroupMode(this.feedback.coursemodule)); + promises.push(AddonModFeedback.invalidateResumePageData(this.feedback.id)); + } + + this.tabsLoaded.analysis = false; + this.tabsLoaded.overview = false; + + await Promise.all(promises); + } + + /** + * @inheritdoc + */ + protected isRefreshSyncNeeded(syncEventData: AddonModFeedbackAutoSyncData): boolean { + if (this.feedback && syncEventData.feedbackId == this.feedback.id) { + // Refresh the data. + this.content?.scrollToTop(); + + return true; + } + + return false; + } + + /** + * @inheritdoc + */ + protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + try { + this.feedback = await AddonModFeedback.getFeedback(this.courseId, this.module.id); + + this.description = this.feedback.intro; + this.dataRetrieved.emit(this.feedback); + + if (sync) { + // Try to synchronize the feedback. + await this.syncActivity(showErrors); + } + + // Check if there are answers stored in offline. + this.access = await AddonModFeedback.getFeedbackAccessInformation(this.feedback.id, { cmId: this.module.id }); + + this.showAnalysis = (this.access.canviewreports || this.access.canviewanalysis) && !this.access.isempty; + this.firstSelectedTab = 0; + if (!this.showAnalysis) { + this.tab = 'overview'; + } + + if (this.tab == 'analysis') { + this.firstSelectedTab = 1; + + return await this.fetchFeedbackAnalysisData(); + } + + await this.fetchFeedbackOverviewData(); + } finally { + // Now fill the context menu. + this.fillContextMenu(refresh); + + if (this.feedback) { + // Check if there are responses stored in offline. + this.hasOffline = await AddonModFeedbackOffline.hasFeedbackOfflineData(this.feedback.id); + } + + if (this.tabsReady) { + // Make sure the right tab is selected. + this.tabsComponent?.selectTab(this.tab || 'overview'); + } + } + } + + /** + * Convenience function to get feedback overview data. + * + * @return Resolved when done. + */ + protected async fetchFeedbackOverviewData(): Promise { + const promises: Promise[] = []; + + if (this.access!.cancomplete && this.access!.cansubmit && this.access!.isopen) { + promises.push(AddonModFeedback.getResumePage(this.feedback!.id, { cmId: this.module.id }).then((goPage) => { + this.goPage = goPage > 0 ? goPage : undefined; + + return; + })); + } + + if (this.access!.canedititems) { + this.overview.timeopen = (this.feedback!.timeopen || 0) * 1000; + this.overview.openTimeReadable = this.overview.timeopen ? CoreTimeUtils.userDate(this.overview.timeopen) : ''; + this.overview.timeclose = (this.feedback!.timeclose || 0) * 1000; + this.overview.closeTimeReadable = this.overview.timeclose ? CoreTimeUtils.userDate(this.overview.timeclose) : ''; + } + if (this.access!.canviewanalysis) { + // Get groups (only for teachers). + promises.push(this.fetchGroupInfo(this.module.id)); + } + + try { + await Promise.all(promises); + } finally { + this.tabsLoaded.overview = true; + } + } + + /** + * Convenience function to get feedback analysis data. + * + * @param accessData Retrieved access data. + * @return Resolved when done. + */ + protected async fetchFeedbackAnalysisData(): Promise { + try { + if (this.access!.canviewanalysis) { + // Get groups (only for teachers). + await this.fetchGroupInfo(this.module.id); + } else { + this.tabChanged('overview'); + } + + } finally { + this.tabsLoaded.analysis = true; + } + } + + /** + * Fetch Group info data. + * + * @param cmId Course module ID. + * @return Resolved when done. + */ + protected async fetchGroupInfo(cmId: number): Promise { + this.groupInfo = await CoreGroups.getActivityGroupInfo(cmId); + + await this.setGroup(CoreGroups.validateGroupId(this.group, this.groupInfo)); + } + + /** + * Parse the analysis info to show the info correctly formatted. + * + * @param item Item to parse. + * @return Parsed item. + */ + protected parseAnalysisInfo(item: AddonModFeedbackItem): AddonModFeedbackItem { + switch (item.typ) { + case 'numeric': + item.average = item.data.reduce((prev, current) => prev + Number(current), 0) / item.data.length; + item.templateName = 'numeric'; + break; + + case 'info': + item.data = item.data.map((dataItem) => { + const parsed = > CoreTextUtils.parseJSON(dataItem); + + return typeof parsed.show != 'undefined' ? parsed.show : false; + }).filter((dataItem) => dataItem); // Filter false entries. + + case 'textfield': + case 'textarea': + item.templateName = 'list'; + break; + + case 'multichoicerated': + case 'multichoice': { + const parsedData = []> item.data.map((dataItem) => { + const parsed = > CoreTextUtils.parseJSON(dataItem); + + return typeof parsed.answertext != 'undefined' ? parsed : false; + }).filter((dataItem) => dataItem); // Filter false entries. + + // Format labels. + item.labels = parsedData.map((dataItem) => { + dataItem.quotient = ( dataItem.quotient * 100).toFixed(2); + let label = ''; + + if (typeof dataItem.value != 'undefined') { + label = '(' + dataItem.value + ') '; + } + label += dataItem.answertext; + label += Number(dataItem.quotient) > 0 ? ' (' + dataItem.quotient + '%)' : ''; + + return label; + }); + + item.chartData = parsedData.map((dataItem) => dataItem.answercount); + + if (item.typ == 'multichoicerated') { + item.average = parsedData.reduce((prev, current) => prev + Number(current.avg), 0.0); + } + + const subtype = item.presentation.charAt(0); + + const single = subtype != 'c'; + item.chartType = single ? 'doughnut' : 'bar'; + item.templateName = 'chart'; + break; + } + + default: + break; + } + + return item; + } + + /** + * Function to go to the questions form. + * + * @param preview Preview or edit the form. + */ + gotoAnswerQuestions(preview: boolean = false): void { + CoreNavigator.navigateToSitePath( + AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.module.id}/form`, + { + params: { + preview, + }, + }, + ); + } + + /** + * User entered the page that contains the component. + */ + ionViewDidEnter(): void { + super.ionViewDidEnter(); + + this.tabsComponent?.ionViewDidEnter(); + } + + /** + * User left the page that contains the component. + */ + ionViewDidLeave(): void { + super.ionViewDidLeave(); + + this.tabsComponent?.ionViewDidLeave(); + } + + /** + * Open non respondents page. + */ + openNonRespondents(): void { + CoreNavigator.navigateToSitePath( + AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.module.id}/nonrespondents`, + { + params: { + group: this.group, + }, + }, + ); + } + + /** + * Open respondents page. + */ + openRespondents(): void { + if (!this.access!.canviewreports || this.completedCount <= 0) { + return; + } + + CoreNavigator.navigateToSitePath( + AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.module.id}/respondents`, + { + params: { + group: this.group, + }, + }, + ); + } + + /** + * Tab changed, fetch content again. + * + * @param tabName New tab name. + */ + tabChanged(tabName: string): void { + this.tab = tabName; + + if (!this.tabsLoaded[this.tab]) { + this.loadContent(false, false, true); + } + } + + /** + * Set group to see the analysis. + * + * @param groupId Group ID. + * @return Resolved when done. + */ + async setGroup(groupId: number): Promise { + this.group = groupId; + + const analysis = await AddonModFeedback.getAnalysis(this.feedback!.id, { groupId, cmId: this.module.id }); + + this.completedCount = analysis.completedcount; + this.itemsCount = analysis.itemscount; + + if (this.tab == 'analysis') { + let num = 1; + + this.items = analysis.itemsdata.map((itemData) => { + const item: AddonModFeedbackItem = Object.assign(itemData.item, { + data: itemData.data, + num: num++, + }); + + // Move data inside item. + if (item.data && item.data.length) { + return this.parseAnalysisInfo(item); + } + + return false; + }).filter((item) => item); + + this.warning = ''; + if (analysis.warnings?.length) { + const warning = analysis.warnings.find((warning) => warning.warningcode == 'insufficientresponsesforthisgroup'); + this.warning = warning?.message; + } + } + } + + /** + * @inheritdoc + */ + protected sync(): Promise { + return AddonModFeedbackSync.syncFeedback(this.feedback!.id); + } + + /** + * @inheritdoc + */ + protected hasSyncSucceed(result: AddonModFeedbackSyncResult): boolean { + return result.updated; + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + super.ngOnDestroy(); + this.submitObserver?.off(); + } + +} + +type AddonModFeedbackItem = AddonModFeedbackWSItem & { + data: string[]; + num: number; + templateName?: string; + average?: number; + labels?: string[]; + chartData?: number[]; + chartType?: string; +}; diff --git a/src/addons/mod/feedback/feedback-lazy.module.ts b/src/addons/mod/feedback/feedback-lazy.module.ts new file mode 100644 index 000000000..9c15c101a --- /dev/null +++ b/src/addons/mod/feedback/feedback-lazy.module.ts @@ -0,0 +1,82 @@ +// (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 { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CoreSharedModule } from '@/core/shared.module'; +import { AddonModFeedbackComponentsModule } from './components/components.module'; +import { AddonModFeedbackIndexPage } from './pages/index/index'; +import { AddonModFeedbackRespondentsPage } from './pages/respondents/respondents'; +import { conditionalRoutes } from '@/app/app-routing.module'; +import { CoreScreen } from '@services/screen'; + +const commonRoutes: Routes = [ + { + path: ':courseId/:cmId', + component: AddonModFeedbackIndexPage, + }, + { + path: ':courseId/:cmId/form', + loadChildren: () => import('./pages/form/form.module').then(m => m.AddonModFeedbackFormPageModule), + }, + { + path: ':courseId/:cmId/nonrespondents', + loadChildren: () => import('./pages/nonrespondents/nonrespondents.module') + .then(m => m.AddonModFeedbackNonRespondentsPageModule), + }, +]; + +const mobileRoutes: Routes = [ + ...commonRoutes, + { + path: ':courseId/:cmId/respondents', + component: AddonModFeedbackRespondentsPage, + }, + { + path: ':courseId/:cmId/attempt/:attemptId', + loadChildren: () => import('./pages/attempt/attempt.module').then(m => m.AddonModFeedbackAttemptPageModule), + }, +]; + +const tabletRoutes: Routes = [ + ...commonRoutes, + { + path: ':courseId/:cmId/respondents', + component: AddonModFeedbackRespondentsPage, + children: [ + { + path: 'attempt/:attemptId', + loadChildren: () => import('./pages/attempt/attempt.module').then(m => m.AddonModFeedbackAttemptPageModule), + }, + ], + }, +]; + +const routes: Routes = [ + ...conditionalRoutes(mobileRoutes, () => CoreScreen.isMobile), + ...conditionalRoutes(tabletRoutes, () => CoreScreen.isTablet), +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + AddonModFeedbackComponentsModule, + ], + declarations: [ + AddonModFeedbackIndexPage, + AddonModFeedbackRespondentsPage, + ], +}) +export class AddonModFeedbackLazyModule {} diff --git a/src/addons/mod/feedback/feedback.module.ts b/src/addons/mod/feedback/feedback.module.ts new file mode 100644 index 000000000..d502a3afc --- /dev/null +++ b/src/addons/mod/feedback/feedback.module.ts @@ -0,0 +1,87 @@ +// (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 { APP_INITIALIZER, NgModule, Type } from '@angular/core'; +import { Routes } from '@angular/router'; +import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate'; +import { CoreCronDelegate } from '@services/cron'; +import { CORE_SITE_SCHEMAS } from '@services/sites'; +import { AddonModFeedbackComponentsModule } from './components/components.module'; +import { OFFLINE_SITE_SCHEMA } from './services/database/feedback'; +import { AddonModFeedbackProvider } from './services/feedback'; +import { AddonModFeedbackHelperProvider } from './services/feedback-helper'; +import { AddonModFeedbackOfflineProvider } from './services/feedback-offline'; +import { AddonModFeedbackSyncProvider } from './services/feedback-sync'; +import { AddonModFeedbackAnalysisLinkHandler } from './services/handlers/analysis-link'; +import { AddonModFeedbackCompleteLinkHandler } from './services/handlers/complete-link'; +import { AddonModFeedbackIndexLinkHandler } from './services/handlers/index-link'; +import { AddonModFeedbackListLinkHandler } from './services/handlers/list-link'; +import { AddonModFeedbackModuleHandlerService, AddonModFeedbackModuleHandler } from './services/handlers/module'; +import { AddonModFeedbackPrefetchHandler } from './services/handlers/prefetch'; +import { AddonModFeedbackPrintLinkHandler } from './services/handlers/print-link'; +import { AddonModFeedbackPushClickHandler } from './services/handlers/push-click'; +import { AddonModFeedbackShowEntriesLinkHandler } from './services/handlers/show-entries-link'; +import { AddonModFeedbackShowNonRespondentsLinkHandler } from './services/handlers/show-non-respondents-link'; +import { AddonModFeedbackSyncCronHandler } from './services/handlers/sync-cron'; + +export const ADDON_MOD_FEEDBACK_SERVICES: Type[] = [ + AddonModFeedbackProvider, + AddonModFeedbackOfflineProvider, + AddonModFeedbackHelperProvider, + AddonModFeedbackSyncProvider, +]; + +const routes: Routes = [ + { + path: AddonModFeedbackModuleHandlerService.PAGE_NAME, + loadChildren: () => import('./feedback-lazy.module').then(m => m.AddonModFeedbackLazyModule), + }, +]; + +@NgModule({ + imports: [ + CoreMainMenuTabRoutingModule.forChild(routes), + AddonModFeedbackComponentsModule, + ], + providers: [ + { + provide: CORE_SITE_SCHEMAS, + useValue: [OFFLINE_SITE_SCHEMA], + multi: true, + }, + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => () => { + CoreCourseModuleDelegate.registerHandler(AddonModFeedbackModuleHandler.instance); + CoreCourseModulePrefetchDelegate.registerHandler(AddonModFeedbackPrefetchHandler.instance); + CoreCronDelegate.register(AddonModFeedbackSyncCronHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModFeedbackIndexLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModFeedbackListLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModFeedbackAnalysisLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModFeedbackCompleteLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModFeedbackPrintLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModFeedbackShowEntriesLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModFeedbackShowNonRespondentsLinkHandler.instance); + CorePushNotificationsDelegate.registerClickHandler(AddonModFeedbackPushClickHandler.instance); + }, + }, + ], +}) +export class AddonModFeedbackModule {} diff --git a/src/addons/mod/feedback/lang.json b/src/addons/mod/feedback/lang.json new file mode 100644 index 000000000..5e6381eb3 --- /dev/null +++ b/src/addons/mod/feedback/lang.json @@ -0,0 +1,38 @@ +{ + "analysis": "Analysis", + "anonymous": "Anonymous", + "anonymous_entries": "Anonymous entries ({{$a}})", + "average": "Average", + "captchaofflinewarning": "Feedback with CAPTCHA cannot be completed offline, or if not configured, or if the server is down.", + "complete_the_form": "Answer the questions", + "completed_feedbacks": "Submitted answers", + "continue_the_form": "Continue answering the questions", + "feedback_is_not_open": "The feedback is not open", + "feedback_submitted_offline": "This feedback has been saved to be submitted later.", + "feedbackclose": "Allow answers to", + "feedbackopen": "Allow answers from", + "mapcourses": "Map feedback to courses", + "maximal": "Maximum", + "minimal": "Minimum", + "mode": "Mode", + "modulenameplural": "Feedback", + "next_page": "Next page", + "non_anonymous": "User's name will be logged and shown with answers", + "non_anonymous_entries": "Non anonymous entries ({{$a}})", + "non_respondents_students": "Non-respondent students ({{$a}})", + "not_selected": "Not selected", + "not_started": "Not started", + "numberoutofrange": "Number out of range", + "overview": "Overview", + "page_after_submit": "Completion message", + "preview": "Preview", + "previous_page": "Previous page", + "questions": "Questions", + "response_nr": "Response number", + "responses": "Responses", + "save_entries": "Submit your answers", + "show_entries": "Show responses", + "show_nonrespondents": "Show non-respondents", + "started": "Started", + "this_feedback_is_already_submitted": "You've already completed this activity." +} \ No newline at end of file diff --git a/src/addons/mod/feedback/pages/attempt/attempt.html b/src/addons/mod/feedback/pages/attempt/attempt.html new file mode 100644 index 000000000..aa1c35db7 --- /dev/null +++ b/src/addons/mod/feedback/pages/attempt/attempt.html @@ -0,0 +1,58 @@ + + + + + + + {{ attempt.fullname }} + + {{ 'addon.mod_feedback.response_nr' |translate }}: {{anonAttempt.number}} + + + + + + + + + + +

{{attempt.fullname}}

+

{{attempt.timemodified * 1000 | coreFormatDate }}

+
+
+ + + +

+ {{ 'addon.mod_feedback.response_nr' |translate }}: {{anonAttempt.number}} + ({{ 'addon.mod_feedback.anonymous' |translate }}) +

+
+
+ + + + + + + +

+ {{item.itemnumber}}. + + +

+

+ + +

+
+
+
+
+
+
+
diff --git a/src/addons/mod/feedback/pages/attempt/attempt.module.ts b/src/addons/mod/feedback/pages/attempt/attempt.module.ts new file mode 100644 index 000000000..788d1664a --- /dev/null +++ b/src/addons/mod/feedback/pages/attempt/attempt.module.ts @@ -0,0 +1,37 @@ +// (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 { CoreSharedModule } from '@/core/shared.module'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AddonModFeedbackAttemptPage } from './attempt'; + +const routes: Routes = [ + { + path: '', + component: AddonModFeedbackAttemptPage, + }, +]; + +@NgModule({ + declarations: [ + AddonModFeedbackAttemptPage, + ], + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + ], + exports: [RouterModule], +}) +export class AddonModFeedbackAttemptPageModule {} diff --git a/src/addons/mod/feedback/pages/attempt/attempt.ts b/src/addons/mod/feedback/pages/attempt/attempt.ts new file mode 100644 index 000000000..52e9736fe --- /dev/null +++ b/src/addons/mod/feedback/pages/attempt/attempt.ts @@ -0,0 +1,125 @@ +// (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 { Component, OnInit } from '@angular/core'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreTextUtils } from '@services/utils/text'; +import { + AddonModFeedback, + AddonModFeedbackProvider, + AddonModFeedbackWSAnonAttempt, + AddonModFeedbackWSAttempt, + AddonModFeedbackWSFeedback, +} from '../../services/feedback'; +import { AddonModFeedbackFormItem, AddonModFeedbackHelper } from '../../services/feedback-helper'; + +/** + * Page that displays a feedback attempt review. + */ +@Component({ + selector: 'page-addon-mod-feedback-attempt', + templateUrl: 'attempt.html', +}) +export class AddonModFeedbackAttemptPage implements OnInit { + + protected attemptId!: number; + + cmId!: number; + courseId!: number; + feedback?: AddonModFeedbackWSFeedback; + attempt?: AddonModFeedbackWSAttempt; + anonAttempt?: AddonModFeedbackWSAnonAttempt; + items: AddonModFeedbackAttemptItem[] = []; + component = AddonModFeedbackProvider.COMPONENT; + loaded = false; + + /** + * @inheritdoc + */ + ngOnInit(): void { + this.cmId = CoreNavigator.getRouteNumberParam('cmId')!; + this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; + this.attemptId = CoreNavigator.getRouteNumberParam('attemptId')!; + + this.fetchData(); + } + + /** + * Fetch all the data required for the view. + * + * @return Promise resolved when done. + */ + protected async fetchData(): Promise { + try { + this.feedback = await AddonModFeedback.getFeedback(this.courseId, this.cmId); + + const attempt = await AddonModFeedback.getAttempt(this.feedback.id, this.attemptId, { cmId: this.cmId }); + + if (this.isAnonAttempt(attempt)) { + this.anonAttempt = attempt; + delete this.attempt; + } else { + this.attempt = attempt; + delete this.anonAttempt; + } + + const items = await AddonModFeedback.getItems(this.feedback.id, { cmId: this.cmId }); + + // Add responses and format items. + this.items = items.items.map((item) => { + const formItem = AddonModFeedbackHelper.getItemForm(item, true); + if (!formItem) { + return; + } + + const attemptItem = formItem; + + if (item.typ == 'label') { + attemptItem.submittedValue = CoreTextUtils.replacePluginfileUrls(item.presentation, item.itemfiles); + } else { + for (const x in attempt.responses) { + if (attempt.responses[x].id == item.id) { + attemptItem.submittedValue = attempt.responses[x].printval; + break; + } + } + } + + return attemptItem; + }).filter((itemData) => itemData); // Filter items with errors. + + } catch (message) { + // Some call failed on fetch, go back. + CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); + CoreNavigator.back(); + } finally { + this.loaded = true; + } + } + + /** + * Check if an attempt is anonymous or not. + * + * @param attempt Attempt to check. + */ + isAnonAttempt(attempt: AddonModFeedbackWSAttempt | AddonModFeedbackWSAnonAttempt): attempt is AddonModFeedbackWSAnonAttempt { + return !('fullname' in attempt); + } + +} + +type AddonModFeedbackAttemptItem = AddonModFeedbackFormItem & { + submittedValue?: string; +}; diff --git a/src/addons/mod/feedback/pages/form/form.html b/src/addons/mod/feedback/pages/form/form.html new file mode 100644 index 000000000..c96b3c5a7 --- /dev/null +++ b/src/addons/mod/feedback/pages/form/form.html @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + +

{{ 'addon.mod_feedback.mode' | translate }}

+

{{ 'addon.mod_feedback.anonymous' | translate }}

+

{{ 'addon.mod_feedback.non_anonymous' | translate }}

+
+
+ + + + + + + +

+ {{item.itemnumber}}. + + + {{item.postfix}} +

+

+ + +

+
+ + + + + + + + + {{ 'addon.mod_feedback.numberoutofrange' | translate }} [{{item.rangefrom}} + , {{item.rangeto}}] + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + {{ 'addon.mod_feedback.captchaofflinewarning' | translate }} + +
+
+
+
+ + + + + + {{ 'addon.mod_feedback.previous_page' | translate }} + + + + + {{ 'addon.mod_feedback.next_page' | translate }} + + + + + + {{ 'addon.mod_feedback.save_entries' | translate }} + + + + +
+
+ + + + + +

+ {{ 'addon.mod_feedback.this_feedback_is_already_submitted' | translate }} +

+

+ {{ 'addon.mod_feedback.feedback_submitted_offline' | translate }} +

+

+ + +

+
+
+
+ + + + + + + + {{ 'addon.mod_feedback.completed_feedbacks' | translate }} + + + + + {{ 'core.continue' | translate }} + + + + + + +
+
diff --git a/src/addons/mod/feedback/pages/form/form.module.ts b/src/addons/mod/feedback/pages/form/form.module.ts new file mode 100644 index 000000000..9dd8eea72 --- /dev/null +++ b/src/addons/mod/feedback/pages/form/form.module.ts @@ -0,0 +1,39 @@ +// (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 { CoreSharedModule } from '@/core/shared.module'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CanLeaveGuard } from '@guards/can-leave'; +import { AddonModFeedbackFormPage } from './form'; + +const routes: Routes = [ + { + path: '', + component: AddonModFeedbackFormPage, + canDeactivate: [CanLeaveGuard], + }, +]; + +@NgModule({ + declarations: [ + AddonModFeedbackFormPage, + ], + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + ], + exports: [RouterModule], +}) +export class AddonModFeedbackFormPageModule {} diff --git a/src/addons/mod/feedback/pages/form/form.scss b/src/addons/mod/feedback/pages/form/form.scss new file mode 100644 index 000000000..b64a532ef --- /dev/null +++ b/src/addons/mod/feedback/pages/form/form.scss @@ -0,0 +1,11 @@ +:host { + .addon-mod_feedback-item ion-label.label-stacked { + margin: 11px 0px 10px; + transform: none; + } + + .addon-mod_feedback-item-error { + padding-top: 5px; + padding-bottom: 8px; + } +} diff --git a/src/addons/mod/feedback/pages/form/form.ts b/src/addons/mod/feedback/pages/form/form.ts new file mode 100644 index 000000000..306ccb2c7 --- /dev/null +++ b/src/addons/mod/feedback/pages/form/form.ts @@ -0,0 +1,428 @@ +// (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 { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { CoreSite } from '@classes/site'; +import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; +import { CoreCourse, CoreCourseCommonModWSOptions, CoreCourseWSModule } from '@features/course/services/course'; +import { CoreCourseHelper } from '@features/course/services/course-helper'; +import { CanLeave } from '@guards/can-leave'; +import { IonContent } from '@ionic/angular'; +import { CoreApp } from '@services/app'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { Network, NgZone, Translate } from '@singletons'; +import { CoreEvents } from '@singletons/events'; +import { Subscription } from 'rxjs'; +import { + AddonModFeedback, + AddonModFeedbackGetFeedbackAccessInformationWSResponse, + AddonModFeedbackPageItems, + AddonModFeedbackProvider, + AddonModFeedbackResponseValue, + AddonModFeedbackWSFeedback, +} from '../../services/feedback'; +import { AddonModFeedbackFormItem, AddonModFeedbackHelper } from '../../services/feedback-helper'; +import { AddonModFeedbackSync } from '../../services/feedback-sync'; +import { AddonModFeedbackModuleHandlerService } from '../../services/handlers/module'; + +/** + * Page that displays feedback form. + */ +@Component({ + selector: 'page-addon-mod-feedback-form', + templateUrl: 'form.html', + styleUrls: ['form.scss'], +}) +export class AddonModFeedbackFormPage implements OnInit, OnDestroy, CanLeave { + + @ViewChild(IonContent) content?: IonContent; + + protected module?: CoreCourseWSModule; + protected currentPage?: number; + protected siteAfterSubmit?: string; + protected onlineObserver: Subscription; + protected originalData?: Record; + protected currentSite: CoreSite; + protected forceLeave = false; + + title?: string; + preview = false; + cmId!: number; + courseId!: number; + feedback?: AddonModFeedbackWSFeedback; + completionPageContents?: string; + component = AddonModFeedbackProvider.COMPONENT; + offline = false; + feedbackLoaded = false; + access?: AddonModFeedbackGetFeedbackAccessInformationWSResponse; + items: AddonModFeedbackFormItem[] = []; + hasPrevPage = false; + hasNextPage = false; + completed = false; + completedOffline = false; + + constructor() { + this.currentSite = CoreSites.getCurrentSite()!; + + // Refresh online status when changes. + this.onlineObserver = Network.onChange().subscribe(() => { + // Execute the callback in the Angular zone, so change detection doesn't stop working. + NgZone.run(() => { + this.offline = !CoreApp.isOnline(); + }); + }); + } + + /** + * @inheritdoc + */ + async ngOnInit(): Promise { + this.cmId = CoreNavigator.getRouteNumberParam('cmId')!; + this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; + this.currentPage = CoreNavigator.getRouteNumberParam('page'); + this.title = CoreNavigator.getRouteParam('title'); + this.preview = !!CoreNavigator.getRouteBooleanParam('preview'); + + await this.fetchData(); + + if (!this.feedback) { + return; + } + + try { + await AddonModFeedback.logView(this.feedback.id, this.feedback.name, true); + + CoreCourse.checkModuleCompletion(this.courseId, this.module!.completiondata); + } catch { + // Ignore errors. + } + } + + /** + * View entered. + */ + ionViewDidEnter(): void { + this.forceLeave = false; + } + + /** + * @inheritdoc + */ + async canLeave(): Promise { + if (this.forceLeave) { + return true; + } + + if (!this.preview) { + const responses = AddonModFeedbackHelper.getPageItemsResponses(this.items); + + if (this.items && !this.completed && this.originalData) { + // Form submitted. Check if there is any change. + if (!CoreUtils.basicLeftCompare(responses, this.originalData, 3)) { + await CoreDomUtils.showConfirm(Translate.instant('core.confirmcanceledit')); + } + } + } + + return true; + } + + /** + * Fetch all the data required for the view. + * + * @return Promise resolved when done. + */ + protected async fetchData(): Promise { + try { + this.module = await CoreCourse.getModule(this.cmId, this.courseId, undefined, true, false, this.currentSite.getId()); + + this.offline = !CoreApp.isOnline(); + const options = { + cmId: this.cmId, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + siteId: this.currentSite.getId(), + }; + + this.feedback = await AddonModFeedback.getFeedback(this.courseId, this.cmId); + + this.title = this.feedback.name || this.title; + + await this.fetchAccessData(options); + + let page = 0; + + if (!this.preview && this.access!.cansubmit && !this.access!.isempty) { + page = this.currentPage ?? await this.fetchResumePage(options); + } else { + this.preview = true; + } + + await this.fetchFeedbackPageData(page); + } catch (message) { + CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); + this.forceLeave = true; + CoreNavigator.back(); + } finally { + this.feedbackLoaded = true; + } + } + + /** + * Fetch access information. + * + * @param options Options. + * @return Promise resolved when done. + */ + protected async fetchAccessData(options: CoreCourseCommonModWSOptions): Promise { + try { + this.access = await AddonModFeedback.getFeedbackAccessInformation(this.feedback!.id, options); + } catch (error) { + if (this.offline || CoreUtils.isWebServiceError(error)) { + // Already offline or shouldn't go offline, fail. + throw error; + } + + // If it fails, go offline. + this.offline = true; + options.readingStrategy = CoreSitesReadingStrategy.PreferCache; + + this.access = await AddonModFeedback.getFeedbackAccessInformation(this.feedback!.id, options); + } + } + + /** + * Get resume page from WS. + * + * @param options Options. + * @return Promise resolved with the page to resume. + */ + protected async fetchResumePage(options: CoreCourseCommonModWSOptions): Promise { + try { + return await AddonModFeedback.getResumePage(this.feedback!.id, options); + } catch (error) { + if (this.offline || CoreUtils.isWebServiceError(error)) { + // Already offline or shouldn't go offline, fail. + throw error; + } + + // Go offline. + this.offline = true; + options.readingStrategy = CoreSitesReadingStrategy.PreferCache; + + return AddonModFeedback.getResumePage(this.feedback!.id, options); + } + } + + /** + * Fetch page data. + * + * @param page Page to load. + * @return Promise resolved when done. + */ + protected async fetchFeedbackPageData(page: number = 0): Promise { + this.items = []; + const response = await this.fetchPageItems(page); + + this.items = response.items + .map((itemData) => AddonModFeedbackHelper.getItemForm(itemData, this.preview)) + .filter((itemData) => itemData); // Filter items with errors. + + if (!this.preview) { + const itemsCopy = CoreUtils.clone(this.items); // Copy the array to avoid modifications. + this.originalData = AddonModFeedbackHelper.getPageItemsResponses(itemsCopy); + } + } + + /** + * Fetch page items. + * + * @param page Page to get. + * @return Promise resolved with WS response. + */ + protected async fetchPageItems(page: number): Promise { + const options = { + cmId: this.cmId, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + siteId: this.currentSite.getId(), + }; + + if (this.preview) { + const response = await AddonModFeedback.getItems(this.feedback!.id, options); + + return { + items: response.items, + warnings: response.warnings, + hasnextpage: false, + hasprevpage: false, + }; + } + + this.currentPage = page; + let response: AddonModFeedbackPageItems; + + try { + response = await AddonModFeedback.getPageItemsWithValues(this.feedback!.id, page, options); + } catch (error) { + if (this.offline || CoreUtils.isWebServiceError(error)) { + // Already offline or shouldn't go offline, fail. + throw error; + } + + // Go offline. + this.offline = true; + options.readingStrategy = CoreSitesReadingStrategy.PreferCache; + + response = await AddonModFeedback.getPageItemsWithValues(this.feedback!.id, page, options); + } + + this.hasPrevPage = !!response.hasprevpage; + this.hasNextPage = !!response.hasnextpage; + + return response; + } + + /** + * Function to allow page navigation through the questions form. + * + * @param goPrevious If true it will go back to the previous page, if false, it will go forward. + * @return Resolved when done. + */ + async gotoPage(goPrevious: boolean): Promise { + this.content?.scrollToTop(); + this.feedbackLoaded = false; + + const responses = AddonModFeedbackHelper.getPageItemsResponses(this.items); + const formHasErrors = this.items.some((item) => item.isEmpty || item.hasError); + + try { + // Sync other pages first. + await CoreUtils.ignoreErrors(AddonModFeedbackSync.syncFeedback(this.feedback!.id)); + + const response = await AddonModFeedback.processPage(this.feedback!.id, this.currentPage!, responses, { + goPrevious, + formHasErrors, + courseId: this.courseId, + cmId: this.cmId, + }); + + if (response.completed) { + // Form is completed, show completion message and buttons. + this.items = []; + this.completed = true; + this.completedOffline = !!response.offline; + this.completionPageContents = response.completionpagecontents; + this.siteAfterSubmit = response.siteaftersubmit; + + CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'feedback' }); + + // Invalidate access information so user will see home page updated (continue form or completion messages). + await Promise.all([ + AddonModFeedback.invalidateFeedbackAccessInformationData(this.feedback!.id), + AddonModFeedback.invalidateResumePageData(this.feedback!.id), + ]); + + // If form has been submitted, the info has been already invalidated but we should update index view. + CoreEvents.trigger(AddonModFeedbackProvider.FORM_SUBMITTED, { + feedbackId: this.feedback!.id, + tab: 'overview', + offline: this.completedOffline, + }); + + await this.fetchAccessData({ + cmId: this.cmId, + readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork, + siteId: this.currentSite.getId(), + }); + } else if (typeof response.jumpto != 'number' || response.jumpto == this.currentPage) { + // Errors on questions, stay in page. + } else { + // Invalidate access information so user will see home page updated (continue form). + await AddonModFeedback.invalidateResumePageData(this.feedback!.id); + + CoreEvents.trigger(AddonModFeedbackProvider.FORM_SUBMITTED, { + feedbackId: this.feedback!.id, + tab: 'overview', + offline: this.completedOffline, + }); + + // Fetch the new page. + await this.fetchFeedbackPageData(response.jumpto); + } + } catch (message) { + CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); + } finally { + this.feedbackLoaded = true; + } + } + + /** + * Function to link implemented features. + */ + showAnalysis(): void { + const indexPath = AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${this.courseId}/${this.cmId}`; + const previousPath = CoreNavigator.getPreviousPath(); + + if (previousPath.match(new RegExp(indexPath + '$'))) { + // Previous page is the index page, go back. + CoreEvents.trigger(AddonModFeedbackProvider.FORM_SUBMITTED, { + feedbackId: this.feedback!.id, + tab: 'analysis', + offline: this.completedOffline, + }); + + CoreNavigator.back(); + + return; + } + + CoreNavigator.navigateToSitePath(indexPath, { + params: { + module: this.module, + tab: 'analysis', + }, + }); + } + + /** + * Function to go to the page after submit. + */ + async continue(): Promise { + if (!this.siteAfterSubmit) { + return CoreCourseHelper.getAndOpenCourse(this.courseId, {}, this.currentSite.getId()); + } + + const modal = await CoreDomUtils.showModalLoading(); + + try { + const treated = await CoreContentLinksHelper.handleLink(this.siteAfterSubmit); + + if (!treated) { + await this.currentSite.openInBrowserWithAutoLoginIfSameSite(this.siteAfterSubmit); + } + } finally { + modal.dismiss(); + } + } + + /** + * Component being destroyed. + */ + ngOnDestroy(): void { + this.onlineObserver.unsubscribe(); + } + +} diff --git a/src/addons/mod/feedback/pages/index/index.html b/src/addons/mod/feedback/pages/index/index.html new file mode 100644 index 000000000..65b4c69e0 --- /dev/null +++ b/src/addons/mod/feedback/pages/index/index.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/addons/mod/feedback/pages/index/index.ts b/src/addons/mod/feedback/pages/index/index.ts new file mode 100644 index 000000000..a33a9b741 --- /dev/null +++ b/src/addons/mod/feedback/pages/index/index.ts @@ -0,0 +1,43 @@ +// (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 { Component, OnInit, ViewChild } from '@angular/core'; +import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page'; +import { CoreNavigator } from '@services/navigator'; +import { AddonModFeedbackIndexComponent } from '../../components/index/index'; + +/** + * Page that displays a feedback. + */ +@Component({ + selector: 'page-addon-mod-feedback-index', + templateUrl: 'index.html', +}) +export class AddonModFeedbackIndexPage extends CoreCourseModuleMainActivityPage implements OnInit { + + @ViewChild(AddonModFeedbackIndexComponent) activityComponent?: AddonModFeedbackIndexComponent; + + selectedTab?: string; + selectedGroup?: number; + + /** + * Component being initialized. + */ + ngOnInit(): void { + super.ngOnInit(); + this.selectedTab = CoreNavigator.getRouteParam('tab'); + this.selectedGroup = CoreNavigator.getRouteNumberParam('group'); + } + +} diff --git a/src/addons/mod/feedback/pages/nonrespondents/nonrespondents.html b/src/addons/mod/feedback/pages/nonrespondents/nonrespondents.html new file mode 100644 index 000000000..3a83b44c2 --- /dev/null +++ b/src/addons/mod/feedback/pages/nonrespondents/nonrespondents.html @@ -0,0 +1,52 @@ + + + + + + {{ 'addon.mod_feedback.responses' |translate }} + + + + + + + + + + + {{'core.groupsseparate' | translate }} + {{'core.groupsvisible' | translate }} + + + + {{groupOpt.name}} + + + + + + {{ 'addon.mod_feedback.non_respondents_students' | translate : {$a: total } }} + + + + + +

{{ user.fullname }}

+

+ + {{ 'addon.mod_feedback.started' | translate}} + + + {{ 'addon.mod_feedback.not_started' | translate}} + +

+
+
+
+ + + +
+
+
diff --git a/src/addons/mod/feedback/pages/nonrespondents/nonrespondents.module.ts b/src/addons/mod/feedback/pages/nonrespondents/nonrespondents.module.ts new file mode 100644 index 000000000..654d85141 --- /dev/null +++ b/src/addons/mod/feedback/pages/nonrespondents/nonrespondents.module.ts @@ -0,0 +1,37 @@ +// (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 { CoreSharedModule } from '@/core/shared.module'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AddonModFeedbackNonRespondentsPage } from './nonrespondents'; + +const routes: Routes = [ + { + path: '', + component: AddonModFeedbackNonRespondentsPage, + }, +]; + +@NgModule({ + declarations: [ + AddonModFeedbackNonRespondentsPage, + ], + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + ], + exports: [RouterModule], +}) +export class AddonModFeedbackNonRespondentsPageModule {} diff --git a/src/addons/mod/feedback/pages/nonrespondents/nonrespondents.ts b/src/addons/mod/feedback/pages/nonrespondents/nonrespondents.ts new file mode 100644 index 000000000..d95111480 --- /dev/null +++ b/src/addons/mod/feedback/pages/nonrespondents/nonrespondents.ts @@ -0,0 +1,165 @@ +// (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 { Component, OnInit } from '@angular/core'; +import { IonRefresher } from '@ionic/angular'; +import { CoreGroupInfo, CoreGroups } from '@services/groups'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { AddonModFeedback, AddonModFeedbackWSFeedback } from '../../services/feedback'; +import { AddonModFeedbackHelper, AddonModFeedbackNonRespondent } from '../../services/feedback-helper'; + +/** + * Page that displays feedback non respondents. + */ +@Component({ + selector: 'page-addon-mod-feedback-nonrespondents', + templateUrl: 'nonrespondents.html', +}) +export class AddonModFeedbackNonRespondentsPage implements OnInit { + + protected cmId!: number; + protected courseId!: number; + protected feedback?: AddonModFeedbackWSFeedback; + protected page = 0; + + selectedGroup!: number; + groupInfo?: CoreGroupInfo; + users: AddonModFeedbackNonRespondent[] = []; + total = 0; + canLoadMore = false; + loaded = false; + loadMoreError = false; + + /** + * @inheritdoc + */ + ngOnInit(): void { + this.cmId = CoreNavigator.getRouteNumberParam('cmId')!; + this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; + this.selectedGroup = CoreNavigator.getRouteNumberParam('group') || 0; + + this.fetchData(); + } + + /** + * Fetch all the data required for the view. + * + * @param refresh Empty events array first. + * @return Promise resolved when done. + */ + protected async fetchData(refresh: boolean = false): Promise { + this.page = 0; + this.total = 0; + this.users = []; + + try { + this.feedback = await AddonModFeedback.getFeedback(this.courseId, this.cmId); + + this.groupInfo = await CoreGroups.getActivityGroupInfo(this.cmId); + + this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo); + + await this.loadGroupUsers(this.selectedGroup); + } catch (message) { + CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true); + + if (!refresh) { + // Some call failed on first fetch, go back. + CoreNavigator.back(); + } + } + } + + /** + * Load Group responses. + * + * @param groupId If defined it will change group if not, it will load more users for the same group. + * @return Promise resolved when done. + */ + protected async loadGroupUsers(groupId?: number): Promise { + this.loadMoreError = false; + + if (typeof groupId == 'undefined') { + this.page++; + } else { + this.selectedGroup = groupId; + this.page = 0; + this.total = 0; + this.users = []; + this.loaded = false; + } + + try { + const response = await AddonModFeedbackHelper.getNonRespondents(this.feedback!.id, { + groupId: this.selectedGroup, + page: this.page, + cmId: this.cmId, + }); + + this.total = response.total; + if (this.users.length < response.total) { + this.users = this.users.concat(response.users); + } + + this.canLoadMore = this.users.length < response.total; + } catch (error) { + this.loadMoreError = true; + + throw error; + } finally { + this.loaded = true; + } + } + + /** + * Change selected group or load more users. + * + * @param groupId Group ID selected. If not defined, it will load more users. + * @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading. + */ + async loadAttempts(groupId?: number, infiniteComplete?: () => void): Promise { + try { + await this.loadGroupUsers(groupId); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); + } finally { + infiniteComplete && infiniteComplete(); + } + } + + /** + * Refresh the attempts. + * + * @param refresher Refresher. + */ + async refreshFeedback(refresher: IonRefresher): Promise { + try { + const promises: Promise[] = []; + + promises.push(CoreGroups.invalidateActivityGroupInfo(this.cmId)); + if (this.feedback) { + promises.push(AddonModFeedback.invalidateNonRespondentsData(this.feedback.id)); + } + + await CoreUtils.ignoreErrors(Promise.all(promises)); + + await this.fetchData(true); + } finally { + refresher.complete(); + } + } + +} diff --git a/src/addons/mod/feedback/pages/respondents/respondents.html b/src/addons/mod/feedback/pages/respondents/respondents.html new file mode 100644 index 000000000..33b20567b --- /dev/null +++ b/src/addons/mod/feedback/pages/respondents/respondents.html @@ -0,0 +1,79 @@ + + + + + + {{ 'addon.mod_feedback.responses' |translate }} + + + + + + + + + + + + {{'core.groupsseparate' | translate }} + {{'core.groupsvisible' | translate }} + + + + {{groupOpt.name}} + + + + + + + + {{ 'addon.mod_feedback.non_anonymous_entries' | translate : {$a: responses.responses.total } }} + + + + + +

{{ attempt.fullname }}

+

{{attempt.timemodified * 1000 | coreFormatDate }}

+
+
+ + + + {{ 'core.loadmore' | translate }} + + + + +
+ + + + + {{ 'addon.mod_feedback.anonymous_entries' |translate : {$a: responses.anonResponses.total } }} + + + + +

{{ 'addon.mod_feedback.response_nr' |translate }}: {{attempt.number}}

+
+
+ + + + {{ 'core.loadmore' | translate }} + + + + +
+
+
+
+
diff --git a/src/addons/mod/feedback/pages/respondents/respondents.ts b/src/addons/mod/feedback/pages/respondents/respondents.ts new file mode 100644 index 000000000..8539b2cb2 --- /dev/null +++ b/src/addons/mod/feedback/pages/respondents/respondents.ts @@ -0,0 +1,248 @@ +// (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 { AfterViewInit, Component, ViewChild } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { CorePageItemsListManager } from '@classes/page-items-list-manager'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { IonRefresher } from '@ionic/angular'; +import { CoreGroupInfo, CoreGroups } from '@services/groups'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreUtils } from '@services/utils/utils'; +import { + AddonModFeedback, + AddonModFeedbackWSAnonAttempt, + AddonModFeedbackWSAttempt, + AddonModFeedbackWSFeedback, +} from '../../services/feedback'; +import { AddonModFeedbackHelper, AddonModFeedbackResponsesAnalysis } from '../../services/feedback-helper'; + +/** + * Page that displays feedback respondents. + */ +@Component({ + selector: 'page-addon-mod-feedback-respondents', + templateUrl: 'respondents.html', +}) +export class AddonModFeedbackRespondentsPage implements AfterViewInit { + + @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; + + protected cmId!: number; + protected courseId!: number; + protected page = 0; + protected feedback?: AddonModFeedbackWSFeedback; + + responses: AddonModFeedbackResponsesManager; + selectedGroup!: number; + groupInfo?: CoreGroupInfo; + loaded = false; + loadingMore = false; + + constructor( + route: ActivatedRoute, + ) { + this.responses = new AddonModFeedbackResponsesManager( + route.component, + ); + } + + /** + * @inheritdoc + */ + async ngAfterViewInit(): Promise { + this.cmId = CoreNavigator.getRouteNumberParam('cmId')!; + this.courseId = CoreNavigator.getRouteNumberParam('courseId')!; + this.selectedGroup = CoreNavigator.getRouteNumberParam('group') || 0; + + await this.fetchData(); + + this.responses.start(this.splitView); + } + + /** + * Fetch all the data required for the view. + * + * @param refresh Empty events array first. + * @return Promise resolved when done. + */ + async fetchData(refresh: boolean = false): Promise { + this.page = 0; + this.responses.resetItems(); + + try { + this.feedback = await AddonModFeedback.getFeedback(this.courseId, this.cmId); + + this.groupInfo = await CoreGroups.getActivityGroupInfo(this.cmId); + + this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo); + + await this.loadGroupAttempts(this.selectedGroup); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); + + if (!refresh) { + // Some call failed on first fetch, go back. + CoreNavigator.back(); + } + } + } + + /** + * Load Group attempts. + * + * @param groupId If defined it will change group if not, it will load more attempts for the same group. + * @return Resolved with the attempts loaded. + */ + protected async loadGroupAttempts(groupId?: number): Promise { + if (typeof groupId == 'undefined') { + this.page++; + this.loadingMore = true; + } else { + this.selectedGroup = groupId; + this.page = 0; + this.responses.resetItems(); + } + + try { + const responses = await AddonModFeedbackHelper.getResponsesAnalysis(this.feedback!.id, { + groupId: this.selectedGroup, + page: this.page, + cmId: this.cmId, + }); + + this.responses.setResponses(responses); + } finally { + this.loadingMore = false; + this.loaded = true; + } + } + + /** + * Change selected group or load more attempts. + * + * @param groupId Group ID selected. If not defined, it will load more attempts. + */ + async loadAttempts(groupId?: number): Promise { + try { + await this.loadGroupAttempts(groupId); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); + } + } + + /** + * Refresh the attempts. + * + * @param refresher Refresher. + */ + async refreshFeedback(refresher: IonRefresher): Promise { + const promises: Promise[] = []; + + promises.push(CoreGroups.invalidateActivityGroupInfo(this.cmId)); + if (this.feedback) { + promises.push(AddonModFeedback.invalidateResponsesAnalysisData(this.feedback.id)); + } + + try { + await CoreUtils.ignoreErrors(Promise.all(promises)); + + await this.fetchData(true); + } finally { + refresher.complete(); + } + } + +} + +/** + * Type of items that can be held by the entries manager. + */ +type EntryItem = AddonModFeedbackWSAttempt | AddonModFeedbackWSAnonAttempt; + +/** + * Entries manager. + */ +class AddonModFeedbackResponsesManager extends CorePageItemsListManager { + + responses: AddonModFeedbackResponses = { + attempts: [], + total: 0, + canLoadMore: false, + }; + + anonResponses: AddonModFeedbackAnonResponses = { + attempts: [], + total: 0, + canLoadMore: false, + }; + + constructor(pageComponent: unknown) { + super(pageComponent); + } + + /** + * Update responses. + * + * @param responses Responses. + */ + setResponses(responses: AddonModFeedbackResponsesAnalysis): void { + this.responses.total = responses.totalattempts; + this.anonResponses.total = responses.totalanonattempts; + + if (this.anonResponses.attempts.length < responses.totalanonattempts) { + this.anonResponses.attempts = this.anonResponses.attempts.concat(responses.anonattempts); + } + if (this.responses.attempts.length < responses.totalattempts) { + this.responses.attempts = this.responses.attempts.concat(responses.attempts); + } + + this.anonResponses.canLoadMore = this.anonResponses.attempts.length < responses.totalanonattempts; + this.responses.canLoadMore = this.responses.attempts.length < responses.totalattempts; + + this.setItems(( this.responses.attempts).concat(this.anonResponses.attempts)); + } + + /** + * @inheritdoc + */ + resetItems(): void { + super.resetItems(); + this.responses.total = 0; + this.responses.attempts = []; + this.anonResponses.total = 0; + this.anonResponses.attempts = []; + } + + /** + * @inheritdoc + */ + protected getItemPath(entry: EntryItem): string { + return `attempt/${entry.id}`; + } + +} + +type AddonModFeedbackResponses = { + attempts: AddonModFeedbackWSAttempt[]; + total: number; + canLoadMore: boolean; +}; + +type AddonModFeedbackAnonResponses = { + attempts: AddonModFeedbackWSAnonAttempt[]; + total: number; + canLoadMore: boolean; +}; diff --git a/src/addons/mod/feedback/services/database/feedback.ts b/src/addons/mod/feedback/services/database/feedback.ts new file mode 100644 index 000000000..265ea6ed4 --- /dev/null +++ b/src/addons/mod/feedback/services/database/feedback.ts @@ -0,0 +1,63 @@ +// (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 { CoreSiteSchema } from '@services/sites'; + +/** + * Database variables for AddonModFeedbackOfflineProvider. + */ +export const FEEDBACK_TABLE_NAME = 'addon_mod_feedback_answers'; +export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = { + name: 'AddonModFeedbackOfflineProvider', + version: 1, + tables: [ + { + name: FEEDBACK_TABLE_NAME, + columns: [ + { + name: 'feedbackid', + type: 'INTEGER', + }, + { + name: 'page', + type: 'INTEGER', + }, + { + name: 'courseid', + type: 'INTEGER', + }, + { + name: 'responses', + type: 'TEXT', + }, + { + name: 'timemodified', + type: 'INTEGER', + }, + ], + primaryKeys: ['feedbackid', 'page'], + }, + ], +}; + +/** + * Response data. + */ +export type AddonModFeedbackResponseDBRecord = { + feedbackid: number; + page: number; + courseid: number; + responses: string; + timemodified: number; +}; diff --git a/src/addons/mod/feedback/services/feedback-helper.ts b/src/addons/mod/feedback/services/feedback-helper.ts new file mode 100644 index 000000000..3254c4f27 --- /dev/null +++ b/src/addons/mod/feedback/services/feedback-helper.ts @@ -0,0 +1,585 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreUser } from '@features/user/services/user'; +import { CoreNavigator } from '@services/navigator'; +import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreTimeUtils } from '@services/utils/time'; +import { CoreUtils } from '@services/utils/utils'; +import { makeSingleton, Translate } from '@singletons'; +import { + AddonModFeedback, + AddonModFeedbackGetNonRespondentsWSResponse, + AddonModFeedbackGetResponsesAnalysisWSResponse, + AddonModFeedbackGroupPaginatedOptions, + AddonModFeedbackItem, + AddonModFeedbackProvider, + AddonModFeedbackResponseValue, + AddonModFeedbackWSAttempt, + AddonModFeedbackWSNonRespondent, +} from './feedback'; +import { AddonModFeedbackModuleHandlerService } from './handlers/module'; + +const MODE_RESPONSETIME = 1; +const MODE_COURSE = 2; +const MODE_CATEGORY = 3; + +/** + * Service that provides helper functions for feedbacks. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModFeedbackHelperProvider { + + /** + * Retrieves a list of students who didn't submit the feedback with extra info. + * + * @param feedbackId Feedback ID. + * @param options Other options. + * @return Promise resolved when the info is retrieved. + */ + async getNonRespondents( + feedbackId: number, + options: AddonModFeedbackGroupPaginatedOptions = {}, + ): Promise { + const responses: AddonModFeedbackGetNonRespondents = await AddonModFeedback.getNonRespondents(feedbackId, options); + + responses.users = await this.addImageProfile(responses.users); + + return responses; + } + + /** + * Get page items responses to be sent. + * + * @param items Items where the values are. + * @return Responses object to be sent. + */ + getPageItemsResponses(items: AddonModFeedbackFormItem[]): Record { + const responses: Record = {}; + + items.forEach((itemData) => { + let answered = false; + itemData.hasError = false; + + if (itemData.typ == 'captcha') { + const value = itemData.value || ''; + const name = itemData.typ + '_' + itemData.id; + + answered = !!value; + responses[name] = 1; + responses['g-recaptcha-response'] = value; + responses['recaptcha_element'] = 'dummyvalue'; + + if (itemData.required && !answered) { + // Check if it has any value. + itemData.isEmpty = true; + } else { + itemData.isEmpty = false; + } + } else if (itemData.hasvalue) { + let name: string; + let value: AddonModFeedbackResponseValue; + const nameTemp = itemData.typ + '_' + itemData.id; + + if (this.isMultiChoiceItem(itemData) && itemData.subtype == 'c') { + name = nameTemp + '[0]'; + responses[name] = 0; + itemData.choices.forEach((choice, index) => { + name = nameTemp + '[' + (index + 1) + ']'; + value = choice.checked ? choice.value : 0; + if (!answered && value) { + answered = true; + } + responses[name] = value; + }); + } else { + if (this.isMultiChoiceItem(itemData) && itemData.subtype != 'r') { + name = nameTemp + '[0]'; + } else { + name = nameTemp; + } + + if (itemData.typ == 'multichoice' || itemData.typ == 'multichoicerated') { + value = itemData.value || 0; + } else if (this.isNumericItem(itemData)) { + value = itemData.value || itemData.value == 0 ? itemData.value : ''; + + if (value != '') { + if ((itemData.rangefrom != '' && value < itemData.rangefrom) || + (itemData.rangeto != '' && value > itemData.rangeto)) { + itemData.hasError = true; + } + } + } else { + value = itemData.value || itemData.value == 0 ? itemData.value : ''; + } + + answered = !!value; + responses[name] = value; + } + + if (itemData.required && !answered) { + // Check if it has any value. + itemData.isEmpty = true; + } else { + itemData.isEmpty = false; + } + } + }); + + return responses; + } + + /** + * Returns the feedback user responses with extra info. + * + * @param feedbackId Feedback ID. + * @param options Other options. + * @return Promise resolved when the info is retrieved. + */ + async getResponsesAnalysis( + feedbackId: number, + options: AddonModFeedbackGroupPaginatedOptions = {}, + ): Promise { + const responses: AddonModFeedbackResponsesAnalysis = await AddonModFeedback.getResponsesAnalysis(feedbackId, options); + + responses.attempts = await this.addImageProfile(responses.attempts); + + return responses; + } + + /** + * Handle a show entries link. + * + * @param params URL params. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + async handleShowEntriesLink(params: Record, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const modal = await CoreDomUtils.showModalLoading(); + + try { + const module = await CoreCourse.getModuleBasicInfo(Number(params.id), siteId); + + if (typeof params.showcompleted == 'undefined') { + // Param showcompleted not defined. Show entry list. + await CoreNavigator.navigateToSitePath( + AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${module.course}/${module.id}/respondents`, + { siteId }, + ); + + return; + } + + const attempt = await AddonModFeedback.getAttempt(module.instance, Number(params.showcompleted), { + cmId: module.id, + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }); + + await CoreNavigator.navigateToSitePath( + AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${module.course}/${module.id}/attempt/${attempt.id}`, + { + params: { + feedbackId: module.instance, + attempt: attempt, + }, + siteId, + }, + ); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'Error opening link.'); + } finally { + modal.dismiss(); + } + } + + /** + * Add Image profile url field on some entries. + * + * @param entries Entries array to get profile from. + * @return Returns the same array with the profileimageurl added if found. + */ + protected async addImageProfile(entries: AddonModFeedbackWSAttempt[]): Promise; + protected async addImageProfile(entries: AddonModFeedbackWSNonRespondent[]): Promise; + protected async addImageProfile( + entries: (AddonModFeedbackWSAttempt | AddonModFeedbackWSNonRespondent)[], + ): Promise<(AddonModFeedbackAttempt | AddonModFeedbackNonRespondent)[]> { + return await Promise.all(entries.map(async (entry: AddonModFeedbackAttempt | AddonModFeedbackNonRespondent) => { + try { + const user = await CoreUser.getProfile(entry.userid, entry.courseid, true); + + entry.profileimageurl = user.profileimageurl; + } catch { + // Error getting profile, resolve promise without adding any extra data. + } + + return entry; + })); + } + + /** + * Helper funtion for item type Label. + * + * @param item Item to process. + * @return Item processed to show form. + */ + protected getItemFormLabel(item: AddonModFeedbackItem): AddonModFeedbackFormBasicItem { + item.name = ''; + item.presentation = CoreTextUtils.replacePluginfileUrls(item.presentation, item.itemfiles); + + return Object.assign(item, { + templateName: 'label', + value: '', + hasTextInput: false, + }); + } + + /** + * Helper funtion for item type Info. + * + * @param item Item to process. + * @return Item processed to show form. + */ + protected getItemFormInfo(item: AddonModFeedbackItem): AddonModFeedbackFormBasicItem | undefined { + const formItem: AddonModFeedbackFormBasicItem = Object.assign(item, { + templateName: 'label', + value: '', + hasTextInput: false, + }); + + const type = parseInt(formItem.presentation, 10); + + if (type == MODE_COURSE || type == MODE_CATEGORY) { + formItem.presentation = formItem.otherdata; + formItem.value = typeof formItem.rawValue != 'undefined' ? formItem.rawValue : formItem.otherdata; + } else if (type == MODE_RESPONSETIME) { + formItem.value = '__CURRENT__TIMESTAMP__'; + + const rawValue = Number(formItem.rawValue); + const tempValue = isNaN(rawValue) ? Date.now() : rawValue * 1000; + formItem.presentation = CoreTimeUtils.userDate(tempValue); + } else { + // Errors on item, return false. + return undefined; + } + + return formItem; + } + + /** + * Helper funtion for item type Numeric. + * + * @param item Item to process. + * @return Item processed to show form. + */ + protected getItemFormNumeric(item: AddonModFeedbackItem): AddonModFeedbackNumericItem { + + const range = item.presentation.split(AddonModFeedbackProvider.LINE_SEP) || []; + const rangeFrom = range.length > 0 ? parseInt(range[0], 10) : undefined; + const rangeTo = range.length > 1 ? parseInt(range[1], 10) : undefined; + + const formItem: AddonModFeedbackNumericItem = Object.assign(item, { + templateName: 'numeric', + value: typeof item.rawValue != 'undefined' ? Number(item.rawValue) : '', + rangefrom: typeof rangeFrom == 'number' && !isNaN(rangeFrom) ? range[0] : '', + rangeto: typeof rangeTo == 'number' && !isNaN(rangeTo) ? rangeTo : '', + hasTextInput: true, + }); + formItem.postfix = this.getNumericBoundariesForDisplay(formItem.rangefrom, formItem.rangeto); + + return formItem; + } + + /** + * Helper funtion for item type Text field. + * + * @param item Item to process. + * @return Item processed to show form. + */ + protected getItemFormTextfield(item: AddonModFeedbackItem): AddonModFeedbackTextItem { + return Object.assign(item, { + templateName: 'textfield', + length: Number(item.presentation.split(AddonModFeedbackProvider.LINE_SEP)[1]) || 255, + value: typeof item.rawValue != 'undefined' ? item.rawValue : '', + hasTextInput: true, + }); + } + + /** + * Helper funtion for item type Textarea. + * + * @param item Item to process. + * @return Item processed to show form. + */ + protected getItemFormTextarea(item: AddonModFeedbackItem): AddonModFeedbackFormBasicItem { + return Object.assign(item, { + templateName: 'textarea', + value: typeof item.rawValue != 'undefined' ? item.rawValue : '', + hasTextInput: true, + }); + } + + /** + * Helper funtion for item type Multichoice. + * + * @param item Item to process. + * @return Item processed to show form. + */ + protected getItemFormMultichoice(item: AddonModFeedbackItem): AddonModFeedbackMultichoiceItem { + + let parts = item.presentation.split(AddonModFeedbackProvider.MULTICHOICE_TYPE_SEP) || []; + const subType = parts.length > 0 && parts[0] ? parts[0] : 'r'; + + const formItem: AddonModFeedbackMultichoiceItem = Object.assign(item, { + templateName: 'multichoice-' + subType, + subtype: subType, + value: '', + choices: [], + hasTextInput: false, + }); + + formItem.presentation = parts.length > 1 ? parts[1] : ''; + if (formItem.subtype != 'd') { + parts = formItem.presentation.split(AddonModFeedbackProvider.MULTICHOICE_ADJUST_SEP) || []; + formItem.presentation = parts.length > 0 ? parts[0] : ''; + // Horizontal are not supported right now. item.horizontal = parts.length > 1 && !!parts[1]; + } + + const choices = formItem.presentation.split(AddonModFeedbackProvider.LINE_SEP) || []; + formItem.choices = choices.map((choice, index) => { + const weightValue = choice.split(AddonModFeedbackProvider.MULTICHOICERATED_VALUE_SEP) || ['']; + choice = weightValue.length == 1 ? weightValue[0] : '(' + weightValue[0] + ') ' + weightValue[1]; + + return { value: index + 1, label: choice }; + }); + + if (formItem.subtype === 'r' && formItem.options.search(AddonModFeedbackProvider.MULTICHOICE_HIDENOSELECT) == -1) { + formItem.choices.unshift({ value: 0, label: Translate.instant('addon.mod_feedback.not_selected') }); + formItem.value = typeof formItem.rawValue != 'undefined' ? Number(formItem.rawValue) : 0; + } else if (formItem.subtype === 'd') { + formItem.choices.unshift({ value: 0, label: '' }); + formItem.value = typeof formItem.rawValue != 'undefined' ? Number(formItem.rawValue) : 0; + } else if (formItem.subtype === 'c') { + if (typeof formItem.rawValue != 'undefined') { + formItem.rawValue = String(formItem.rawValue); + const values = formItem.rawValue.split(AddonModFeedbackProvider.LINE_SEP); + formItem.choices.forEach((choice) => { + for (const x in values) { + if (choice.value == Number(values[x])) { + choice.checked = true; + + return; + } + } + }); + } + } else { + formItem.value = typeof formItem.rawValue != 'undefined' ? Number(formItem.rawValue) : ''; + } + + return formItem; + } + + /** + * Helper funtion for item type Captcha. + * + * @param item Item to process. + * @return Item processed to show form. + */ + protected getItemFormCaptcha(item: AddonModFeedbackItem): AddonModFeedbackCaptchaItem { + const formItem: AddonModFeedbackCaptchaItem = Object.assign(item, { + templateName: 'captcha', + value: '', + hasTextInput: false, + }); + + const data = CoreTextUtils.parseJSON(item.otherdata); + if (data && data.length > 3) { + formItem.captcha = { + recaptchapublickey: data[3], + }; + } + + return formItem; + } + + /** + * Process and returns item to print form. + * + * @param item Item to process. + * @param preview Previewing options. + * @return Item processed to show form. + */ + getItemForm(item: AddonModFeedbackItem, preview: boolean): AddonModFeedbackFormItem | undefined { + switch (item.typ) { + case 'label': + return this.getItemFormLabel(item); + case 'info': + return this.getItemFormInfo(item); + case 'numeric': + return this.getItemFormNumeric(item); + case 'textfield': + return this.getItemFormTextfield(item); + case 'textarea': + return this.getItemFormTextarea(item); + case 'multichoice': + return this.getItemFormMultichoice(item); + case 'multichoicerated': + return this.getItemFormMultichoice(item); + case 'pagebreak': + if (!preview) { + // Pagebreaks are only used on preview. + return undefined; + } + break; + case 'captcha': + // Captcha is not supported right now. However label will be shown. + return this.getItemFormCaptcha(item); + default: + return undefined; + } + } + + /** + * Returns human-readable boundaries (min - max). + * Based on Moodle's get_boundaries_for_display. + * + * @param rangeFrom Range from. + * @param rangeTo Range to. + * @return Human-readable boundaries. + */ + protected getNumericBoundariesForDisplay(rangeFrom: number | string, rangeTo: number | string): string { + const rangeFromSet = typeof rangeFrom == 'number'; + const rangeToSet = typeof rangeTo == 'number'; + + if (!rangeFromSet && rangeToSet) { + return ' (' + Translate.instant('addon.mod_feedback.maximal') + ': ' + CoreUtils.formatFloat(rangeTo) + ')'; + } else if (rangeFromSet && !rangeToSet) { + return ' (' + Translate.instant('addon.mod_feedback.minimal') + ': ' + CoreUtils.formatFloat(rangeFrom) + ')'; + } else if (!rangeFromSet && !rangeToSet) { + return ''; + } + + return ' (' + CoreUtils.formatFloat(rangeFrom) + ' - ' + CoreUtils.formatFloat(rangeTo) + ')'; + } + + /** + * Check if a form item is multichoice. + * + * @param item Item. + * @return Whether item is multichoice. + */ + protected isMultiChoiceItem(item: AddonModFeedbackFormItem): item is AddonModFeedbackMultichoiceItem { + return item.typ == 'multichoice'; + } + + /** + * Check if a form item is numeric. + * + * @param item Item. + * @return Whether item is numeric. + */ + protected isNumericItem(item: AddonModFeedbackFormItem): item is AddonModFeedbackNumericItem { + return item.typ == 'numeric'; + } + +} + +export const AddonModFeedbackHelper = makeSingleton(AddonModFeedbackHelperProvider); + +/** + * Attempt with some calculated data. + */ +export type AddonModFeedbackAttempt = AddonModFeedbackWSAttempt & { + profileimageurl?: string; +}; + +/** + * Non respondent with some calculated data. + */ +export type AddonModFeedbackNonRespondent = AddonModFeedbackWSNonRespondent & { + profileimageurl?: string; +}; + +/** + * Non respondents with some calculated data. + */ +export type AddonModFeedbackResponsesAnalysis = Omit & { + attempts: AddonModFeedbackAttempt[]; +}; + +/** + * Non respondents with some calculated data. + */ +export type AddonModFeedbackGetNonRespondents = Omit & { + users: AddonModFeedbackNonRespondent[]; +}; + +/** + * Item with form data. + */ +export type AddonModFeedbackFormItem = + AddonModFeedbackFormBasicItem | AddonModFeedbackNumericItem | AddonModFeedbackTextItem | AddonModFeedbackMultichoiceItem | + AddonModFeedbackCaptchaItem; + +/** + * Common calculated data for all form items. + */ +export type AddonModFeedbackFormBasicItem = AddonModFeedbackItem & { + templateName: string; + value: AddonModFeedbackResponseValue; + hasTextInput: boolean; + isEmpty?: boolean; + hasError?: boolean; +}; + +/** + * Numeric item. + */ +export type AddonModFeedbackNumericItem = AddonModFeedbackFormBasicItem & { + rangefrom: number | string; + rangeto: number | string; + postfix?: string; +}; + +/** + * Text item. + */ +export type AddonModFeedbackTextItem = AddonModFeedbackFormBasicItem & { + length: number; +}; + +/** + * Multichoice item. + */ +export type AddonModFeedbackMultichoiceItem = AddonModFeedbackFormBasicItem & { + subtype: string; + choices: { value: number; label: string; checked?: boolean }[]; +}; + +/** + * Captcha item. + */ +export type AddonModFeedbackCaptchaItem = AddonModFeedbackFormBasicItem & { + captcha?: { + recaptchapublickey: string; + }; +}; diff --git a/src/addons/mod/feedback/services/feedback-offline.ts b/src/addons/mod/feedback/services/feedback-offline.ts new file mode 100644 index 000000000..1f9187328 --- /dev/null +++ b/src/addons/mod/feedback/services/feedback-offline.ts @@ -0,0 +1,162 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreSites } from '@services/sites'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreTimeUtils } from '@services/utils/time'; +import { makeSingleton } from '@singletons'; +import { AddonModFeedbackResponseDBRecord, FEEDBACK_TABLE_NAME } from './database/feedback'; +import { AddonModFeedbackResponseValue } from './feedback'; + +/** + * Service to handle offline feedback. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModFeedbackOfflineProvider { + + /** + * Delete the stored for a certain feedback page. + * + * @param feedbackId Feedback ID. + * @param page Page of the form to delete responses from. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if deleted, rejected if failure. + */ + async deleteFeedbackPageResponses(feedbackId: number, page: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.getDb().deleteRecords(FEEDBACK_TABLE_NAME, > { + feedbackid: feedbackId, + page: page, + }); + } + + /** + * Get all the stored feedback responses data from all the feedback. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with entries. + */ + async getAllFeedbackResponses(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const entries = await site.getDb().getAllRecords(FEEDBACK_TABLE_NAME); + + return entries.map(entry => this.parseResponse(entry)); + } + + /** + * Get all the stored responses from a certain feedback. + * + * @param feedbackId Feedback ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with responses. + */ + async getFeedbackResponses(feedbackId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const entries = await site.getDb().getRecords(FEEDBACK_TABLE_NAME, { + feedbackid: feedbackId, + }); + + return entries.map(entry => this.parseResponse(entry)); + } + + /** + * Get the stored responses for a certain feedback page. + * + * @param feedbackId Feedback ID. + * @param page Page of the form to get responses from. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with responses. + */ + async getFeedbackPageResponses(feedbackId: number, page: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const conditions: Partial = { + feedbackid: feedbackId, + page: page, + }; + + const entry = await site.getDb().getRecord(FEEDBACK_TABLE_NAME, conditions); + + return this.parseResponse(entry); + } + + /** + * Get if the feedback have something to be synced. + * + * @param feedbackId Feedback ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with true if the feedback have something to be synced. + */ + async hasFeedbackOfflineData(feedbackId: number, siteId?: string): Promise { + const responses = await this.getFeedbackResponses(feedbackId, siteId); + + return !!responses.length; + } + + /** + * Parse "options" and "attachments" columns of a fetched record. + * + * @param records Record object + * @return Record object with columns parsed. + */ + protected parseResponse(record: AddonModFeedbackResponseDBRecord): AddonModFeedbackOfflineResponse { + return Object.assign(record, { + responses: > CoreTextUtils.parseJSON(record.responses), + }); + } + + /** + * Save page responses to be sent later. + * + * @param feedbackId Feedback ID. + * @param page The page being processed. + * @param responses The data to be processed the key is the field name (usually type[index]_id) + * @param courseId Course ID the feedback belongs to. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if stored, rejected if failure. + */ + async saveResponses( + feedbackId: number, + page: number, + responses: Record, + courseId: number, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const entry: AddonModFeedbackResponseDBRecord = { + feedbackid: feedbackId, + page: page, + courseid: courseId, + responses: JSON.stringify(responses), + timemodified: CoreTimeUtils.timestamp(), + }; + + await site.getDb().insertRecord(FEEDBACK_TABLE_NAME, entry); + } + +} + +export const AddonModFeedbackOffline = makeSingleton(AddonModFeedbackOfflineProvider); + +/** + * Feedback offline response with parsed data. + */ +export type AddonModFeedbackOfflineResponse = Omit & { + responses: Record; +}; diff --git a/src/addons/mod/feedback/services/feedback-sync.ts b/src/addons/mod/feedback/services/feedback-sync.ts new file mode 100644 index 000000000..51c4cae18 --- /dev/null +++ b/src/addons/mod/feedback/services/feedback-sync.ts @@ -0,0 +1,305 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreSyncBlockedError } from '@classes/base-sync'; +import { CoreNetworkError } from '@classes/errors/network-error'; +import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync'; +import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; +import { CoreCourseLogHelper } from '@features/course/services/log-helper'; +import { CoreApp } from '@services/app'; +import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreSync } from '@services/sync'; +import { CoreUtils } from '@services/utils/utils'; +import { makeSingleton, Translate } from '@singletons'; +import { CoreEvents } from '@singletons/events'; +import { AddonModFeedback, AddonModFeedbackProvider, AddonModFeedbackWSFeedback } from './feedback'; +import { AddonModFeedbackOffline, AddonModFeedbackOfflineResponse } from './feedback-offline'; +import { AddonModFeedbackPrefetchHandler, AddonModFeedbackPrefetchHandlerService } from './handlers/prefetch'; + +/** + * Service to sync feedbacks. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModFeedbackSyncProvider extends CoreCourseActivitySyncBaseProvider { + + static readonly AUTO_SYNCED = 'addon_mod_feedback_autom_synced'; + + protected componentTranslatableString = 'feedback'; + + constructor() { + super('AddonModFeedbackSyncProvider'); + } + + /** + * @inheritdoc + */ + prefetchAfterUpdate( + prefetchHandler: AddonModFeedbackPrefetchHandlerService, + module: CoreCourseAnyModuleData, + courseId: number, + regex?: RegExp, + siteId?: string, + ): Promise { + regex = regex || /^.*files$|^timers/; + + return super.prefetchAfterUpdate(prefetchHandler, module, courseId, regex, siteId); + } + + /** + * Try to synchronize all the feedbacks in a certain site or in all sites. + * + * @param siteId Site ID to sync. If not defined, sync all sites. + * @param force Wether to force sync not depending on last execution. + * @return Promise resolved if sync is successful, rejected if sync fails. + */ + syncAllFeedbacks(siteId?: string, force?: boolean): Promise { + return this.syncOnSites('all feedbacks', this.syncAllFeedbacksFunc.bind(this, !!force), siteId); + } + + /** + * Sync all pending feedbacks on a site. + * + * @param force Wether to force sync not depending on last execution. + * @param siteId Site ID to sync. If not defined, sync all sites. + * @param Promise resolved if sync is successful, rejected if sync fails. + */ + protected async syncAllFeedbacksFunc(force: boolean, siteId?: string): Promise { + // Sync all new responses. + const responses = await AddonModFeedbackOffline.getAllFeedbackResponses(siteId); + + // Do not sync same feedback twice. + const treated: Record = {}; + + await Promise.all(responses.map(async (response) => { + if (treated[response.feedbackid]) { + return; + } + + treated[response.feedbackid] = true; + + const result = force ? + await this.syncFeedback(response.feedbackid, siteId) : + await this.syncFeedbackIfNeeded(response.feedbackid, siteId); + + if (result?.updated) { + // Sync successful, send event. + CoreEvents.trigger(AddonModFeedbackSyncProvider.AUTO_SYNCED, { + feedbackId: response.feedbackid, + warnings: result.warnings, + }, siteId); + } + })); + } + + /** + * Sync a feedback only if a certain time has passed since the last time. + * + * @param feedbackId Feedback ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the feedback is synced or if it doesn't need to be synced. + */ + async syncFeedbackIfNeeded(feedbackId: number, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const needed = await this.isSyncNeeded(feedbackId, siteId); + + if (needed) { + return this.syncFeedback(feedbackId, siteId); + } + } + + /** + * Synchronize all offline responses of a feedback. + * + * @param feedbackId Feedback ID to be synced. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if sync is successful, rejected otherwise. + */ + syncFeedback(feedbackId: number, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + if (this.isSyncing(feedbackId, siteId)) { + // There's already a sync ongoing for this feedback, return the promise. + return this.getOngoingSync(feedbackId, siteId)!; + } + + // Verify that feedback isn't blocked. + if (CoreSync.isBlocked(AddonModFeedbackProvider.COMPONENT, feedbackId, siteId)) { + this.logger.debug(`Cannot sync feedback '${feedbackId}' because it is blocked.`); + + throw new CoreSyncBlockedError(Translate.instant('core.errorsyncblocked', { $a: this.componentTranslate })); + } + + this.logger.debug(`Try to sync feedback '${feedbackId}' in site ${siteId}'`); + + return this.addOngoingSync(feedbackId, this.performSyncFeedback(feedbackId, siteId), siteId); + } + + /** + * Perform the feedback sync. + * + * @param feedbackId Feedback ID. + * @param siteId Site ID. + * @return Promise resolved in success. + */ + protected async performSyncFeedback(feedbackId: number, siteId: string): Promise { + const result: AddonModFeedbackSyncResult = { + warnings: [], + updated: false, + }; + + // Sync offline logs. + await CoreUtils.ignoreErrors(CoreCourseLogHelper.syncActivity(AddonModFeedbackProvider.COMPONENT, feedbackId, siteId)); + + // Get offline responses to be sent. + const responses = await CoreUtils.ignoreErrors(AddonModFeedbackOffline.getFeedbackResponses(feedbackId, siteId)); + + if (!responses || !responses.length) { + // Nothing to sync. + await CoreUtils.ignoreErrors(this.setSyncTime(feedbackId, siteId)); + + return result; + } + + if (!CoreApp.isOnline()) { + // Cannot sync in offline. + throw new CoreNetworkError(); + } + + const courseId = responses[0].courseid; + + const feedback = await AddonModFeedback.getFeedbackById(courseId, feedbackId, { siteId }); + + if (!feedback.multiple_submit) { + // If it does not admit multiple submits, check if it is completed to know if we can submit. + const isCompleted = await AddonModFeedback.isCompleted(feedbackId, { cmId: feedback.coursemodule, siteId }); + + if (isCompleted) { + // Cannot submit again, delete resposes. + await Promise.all(responses.map((data) => + AddonModFeedbackOffline.deleteFeedbackPageResponses(feedbackId, data.page, siteId))); + + result.updated = true; + this.addOfflineDataDeletedWarning( + result.warnings, + feedback.name, + Translate.instant('addon.mod_feedback.this_feedback_is_already_submitted'), + ); + + await CoreUtils.ignoreErrors(this.setSyncTime(feedbackId, siteId)); + + return result; + } + } + + const timemodified = await AddonModFeedback.getCurrentCompletedTimeModified(feedbackId, { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }); + // Sort by page. + responses.sort((a, b) => a.page - b.page); + + const orderedData = responses.map((data) => ({ + function: this.processPage.bind(this, feedback, data, siteId, timemodified, result), + blocking: true, + })); + + // Execute all the processes in order to solve dependencies. + await CoreUtils.executeOrderedPromises(orderedData); + + if (result.updated) { + // Data has been sent to server, update data. + try { + const module = await CoreCourse.getModuleBasicInfoByInstance(feedbackId, 'feedback', siteId); + + await this.prefetchAfterUpdate(AddonModFeedbackPrefetchHandler.instance, module, courseId, undefined, siteId); + } catch { + // Ignore errors. + } + } + + // Sync finished, set sync time. + await CoreUtils.ignoreErrors(this.setSyncTime(feedbackId, siteId)); + + return result; + } + + /** + * Convenience function to sync process page calls. + * + * @param feedback Feedback object. + * @param data Response data. + * @param siteId Site Id. + * @param timemodified Current completed modification time. + * @param result Result object to be modified. + * @return Resolve when done or rejected with error. + */ + protected async processPage( + feedback: AddonModFeedbackWSFeedback, + data: AddonModFeedbackOfflineResponse, + siteId: string, + timemodified: number, + result: AddonModFeedbackSyncResult, + ): Promise { + // Delete all pages that are submitted before changing website. + if (timemodified > data.timemodified) { + return AddonModFeedbackOffline.deleteFeedbackPageResponses(feedback.id, data.page, siteId); + } + + try { + await AddonModFeedback.processPageOnline(feedback.id, data.page, data.responses, false, siteId); + + result.updated = true; + + await AddonModFeedbackOffline.deleteFeedbackPageResponses(feedback.id, data.page, siteId); + } catch (error) { + if (!CoreUtils.isWebServiceError(error)) { + // Couldn't connect to server, reject. + throw error; + } + + // The WebService has thrown an error, this means that responses cannot be submitted. Delete them. + result.updated = true; + + await AddonModFeedbackOffline.deleteFeedbackPageResponses(feedback.id, data.page, siteId); + + // Responses deleted, add a warning. + this.addOfflineDataDeletedWarning( + result.warnings, + feedback.name, + error, + ); + } + } + +} + +export const AddonModFeedbackSync = makeSingleton(AddonModFeedbackSyncProvider); + +/** + * Data returned by a feedback sync. + */ +export type AddonModFeedbackSyncResult = { + warnings: string[]; // List of warnings. + updated: boolean; // Whether some data was sent to the server or offline data was updated. +}; + +/** + * Data passed to AUTO_SYNCED event. + */ +export type AddonModFeedbackAutoSyncData = { + feedbackId: number; + warnings: string[]; +}; diff --git a/src/addons/mod/feedback/services/feedback.ts b/src/addons/mod/feedback/services/feedback.ts new file mode 100644 index 000000000..04c4e1e1e --- /dev/null +++ b/src/addons/mod/feedback/services/feedback.ts @@ -0,0 +1,1762 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreError } from '@classes/errors/error'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; +import { CoreCourseLogHelper } from '@features/course/services/log-helper'; +import { CoreApp } from '@services/app'; +import { CoreFilepool } from '@services/filepool'; +import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreWSExternalFile, CoreWSExternalWarning, CoreWSStoredFile } from '@services/ws'; +import { makeSingleton } from '@singletons'; +import { AddonModFeedbackOffline } from './feedback-offline'; +import { AddonModFeedbackAutoSyncData, AddonModFeedbackSyncProvider } from './feedback-sync'; + +const ROOT_CACHE_KEY = 'AddonModFeedback:'; + +/** + * Service that provides some features for feedbacks. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModFeedbackProvider { + + static readonly COMPONENT = 'mmaModFeedback'; + static readonly FORM_SUBMITTED = 'addon_mod_feedback_form_submitted'; + static readonly LINE_SEP = '|'; + static readonly MULTICHOICE_TYPE_SEP = '>>>>>'; + static readonly MULTICHOICE_ADJUST_SEP = '<<<<<'; + static readonly MULTICHOICE_HIDENOSELECT = 'h'; + static readonly MULTICHOICERATED_VALUE_SEP = '####'; + static readonly PER_PAGE = 20; + + /** + * Check dependency of a question item. + * + * @param items All question items to check dependency. + * @param item Item to check. + * @return Return true if dependency is acomplished and it can be shown. False, otherwise. + */ + protected checkDependencyItem(items: AddonModFeedbackItem[], item: AddonModFeedbackItem): boolean { + const depend = items.find((itemFind) => itemFind.id == item.dependitem); + + // Item not found, looks like dependent item has been removed or is in the same or following pages. + if (!depend) { + return true; + } + + switch (depend.typ) { + case 'label': + return false; + case 'multichoice': + case 'multichoicerated': + return this.compareDependItemMultichoice(depend, item.dependvalue); + default: + break; + } + + return item.dependvalue == depend.rawValue; + } + + /** + * Check dependency item of type Multichoice. + * + * @param item Item to check. + * @param dependValue Value to compare. + * @return Return true if dependency is acomplished and it can be shown. False, otherwise. + */ + protected compareDependItemMultichoice(item: AddonModFeedbackItem, dependValue: string): boolean { + const parts = item.presentation.split(AddonModFeedbackProvider.MULTICHOICE_TYPE_SEP) || []; + const subtype = parts.length > 0 && parts[0] ? parts[0] : 'r'; + + const choicesStr = (parts[1] || '').split(AddonModFeedbackProvider.MULTICHOICE_ADJUST_SEP)[0] || ''; + const choices = choicesStr.split(AddonModFeedbackProvider.LINE_SEP) || []; + let values: AddonModFeedbackResponseValue[]; + + if (subtype === 'c') { + if (typeof item.rawValue == 'undefined') { + values = ['']; + } else { + item.rawValue = '' + item.rawValue; + values = item.rawValue.split(AddonModFeedbackProvider.LINE_SEP); + } + } else { + values = [item.rawValue || '']; + } + + for (let index = 0; index < choices.length; index++) { + for (const x in values) { + if (values[x] == index + 1) { + let value = choices[index]; + + if (item.typ == 'multichoicerated') { + value = value.split(AddonModFeedbackProvider.MULTICHOICERATED_VALUE_SEP)[1] || ''; + } + + if (value.trim() == dependValue) { + return true; + } + + // We can finish checking if only searching on one value and we found it. + if (values.length == 1) { + return false; + } + } + } + } + + return false; + } + + /** + * Fill values of item questions. + * + * @param feedbackId Feedback ID. + * @param items Item to fill the value. + * @param options Other options. + * @return Resolved with values when done. + */ + protected async fillValues( + feedbackId: number, + items: AddonModFeedbackWSItem[], + options: CoreCourseCommonModWSOptions = {}, + ): Promise { + const filledItems = items; + + try { + const valuesArray = await this.getCurrentValues(feedbackId, options); + + const values: Record = {}; + + valuesArray.forEach((value) => { + values[value.item] = value.value; + }); + + filledItems.forEach((itemData) => { + if (itemData.hasvalue && typeof values[itemData.id] != 'undefined') { + itemData.rawValue = values[itemData.id]; + } + }); + } catch { + // Ignore errors. + } + + // Merge with offline data. + const offlineResponses = await CoreUtils.ignoreErrors( + AddonModFeedbackOffline.getFeedbackResponses(feedbackId, options.siteId), + ); + + if (!offlineResponses) { + return items; + } + + const offlineValues: Record = {}; + + // Merge all values into one array. + const offlineValuesArray = offlineResponses.reduce((array, entry) => { + const responses = CoreUtils.objectToArrayOfObjects(entry.responses, 'id', 'value'); + + return array.concat(responses); + }, []).map((valueEntry) => { + const parts = valueEntry.id.split('_'); + + return { + ...valueEntry, + typ: parts[0], + item: Number(parts[1]), + }; + }); + + offlineValuesArray.forEach((value) => { + if (typeof offlineValues[value.item] == 'undefined') { + offlineValues[value.item] = []; + } + offlineValues[value.item].push(value.value); + }); + + filledItems.forEach((item) => { + if (!item.hasvalue || offlineValues[item.id] === undefined) { + return; + } + + // Treat multichoice checkboxes. + if (item.typ == 'multichoice' && item.presentation.split(AddonModFeedbackProvider.MULTICHOICE_TYPE_SEP)[0] == 'c') { + + offlineValues[item.id] = offlineValues[item.id].filter((value) => value > 0); + item.rawValue = offlineValues[item.id].join(AddonModFeedbackProvider.LINE_SEP); + } else { + item.rawValue = offlineValues[item.id][0]; + } + }); + + return filledItems; + } + + /** + * Returns all the feedback non respondents users. + * + * @param feedbackId Feedback ID. + * @param options Other options. + * @param previous Only for recurrent use. Object with the previous fetched info. + * @return Promise resolved when the info is retrieved. + */ + async getAllNonRespondents( + feedbackId: number, + options: AddonModFeedbackGroupOptions = {}, + previous?: AddonModFeedbackPreviousNonRespondents, + ): Promise { + options.siteId = options.siteId || CoreSites.getCurrentSiteId(); + previous = previous || { + page: 0, + users: [], + }; + + const response = await this.getNonRespondents(feedbackId, { + page: previous.page, + ...options, // Include all options. + }); + + if (previous.users.length < response.total) { + previous.users = previous.users.concat(response.users); + } + + if (previous.users.length < response.total) { + // Can load more. + previous.page++; + + return this.getAllNonRespondents(feedbackId, options, previous); + } + + return { + ...previous, + total: response.total, + }; + } + + /** + * Returns all the feedback user responses. + * + * @param feedbackId Feedback ID. + * @param options Other options. + * @param previous Only for recurrent use. Object with the previous fetched info. + * @return Promise resolved when the info is retrieved. + */ + async getAllResponsesAnalysis( + feedbackId: number, + options: AddonModFeedbackGroupOptions = {}, + previous?: AddonModFeedbackPreviousResponsesAnalysis, + ): Promise { + + options.siteId = options.siteId || CoreSites.getCurrentSiteId(); + previous = previous || { + page: 0, + attempts: [], + anonattempts: [], + }; + + const responses = await this.getResponsesAnalysis(feedbackId, { + page: previous.page, + ...options, // Include all options. + }); + + if (previous.anonattempts.length < responses.totalanonattempts) { + previous.anonattempts = previous.anonattempts.concat(responses.anonattempts); + } + + if (previous.attempts.length < responses.totalattempts) { + previous.attempts = previous.attempts.concat(responses.attempts); + } + + if (previous.anonattempts.length < responses.totalanonattempts || previous.attempts.length < responses.totalattempts) { + // Can load more. + previous.page++; + + return this.getAllResponsesAnalysis(feedbackId, options, previous); + } + + return { + ...previous, + totalattempts: responses.totalattempts, + totalanonattempts: responses.totalanonattempts, + }; + } + + /** + * Get analysis information for a given feedback. + * + * @param feedbackId Feedback ID. + * @param options Other options. + * @return Promise resolved when the feedback is retrieved. + */ + async getAnalysis( + feedbackId: number, + options: AddonModFeedbackGroupOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModFeedbackGetAnalysisWSParams = { + feedbackid: feedbackId, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getAnalysisDataCacheKey(feedbackId, options.groupId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + if (options.groupId) { + params.groupid = options.groupId; + } + + return site.read('mod_feedback_get_analysis', params, preSets); + } + + /** + * Get cache key for feedback analysis data WS calls. + * + * @param feedbackId Feedback ID. + * @param groupId Group ID. + * @return Cache key. + */ + protected getAnalysisDataCacheKey(feedbackId: number, groupId: number = 0): string { + return this.getAnalysisDataPrefixCacheKey(feedbackId) + groupId; + } + + /** + * Get prefix cache key for feedback analysis data WS calls. + * + * @param feedbackId Feedback ID. + * @return Cache key. + */ + protected getAnalysisDataPrefixCacheKey(feedbackId: number): string { + return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':analysis:'; + } + + /** + * Find an attempt in all responses analysis. + * + * @param feedbackId Feedback ID. + * @param attemptId Attempt ID to find. + * @param options Other options. + * @param previous Only for recurrent use. Object with the previous fetched info. + * @return Promise resolved when the info is retrieved. + */ + async getAttempt( + feedbackId: number, + attemptId: number, + options: CoreCourseCommonModWSOptions = {}, + previous?: AddonModFeedbackGetAttemptPreviousData, + ): Promise { + options.siteId = options.siteId || CoreSites.getCurrentSiteId(); + previous = previous || { + page: 0, + attemptsLoaded: 0, + anonAttemptsLoaded: 0, + }; + + const responses = await this.getResponsesAnalysis(feedbackId, { + page: previous.page, + groupId: 0, + ...options, // Include all options. + }); + + const attempt = responses.attempts.find((attempt) => attemptId == attempt.id); + + if (attempt) { + return attempt; + } + + const anonAttempt = responses.anonattempts.find((attempt) => attemptId == attempt.id); + + if (anonAttempt) { + return anonAttempt; + } + + if (previous.anonAttemptsLoaded < responses.totalanonattempts) { + previous.anonAttemptsLoaded += responses.anonattempts.length; + } + if (previous.attemptsLoaded < responses.totalattempts) { + previous.attemptsLoaded += responses.attempts.length; + } + + if (previous.anonAttemptsLoaded < responses.totalanonattempts || previous.attemptsLoaded < responses.totalattempts) { + // Can load more. Check there. + previous.page++; + + return this.getAttempt(feedbackId, attemptId, options, previous); + } + + // Not found and all loaded. Reject. + throw new CoreError('Attempt not found.'); + } + + /** + * Get prefix cache key for feedback completion data WS calls. + * + * @param feedbackId Feedback ID. + * @return Cache key. + */ + protected getCompletedDataCacheKey(feedbackId: number): string { + return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':completed:'; + } + + /** + * Returns the temporary completion timemodified for the current user. + * + * @param feedbackId Feedback ID. + * @param options Other options. + * @return Promise resolved when the info is retrieved. + */ + async getCurrentCompletedTimeModified(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModFeedbackGetCurrentCompletedTmpWSParams = { + feedbackid: feedbackId, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getCurrentCompletedTimeModifiedDataCacheKey(feedbackId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + try { + const response = await site.read( + 'mod_feedback_get_current_completed_tmp', + params, + preSets, + ); + + return response.feedback.timemodified; + } catch { + // Ignore errors. + return 0; + } + } + + /** + * Get prefix cache key for feedback current completed temp data WS calls. + * + * @param feedbackId Feedback ID. + * @return Cache key. + */ + protected getCurrentCompletedTimeModifiedDataCacheKey(feedbackId: number): string { + return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':completedtime:'; + } + + /** + * Returns the temporary responses or responses of the last submission for the current user. + * + * @param feedbackId Feedback ID. + * @param options Other options. + * @return Promise resolved when the info is retrieved. + */ + async getCurrentValues( + feedbackId: number, + options: CoreCourseCommonModWSOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModFeedbackGetUnfinishedResponsesWSParams = { + feedbackid: feedbackId, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getCurrentValuesDataCacheKey(feedbackId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const response = await site.read( + 'mod_feedback_get_unfinished_responses', + params, + preSets, + ); + + if (response.responses.length) { + return response.responses; + } + + // No unfinished responses, fetch responses of the last submission. + const finishedResponse = await site.read( + 'mod_feedback_get_finished_responses', + params, + preSets, + ); + + return finishedResponse.responses; + } + + /** + * Get cache key for get current values feedback data WS calls. + * + * @param feedbackId Feedback ID. + * @return Cache key. + */ + protected getCurrentValuesDataCacheKey(feedbackId: number): string { + return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':currentvalues'; + } + + /** + * Get access information for a given feedback. + * + * @param feedbackId Feedback ID. + * @param options Other options. + * @return Promise resolved when the feedback is retrieved. + */ + async getFeedbackAccessInformation( + feedbackId: number, + options: CoreCourseCommonModWSOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModFeedbackGetFeedbackAccessInformationWSParams = { + feedbackid: feedbackId, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getFeedbackAccessInformationDataCacheKey(feedbackId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + return site.read('mod_feedback_get_feedback_access_information', params, preSets); + } + + /** + * Get cache key for feedback access information data WS calls. + * + * @param feedbackId Feedback ID. + * @return Cache key. + */ + protected getFeedbackAccessInformationDataCacheKey(feedbackId: number): string { + return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':access'; + } + + /** + * Get cache key for feedback data WS calls. + * + * @param courseId Course ID. + * @return Cache key. + */ + protected getFeedbackCacheKey(courseId: number): string { + return ROOT_CACHE_KEY + 'feedback:' + courseId; + } + + /** + * Get prefix cache key for all feedback activity data WS calls. + * + * @param feedbackId Feedback ID. + * @return Cache key. + */ + protected getFeedbackDataPrefixCacheKey(feedbackId: number): string { + return ROOT_CACHE_KEY + feedbackId; + } + + /** + * Get a feedback with key=value. If more than one is found, only the first will be returned. + * + * @param courseId Course ID. + * @param key Name of the property to check. + * @param value Value to search. + * @param options Other options. + * @return Promise resolved when the feedback is retrieved. + */ + protected async getFeedbackDataByKey( + courseId: number, + key: string, + value: unknown, + options: CoreSitesCommonWSOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModFeedbackGetFeedbacksByCoursesWSParams = { + courseids: [courseId], + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getFeedbackCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModFeedbackProvider.COMPONENT, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const response = await site.read( + 'mod_feedback_get_feedbacks_by_courses', + params, + preSets, + ); + + const currentFeedback = response.feedbacks.find((feedback) => feedback[key] == value); + if (currentFeedback) { + return currentFeedback; + } + + throw new CoreError('Feedback not found.'); + } + + /** + * Get a feedback by course module ID. + * + * @param courseId Course ID. + * @param cmId Course module ID. + * @param options Other options. + * @return Promise resolved when the feedback is retrieved. + */ + getFeedback(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getFeedbackDataByKey(courseId, 'coursemodule', cmId, options); + } + + /** + * Get a feedback by ID. + * + * @param courseId Course ID. + * @param id Feedback ID. + * @param options Other options. + * @return Promise resolved when the feedback is retrieved. + */ + getFeedbackById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getFeedbackDataByKey(courseId, 'id', id, options); + } + + /** + * Returns the items (questions) in the given feedback. + * + * @param feedbackId Feedback ID. + * @param options Other options. + * @return Promise resolved when the info is retrieved. + */ + async getItems(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModFeedbackGetItemsWSParams = { + feedbackid: feedbackId, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getItemsDataCacheKey(feedbackId), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + return site.read('mod_feedback_get_items', params, preSets); + } + + /** + * Get cache key for get items feedback data WS calls. + * + * @param feedbackId Feedback ID. + * @return Cache key. + */ + protected getItemsDataCacheKey(feedbackId: number): string { + return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':items'; + } + + /** + * Retrieves a list of students who didn't submit the feedback. + * + * @param feedbackId Feedback ID. + * @param options Other options. + * @return Promise resolved when the info is retrieved. + */ + async getNonRespondents( + feedbackId: number, + options: AddonModFeedbackGroupPaginatedOptions = {}, + ): Promise { + options.groupId = options.groupId || 0; + options.page = options.page || 0; + + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModFeedbackGetNonRespondentsWSParams = { + feedbackid: feedbackId, + groupid: options.groupId, + page: options.page, + perpage: AddonModFeedbackProvider.PER_PAGE, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getNonRespondentsDataCacheKey(feedbackId, options.groupId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + return site.read('mod_feedback_get_non_respondents', params, preSets); + } + + /** + * Get cache key for non respondents feedback data WS calls. + * + * @param feedbackId Feedback ID. + * @param groupId Group id, 0 means that the function will determine the user group. + * @return Cache key. + */ + protected getNonRespondentsDataCacheKey(feedbackId: number, groupId: number = 0): string { + return this.getNonRespondentsDataPrefixCacheKey(feedbackId) + groupId; + } + + /** + * Get prefix cache key for feedback non respondents data WS calls. + * + * @param feedbackId Feedback ID. + * @return Cache key. + */ + protected getNonRespondentsDataPrefixCacheKey(feedbackId: number): string { + return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':nonrespondents:'; + } + + /** + * Get a single feedback page items. This function is not cached, use AddonModFeedbackHelperProvider#getPageItems instead. + * + * @param feedbackId Feedback ID. + * @param page The page to get. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the info is retrieved. + */ + async getPageItems(feedbackId: number, page: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonModFeedbackGetPageItemsWSParams = { + feedbackid: feedbackId, + page: page, + }; + + return site.write('mod_feedback_get_page_items', params); + } + + /** + * Get a single feedback page items. If offline or server down it will use getItems to calculate dependencies. + * + * @param feedbackId Feedback ID. + * @param page The page to get. + * @param options Other options. + * @return Promise resolved when the info is retrieved. + */ + async getPageItemsWithValues( + feedbackId: number, + page: number, + options: CoreCourseCommonModWSOptions = {}, + ): Promise { + options.siteId = options.siteId || CoreSites.getCurrentSiteId(); + + try { + const response: AddonModFeedbackPageItems = await this.getPageItems(feedbackId, page, options.siteId); + + response.items = await this.fillValues(feedbackId, response.items, options); + + return response; + } catch { + // If getPageItems fail we should calculate it using getItems. + const response = await this.getItems(feedbackId, options); + + const items = await this.fillValues(feedbackId, response.items, options); + + // Separate items by pages. + let currentPage = 0; + const previousPageItems: AddonModFeedbackItem[] = []; + + const pageItems = items.filter((item) => { + // Greater page, discard all entries. + if (currentPage > page) { + return false; + } + + if (item.typ == 'pagebreak') { + currentPage++; + + return false; + } + + // Save items on previous page to check dependencies and discard entry. + if (currentPage < page) { + previousPageItems.push(item); + + return false; + } + + // Filter depending items. + if (item && item.dependitem > 0 && previousPageItems.length > 0) { + return this.checkDependencyItem(previousPageItems, item); + } + + // Filter items with errors. + return item; + }); + + return { + items: pageItems, + hasprevpage: page > 0, + hasnextpage: currentPage > page, + warnings: response.warnings, + }; + } + } + + /** + * Convenience function to get the page we can jump. + * + * @param feedbackId Feedback ID. + * @param page Page where we want to jump. + * @param changePage If page change is forward (1) or backward (-1). + * @param options Other options. + * @return Page number where to jump. Or false if completed or first page. + */ + protected async getPageJumpTo( + feedbackId: number, + page: number, + changePage: number, + options: { cmId?: number; siteId?: string }, + ): Promise { + + const response = await this.getPageItemsWithValues(feedbackId, page, { + cmId: options.cmId, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId: options.siteId, + }); + + // The page we are going has items. + if (response.items.length > 0) { + return page; + } + + // Check we can jump futher. + if ((changePage == 1 && response.hasnextpage) || (changePage == -1 && response.hasprevpage)) { + return this.getPageJumpTo(feedbackId, page + changePage, changePage, options); + } + + // Completed or first page. + return false; + } + + /** + * Returns the feedback user responses. + * + * @param feedbackId Feedback ID. + * @param options Other options. + * @return Promise resolved when the info is retrieved. + */ + async getResponsesAnalysis( + feedbackId: number, + options: AddonModFeedbackGroupPaginatedOptions = {}, + ): Promise { + options.groupId = options.groupId || 0; + options.page = options.page || 0; + + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModFeedbackGetResponsesAnalysisWSParams = { + feedbackid: feedbackId, + groupid: options.groupId, + page: options.page, + perpage: AddonModFeedbackProvider.PER_PAGE, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getResponsesAnalysisDataCacheKey(feedbackId, options.groupId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + return site.read('mod_feedback_get_responses_analysis', params, preSets); + } + + /** + * Get cache key for responses analysis feedback data WS calls. + * + * @param feedbackId Feedback ID. + * @param groupId Group id, 0 means that the function will determine the user group. + * @return Cache key. + */ + protected getResponsesAnalysisDataCacheKey(feedbackId: number, groupId: number = 0): string { + return this.getResponsesAnalysisDataPrefixCacheKey(feedbackId) + groupId; + } + + /** + * Get prefix cache key for feedback responses analysis data WS calls. + * + * @param feedbackId Feedback ID. + * @return Cache key. + */ + protected getResponsesAnalysisDataPrefixCacheKey(feedbackId: number): string { + return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':responsesanalysis:'; + } + + /** + * Gets the resume page information. + * + * @param feedbackId Feedback ID. + * @param options Other options. + * @return Promise resolved when the info is retrieved. + */ + async getResumePage(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModFeedbackLaunchFeedbackWSParams = { + feedbackid: feedbackId, + }; + const preSets = { + cacheKey: this.getResumePageDataCacheKey(feedbackId), + component: AddonModFeedbackProvider.COMPONENT, + componentId: options.cmId, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + const response = await site.read('mod_feedback_launch_feedback', params, preSets); + + // WS will return -1 for last page but the user need to start again. + return response.gopage > 0 ? response.gopage : 0; + } + + /** + * Get prefix cache key for resume feedback page data WS calls. + * + * @param feedbackId Feedback ID. + * @return Cache key. + */ + protected getResumePageDataCacheKey(feedbackId: number): string { + return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':launch'; + } + + /** + * Invalidates feedback data except files and module info. + * + * @param feedbackId Feedback ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateAllFeedbackData(feedbackId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKeyStartingWith(this.getFeedbackDataPrefixCacheKey(feedbackId)); + } + + /** + * Invalidates feedback analysis data. + * + * @param feedbackId Feedback ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateAnalysisData(feedbackId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.invalidateWsCacheForKeyStartingWith(this.getAnalysisDataPrefixCacheKey(feedbackId)); + } + + /** + * Invalidate the prefetched content. + * To invalidate files, use AddonModFeedbackProvider#invalidateFiles. + * + * @param moduleId The module ID. + * @param courseId Course ID of the module. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const feedback = await this.getFeedback(courseId, moduleId, { siteId }); + + await Promise.all([ + this.invalidateFeedbackData(courseId, siteId), + this.invalidateAllFeedbackData(feedback.id, siteId), + ]); + } + + /** + * Invalidates temporary completion record data. + * + * @param feedbackId Feedback ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateCurrentValuesData(feedbackId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.invalidateWsCacheForKey(this.getCurrentValuesDataCacheKey(feedbackId)); + } + + /** + * Invalidates feedback access information data. + * + * @param feedbackId Feedback ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateFeedbackAccessInformationData(feedbackId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.invalidateWsCacheForKey(this.getFeedbackAccessInformationDataCacheKey(feedbackId)); + } + + /** + * Invalidates feedback data. + * + * @param courseId Course ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateFeedbackData(courseId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.invalidateWsCacheForKey(this.getFeedbackCacheKey(courseId)); + } + + /** + * Invalidate the prefetched files. + * + * @param moduleId The module ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the files are invalidated. + */ + async invalidateFiles(moduleId: number, siteId?: string): Promise { + return CoreFilepool.invalidateFilesByComponent(siteId, AddonModFeedbackProvider.COMPONENT, moduleId); + } + + /** + * Invalidates feedback non respondents record data. + * + * @param feedbackId Feedback ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateNonRespondentsData(feedbackId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKeyStartingWith(this.getNonRespondentsDataPrefixCacheKey(feedbackId)); + } + + /** + * Invalidates feedback user responses record data. + * + * @param feedbackId Feedback ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateResponsesAnalysisData(feedbackId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKeyStartingWith(this.getResponsesAnalysisDataPrefixCacheKey(feedbackId)); + } + + /** + * Invalidates launch feedback data. + * + * @param feedbackId Feedback ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateResumePageData(feedbackId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getResumePageDataCacheKey(feedbackId)); + } + + /** + * Returns if feedback has been completed + * + * @param feedbackId Feedback ID. + * @param options Other options. + * @return Promise resolved when the info is retrieved. + */ + async isCompleted(feedbackId: number, options: CoreCourseCommonModWSOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModFeedbackGetLastCompletedWSParams = { + feedbackid: feedbackId, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getCompletedDataCacheKey(feedbackId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModFeedbackProvider.COMPONENT, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. + }; + + return CoreUtils.promiseWorks(site.read('mod_feedback_get_last_completed', params, preSets)); + } + + /** + * Return whether or not the plugin is enabled in a certain site. Plugin is enabled if the feedback WS are available. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise. + * @since 3.3 + */ + async isPluginEnabled(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.wsAvailable('mod_feedback_get_feedbacks_by_courses') && + site.wsAvailable('mod_feedback_get_feedback_access_information'); + } + + /** + * Report the feedback as being viewed. + * + * @param id Module ID. + * @param name Name of the feedback. + * @param formViewed True if form was viewed. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the WS call is successful. + */ + async logView(id: number, name?: string, formViewed: boolean = false, siteId?: string): Promise { + const params: AddonModFeedbackViewFeedbackWSParams = { + feedbackid: id, + moduleviewed: formViewed, + }; + + await CoreCourseLogHelper.logSingle( + 'mod_feedback_view_feedback', + params, + AddonModFeedbackProvider.COMPONENT, + id, + name, + 'feedback', + { moduleviewed: params.moduleviewed }, + siteId, + ); + } + + /** + * Process a jump between pages. + * + * @param feedbackId Feedback ID. + * @param page The page being processed. + * @param responses The data to be processed the key is the field name (usually type[index]_id). + * @param options Other options. + * @return Promise resolved when the info is retrieved. + */ + async processPage( + feedbackId: number, + page: number, + responses: Record, + options: AddonModFeedbackProcessPageOptions = {}, + ): Promise { + options.siteId = options.siteId || CoreSites.getCurrentSiteId(); + + // Convenience function to store a message to be synchronized later. + const storeOffline = async (): Promise => { + await AddonModFeedbackOffline.saveResponses(feedbackId, page, responses, options.courseId!, options.siteId); + + // Simulate process_page response. + const response: AddonModFeedbackProcessPageResponse = { + jumpto: page, + completed: false, + offline: true, + }; + let changePage = 0; + + if (options.goPrevious) { + if (page > 0) { + changePage = -1; + } + } else if (!options.formHasErrors) { + // We can only go next if it has no errors. + changePage = 1; + } + + if (changePage === 0) { + return response; + } + + const pageItems = await this.getPageItemsWithValues(feedbackId, page, { + cmId: options.cmId, + readingStrategy: CoreSitesReadingStrategy.PreferCache, + siteId: options.siteId, + }); + + // Check completion. + if (changePage == 1 && !pageItems.hasnextpage) { + response.completed = true; + + return response; + } + + const loadPage = await this.getPageJumpTo(feedbackId, page + changePage, changePage, options); + + if (loadPage === false) { + // Completed or first page. + if (changePage == -1) { + response.jumpto = 0; + } else { + response.completed = true; + } + } else { + response.jumpto = loadPage; + } + + return response; + }; + + if (!CoreApp.isOnline()) { + // App is offline, store the action. + return storeOffline(); + } + + // If there's already a response to be sent to the server, discard it first. + await AddonModFeedbackOffline.deleteFeedbackPageResponses(feedbackId, page, options.siteId); + + try { + return await this.processPageOnline(feedbackId, page, responses, !!options.goPrevious, options.siteId); + } catch (error) { + if (CoreUtils.isWebServiceError(error)) { + // The WebService has thrown an error, this means that responses cannot be submitted. + throw error; + } + + // Couldn't connect to server, store in offline. + return storeOffline(); + } + } + + /** + * Process a jump between pages. + * + * @param feedbackId Feedback ID. + * @param page The page being processed. + * @param responses The data to be processed the key is the field name (usually type[index]_id). + * @param goPrevious Whether we want to jump to previous page. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the info is retrieved. + */ + async processPageOnline( + feedbackId: number, + page: number, + responses: Record, + goPrevious: boolean, + siteId?: string, + ): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonModFeedbackProcessPageWSParams = { + feedbackid: feedbackId, + page: page, + responses: CoreUtils.objectToArrayOfObjects(responses, 'name', 'value'), + goprevious: goPrevious, + }; + + const response = await site.write('mod_feedback_process_page', params); + + // Invalidate and update current values because they will change. + await CoreUtils.ignoreErrors(this.invalidateCurrentValuesData(feedbackId, site.getId())); + + await CoreUtils.ignoreErrors(this.getCurrentValues(feedbackId, { siteId: site.getId() })); + + return response; + } + +} + +export const AddonModFeedback = makeSingleton(AddonModFeedbackProvider); + +declare module '@singletons/events' { + + /** + * Augment CoreEventsData interface with events specific to this service. + * + * @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation + */ + export interface CoreEventsData { + [AddonModFeedbackProvider.FORM_SUBMITTED]: AddonModFeedbackFormSubmittedData; + [AddonModFeedbackSyncProvider.AUTO_SYNCED]: AddonModFeedbackAutoSyncData; + } + +} + +/** + * Data passed to FORM_SUBMITTED event. + */ +export type AddonModFeedbackFormSubmittedData = { + feedbackId: number; + tab: string; + offline: boolean; +}; + +/** + * Params of mod_feedback_get_analysis WS. + */ +export type AddonModFeedbackGetAnalysisWSParams = { + feedbackid: number; // Feedback instance id. + groupid?: number; // Group id, 0 means that the function will determine the user group. + courseid?: number; // Course where user completes the feedback (for site feedbacks only). +}; + +/** + * Data returned by mod_feedback_get_analysis WS. + */ +export type AddonModFeedbackGetAnalysisWSResponse = { + completedcount: number; // Number of completed submissions. + itemscount: number; // Number of items (questions). + itemsdata: { + item: AddonModFeedbackWSItem; + data: string[]; + }[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Item data returneds by feedback_item_exporter. + */ +export type AddonModFeedbackWSItem = { + id: number; // The record id. + feedback: number; // The feedback instance id this records belongs to. + template: number; // If it belogns to a template, the template id. + name: string; // The item name. + label: string; // The item label. + presentation: string; // The text describing the item or the available possible answers. + typ: string; // The type of the item. + hasvalue: number; // Whether it has a value or not. + position: number; // The position in the list of questions. + required: boolean; // Whether is a item (question) required or not. + dependitem: number; // The item id this item depend on. + dependvalue: string; // The depend value. + options: string; // Different additional settings for the item (question). + itemfiles: CoreWSStoredFile[]; // Itemfiles. + itemnumber: number; // The item position number. + otherdata: string; // Additional data that may be required by external functions. +}; + +/** + * Item with some calculated data. + */ +export type AddonModFeedbackItem = AddonModFeedbackWSItem & { + rawValue?: AddonModFeedbackResponseValue; +}; + +/** + * Params of mod_feedback_get_current_completed_tmp WS. + */ +export type AddonModFeedbackGetCurrentCompletedTmpWSParams = { + feedbackid: number; // Feedback instance id. + courseid?: number; // Course where user completes the feedback (for site feedbacks only). +}; + +/** + * Data returned by mod_feedback_get_current_completed_tmp WS. + */ +export type AddonModFeedbackGetCurrentCompletedTmpWSResponse = { + feedback: { + id: number; // The record id. + feedback: number; // The feedback instance id this records belongs to. + userid: number; // The user who completed the feedback (0 for anonymous). + guestid: string; // For guests, this is the session key. + timemodified: number; // The last time the feedback was completed. + // eslint-disable-next-line @typescript-eslint/naming-convention + random_response: number; // The response number (used when shuffling anonymous responses). + // eslint-disable-next-line @typescript-eslint/naming-convention + anonymous_response: number; // Whether is an anonymous response. + courseid: number; // The course id where the feedback was completed. + }; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of mod_feedback_get_unfinished_responses WS. + */ +export type AddonModFeedbackGetUnfinishedResponsesWSParams = { + feedbackid: number; // Feedback instance id. + courseid?: number; // Course where user completes the feedback (for site feedbacks only). +}; + +/** + * Data returned by mod_feedback_get_unfinished_responses WS. + */ +export type AddonModFeedbackGetUnfinishedResponsesWSResponse = { + responses: AddonModFeedbackWSUnfinishedResponse[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Unfinished response data returned by feedback_valuetmp_exporter. + */ +export type AddonModFeedbackWSUnfinishedResponse = { + id: number; // The record id. + // eslint-disable-next-line @typescript-eslint/naming-convention + course_id: number; // The course id this record belongs to. + item: number; // The item id that was responded. + completed: number; // Reference to the feedback_completedtmp table. + // eslint-disable-next-line @typescript-eslint/naming-convention + tmp_completed: number; // Old field - not used anymore. + value: string; // The response value. +}; + +/** + * Params of mod_feedback_get_finished_responses WS. + */ +export type AddonModFeedbackGetFinishedResponsesWSParams = { + feedbackid: number; // Feedback instance id. + courseid?: number; // Course where user completes the feedback (for site feedbacks only). +}; + +/** + * Data returned by mod_feedback_get_finished_responses WS. + */ +export type AddonModFeedbackGetFinishedResponsesWSResponse = { + responses: AddonModFeedbackWSFinishedResponse[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Unfinished response data returned by feedback_value_exporter. + */ +export type AddonModFeedbackWSFinishedResponse = { + id: number; // The record id. + // eslint-disable-next-line @typescript-eslint/naming-convention + course_id: number; // The course id this record belongs to. + item: number; // The item id that was responded. + completed: number; // Reference to the feedback_completed table. + // eslint-disable-next-line @typescript-eslint/naming-convention + tmp_completed: number; // Old field - not used anymore. + value: string; // The response value. +}; + +/** + * A response, either finished or unfinished. + */ +export type AddonModFeedbackWSResponse = AddonModFeedbackWSFinishedResponse | AddonModFeedbackWSUnfinishedResponse; + +/** + * Params of mod_feedback_get_feedback_access_information WS. + */ +export type AddonModFeedbackGetFeedbackAccessInformationWSParams = { + feedbackid: number; // Feedback instance id. + courseid?: number; // Course where user completes the feedback (for site feedbacks only). +}; + +/** + * Data returned by mod_feedback_get_feedback_access_information WS. + */ +export type AddonModFeedbackGetFeedbackAccessInformationWSResponse = { + canviewanalysis: boolean; // Whether the user can view the analysis or not. + cancomplete: boolean; // Whether the user can complete the feedback or not. + cansubmit: boolean; // Whether the user can submit the feedback or not. + candeletesubmissions: boolean; // Whether the user can delete submissions or not. + canviewreports: boolean; // Whether the user can view the feedback reports or not. + canedititems: boolean; // Whether the user can edit feedback items or not. + isempty: boolean; // Whether the feedback has questions or not. + isopen: boolean; // Whether the feedback has active access time restrictions or not. + isalreadysubmitted: boolean; // Whether the feedback is already submitted or not. + isanonymous: boolean; // Whether the feedback is anonymous or not. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of mod_feedback_get_feedbacks_by_courses WS. + */ +export type AddonModFeedbackGetFeedbacksByCoursesWSParams = { + courseids?: number[]; // Array of course ids. +}; + +/** + * Data returned by mod_feedback_get_feedbacks_by_courses WS. + */ +export type AddonModFeedbackGetFeedbacksByCoursesWSResponse = { + feedbacks: AddonModFeedbackWSFeedback[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Feedback data returned by mod_feedback_get_feedbacks_by_courses WS. + */ +export type AddonModFeedbackWSFeedback = { + id: number; // The primary key of the record. + course: number; // Course id this feedback is part of. + name: string; // Feedback name. + intro: string; // Feedback introduction text. + introformat?: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + anonymous: number; // Whether the feedback is anonymous. + // eslint-disable-next-line @typescript-eslint/naming-convention + email_notification?: boolean; // Whether email notifications will be sent to teachers. + // eslint-disable-next-line @typescript-eslint/naming-convention + multiple_submit: boolean; // Whether multiple submissions are allowed. + autonumbering: boolean; // Whether questions should be auto-numbered. + // eslint-disable-next-line @typescript-eslint/naming-convention + site_after_submit?: string; // Link to next page after submission. + // eslint-disable-next-line @typescript-eslint/naming-convention + page_after_submit?: string; // Text to display after submission. + // eslint-disable-next-line @typescript-eslint/naming-convention + page_after_submitformat?: number; // Page_after_submit format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + // eslint-disable-next-line @typescript-eslint/naming-convention + publish_stats: boolean; // Whether stats should be published. + timeopen?: number; // Allow answers from this time. + timeclose?: number; // Allow answers until this time. + timemodified?: number; // The time this record was modified. + completionsubmit: boolean; // If set to 1, then the activity will be automatically marked as complete on submission. + coursemodule: number; // Coursemodule. + introfiles: CoreWSExternalFile[]; // Introfiles. + pageaftersubmitfiles?: CoreWSExternalFile[]; // Pageaftersubmitfiles. +}; + +/** + * Params of mod_feedback_get_items WS. + */ +export type AddonModFeedbackGetItemsWSParams = { + feedbackid: number; // Feedback instance id. + courseid?: number; // Course where user completes the feedback (for site feedbacks only). +}; + +/** + * Data returned by mod_feedback_get_items WS. + */ +export type AddonModFeedbackGetItemsWSResponse = { + items: AddonModFeedbackWSItem[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of mod_feedback_get_non_respondents WS. + */ +export type AddonModFeedbackGetNonRespondentsWSParams = { + feedbackid: number; // Feedback instance id. + groupid?: number; // Group id, 0 means that the function will determine the user group. + sort?: string; // Sort param, must be firstname, lastname or lastaccess (default). + page?: number; // The page of records to return. + perpage?: number; // The number of records to return per page. + courseid?: number; // Course where user completes the feedback (for site feedbacks only). +}; + +/** + * Data returned by mod_feedback_get_non_respondents WS. + */ +export type AddonModFeedbackGetNonRespondentsWSResponse = { + users: AddonModFeedbackWSNonRespondent[]; + total: number; // Total number of non respondents. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Data returned by mod_feedback_get_non_respondents WS. + */ +export type AddonModFeedbackWSNonRespondent = { + courseid: number; // Course id. + userid: number; // The user id. + fullname: string; // User full name. + started: boolean; // If the user has started the attempt. +}; + +/** + * Params of mod_feedback_get_page_items WS. + */ +export type AddonModFeedbackGetPageItemsWSParams = { + feedbackid: number; // Feedback instance id. + page: number; // The page to get starting by 0. + courseid?: number; // Course where user completes the feedback (for site feedbacks only). +}; + +/** + * Data returned by mod_feedback_get_page_items WS. + */ +export type AddonModFeedbackGetPageItemsWSResponse = { + items: AddonModFeedbackWSItem[]; + hasprevpage: boolean; // Whether is a previous page. + hasnextpage: boolean; // Whether there are more pages. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Page items with some calculated data. + */ +export type AddonModFeedbackPageItems = Omit & { + items: AddonModFeedbackItem[]; +}; + +/** + * Params of mod_feedback_get_responses_analysis WS. + */ +export type AddonModFeedbackGetResponsesAnalysisWSParams = { + feedbackid: number; // Feedback instance id. + groupid?: number; // Group id, 0 means that the function will determine the user group. + page?: number; // The page of records to return. + perpage?: number; // The number of records to return per page. + courseid?: number; // Course where user completes the feedback (for site feedbacks only). +}; + +/** + * Data returned by mod_feedback_get_responses_analysis WS. + */ +export type AddonModFeedbackGetResponsesAnalysisWSResponse = { + attempts: AddonModFeedbackWSAttempt[]; + totalattempts: number; // Total responses count. + anonattempts: AddonModFeedbackWSAnonAttempt[]; + totalanonattempts: number; // Total anonymous responses count. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Attempt data returned by mod_feedback_get_responses_analysis WS. + */ +export type AddonModFeedbackWSAttempt = { + id: number; // Completed id. + courseid: number; // Course id. + userid: number; // User who responded. + timemodified: number; // Time modified for the response. + fullname: string; // User full name. + responses: AddonModFeedbackWSAttemptResponse[]; +}; + +/** + * Anonymous attempt data returned by mod_feedback_get_responses_analysis WS. + */ +export type AddonModFeedbackWSAnonAttempt = { + id: number; // Completed id. + courseid: number; // Course id. + // eslint-disable-next-line id-blacklist + number: number; // Response number. + responses: AddonModFeedbackWSAttemptResponse[]; +}; + +/** + * Response data returned by mod_feedback_get_responses_analysis WS. + */ +export type AddonModFeedbackWSAttemptResponse = { + id: number; // Response id. + name: string; // Response name. + printval: string; // Response ready for output. + rawval: string; // Response raw value. +}; + +/** + * Params of mod_feedback_launch_feedback WS. + */ +export type AddonModFeedbackLaunchFeedbackWSParams = { + feedbackid: number; // Feedback instance id. + courseid?: number; // Course where user completes the feedback (for site feedbacks only). +}; + +/** + * Data returned by mod_feedback_launch_feedback WS. + */ +export type AddonModFeedbackLaunchFeedbackWSResponse = { + gopage: number; // The next page to go (-1 if we were already in the last page). 0 for first page. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Params of mod_feedback_get_last_completed WS. + */ +export type AddonModFeedbackGetLastCompletedWSParams = { + feedbackid: number; // Feedback instance id. + courseid?: number; // Course where user completes the feedback (for site feedbacks only). +}; + +/** + * Params of mod_feedback_view_feedback WS. + */ +export type AddonModFeedbackViewFeedbackWSParams = { + feedbackid: number; // Feedback instance id. + moduleviewed?: boolean; // If we need to mark the module as viewed for completion. + courseid?: number; // Course where user completes the feedback (for site feedbacks only). +}; + +/** + * Params of mod_feedback_process_page WS. + */ +export type AddonModFeedbackProcessPageWSParams = { + feedbackid: number; // Feedback instance id. + page: number; // The page being processed. + responses?: { // The data to be processed. + name: string; // The response name (usually type[index]_id). + value: string | number; // The response value. + }[]; + goprevious?: boolean; // Whether we want to jump to previous page. + courseid?: number; // Course where user completes the feedback (for site feedbacks only). +}; + +/** + * Data returned by mod_feedback_process_page WS. + */ +export type AddonModFeedbackProcessPageWSResponse = { + jumpto: number; // The page to jump to. + completed: boolean; // If the user completed the feedback. + completionpagecontents: string; // The completion page contents. + siteaftersubmit: string; // The link (could be relative) to show after submit. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Data returned by process page. + */ +export type AddonModFeedbackProcessPageResponse = { + jumpto: number | null; // The page to jump to. + completed: boolean; // If the user completed the feedback. + offline?: boolean; // Whether data has been stored in offline. +} & Partial; + +/** + * Common options with a group ID. + */ +export type AddonModFeedbackGroupOptions = CoreCourseCommonModWSOptions & { + groupId?: number; // Group id, 0 means that the function will determine the user group. Defaults to 0. +}; + +/** + * Common options with a group ID and page. + */ +export type AddonModFeedbackGroupPaginatedOptions = AddonModFeedbackGroupOptions & { + page?: number; // The page of records to return. The page of records to return. +}; + +/** + * Common options with a group ID and page. + */ +export type AddonModFeedbackProcessPageOptions = { + goPrevious?: boolean; // Whether we want to jump to previous page. + formHasErrors?: boolean; // Whether the form we sent has required but empty fields (only used in offline). + cmId?: number; // Module ID. + courseId?: number; // Course ID the feedback belongs to. + siteId?: string; // Site ID. If not defined, current site.; +}; + +/** + * Possible types of responses. + */ +export type AddonModFeedbackResponseValue = string | number; + +type OfflineResponsesArray = { + id: string; + value: AddonModFeedbackResponseValue; +}[]; + +/** + * Previous non respondents when using recursive function. + */ +export type AddonModFeedbackPreviousNonRespondents = { + page: number; + users: AddonModFeedbackWSNonRespondent[]; +}; + +/** + * All non respondents. + */ +export type AddonModFeedbackAllNonRespondent = AddonModFeedbackPreviousNonRespondents & { + total: number; +}; + +export type AddonModFeedbackPreviousResponsesAnalysis = { + page: number; + attempts: AddonModFeedbackWSAttempt[]; + anonattempts: AddonModFeedbackWSAnonAttempt[]; +}; + +export type AddonModFeedbackAllResponsesAnalysis = AddonModFeedbackPreviousResponsesAnalysis & { + totalattempts: number; + totalanonattempts: number; +}; + +export type AddonModFeedbackGetAttemptPreviousData = { + page: number; + attemptsLoaded: number; + anonAttemptsLoaded: number; +}; diff --git a/src/addons/mod/feedback/services/handlers/analysis-link.ts b/src/addons/mod/feedback/services/handlers/analysis-link.ts new file mode 100644 index 000000000..d3c06a61f --- /dev/null +++ b/src/addons/mod/feedback/services/handlers/analysis-link.ts @@ -0,0 +1,92 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { makeSingleton } from '@singletons'; +import { AddonModFeedback } from '../feedback'; +import { AddonModFeedbackModuleHandlerService } from './module'; + +/** + * Content links handler for a feedback analysis. + * Match mod/feedback/analysis.php with a valid feedback id. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModFeedbackAnalysisLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonModFeedbackAnalysisLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModFeedback'; + pattern = /\/mod\/feedback\/analysis\.php.*([&?]id=\d+)/; + + /** + * @inheritdoc + */ + getActions(siteIds: string[], url: string, params: Record): CoreContentLinksAction[] { + return [{ + action: async (siteId: string) => { + const modal = await CoreDomUtils.showModalLoading(); + + const moduleId = Number(params.id); + + try { + const moduleBasicInfo = await CoreCourse.getModuleBasicInfo(moduleId, siteId); + + // Get the module. + const module = await CoreCourse.getModule( + moduleId, + moduleBasicInfo.course, + moduleBasicInfo.section, + false, + false, + siteId, + ); + + CoreNavigator.navigateToSitePath( + AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${module.course}/${module.id}`, + { + params: { + module, + tab: 'analysis', + }, + siteId, + }, + ); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'Error opening link.'); + } finally { + modal.dismiss(); + } + }, + }]; + } + + /** + * @inheritdoc + */ + async isEnabled(siteId: string, url: string, params: Record): Promise { + if (typeof params.id == 'undefined') { + // Cannot treat the URL. + return false; + } + + return AddonModFeedback.isPluginEnabled(siteId); + } + +} + +export const AddonModFeedbackAnalysisLinkHandler = makeSingleton(AddonModFeedbackAnalysisLinkHandlerService); diff --git a/src/addons/mod/feedback/services/handlers/complete-link.ts b/src/addons/mod/feedback/services/handlers/complete-link.ts new file mode 100644 index 000000000..94d46c83d --- /dev/null +++ b/src/addons/mod/feedback/services/handlers/complete-link.ts @@ -0,0 +1,80 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { makeSingleton } from '@singletons'; +import { AddonModFeedback } from '../feedback'; +import { AddonModFeedbackModuleHandlerService } from './module'; + +/** + * Content links handler for feedback complete questions. + * Match mod/feedback/complete.php with a valid feedback id. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModFeedbackCompleteLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonModFeedbackCompleteLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModFeedback'; + pattern = /\/mod\/feedback\/complete\.php.*([?&](id|gopage)=\d+)/; + + /** + * @inheritdoc + */ + getActions(siteIds: string[], url: string, params: Record): CoreContentLinksAction[] { + return [{ + action: async (siteId: string) => { + const modal = await CoreDomUtils.showModalLoading(); + + const moduleId = Number(params.id); + + try { + const module = await CoreCourse.getModuleBasicInfo(moduleId, siteId); + + CoreNavigator.navigateToSitePath( + AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${module.course}/${module.id}/form`, + { + params: { + page: typeof params.gopage != 'undefined' ? Number(params.gopage) : undefined, + }, + siteId, + }, + ); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'Error opening link.'); + } finally { + modal.dismiss(); + } + }, + }]; + } + + /** + * @inheritdoc + */ + async isEnabled(siteId: string, url: string, params: Record): Promise { + if (typeof params.id == 'undefined') { + return false; + } + + return AddonModFeedback.isPluginEnabled(siteId); + } + +} + +export const AddonModFeedbackCompleteLinkHandler = makeSingleton(AddonModFeedbackCompleteLinkHandlerService); diff --git a/src/addons/mod/feedback/services/handlers/index-link.ts b/src/addons/mod/feedback/services/handlers/index-link.ts new file mode 100644 index 000000000..319112eb6 --- /dev/null +++ b/src/addons/mod/feedback/services/handlers/index-link.ts @@ -0,0 +1,47 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler'; +import { makeSingleton } from '@singletons'; +import { AddonModFeedback } from '../feedback'; + +/** + * Handler to treat links to feedback. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModFeedbackIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler { + + name = 'AddonModFeedbackLinkHandler'; + + constructor() { + super('AddonModFeedback', 'feedback'); + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * + * @param siteId The site ID. + * @param url The URL to treat. + * @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param courseId Course ID related to the URL. Optional but recommended. + * @return Whether the handler is enabled for the URL and site. + */ + isEnabled(): Promise { + return AddonModFeedback.isPluginEnabled(); + } + +} + +export const AddonModFeedbackIndexLinkHandler = makeSingleton(AddonModFeedbackIndexLinkHandlerService); diff --git a/src/addons/mod/feedback/services/handlers/list-link.ts b/src/addons/mod/feedback/services/handlers/list-link.ts new file mode 100644 index 000000000..9f62a378c --- /dev/null +++ b/src/addons/mod/feedback/services/handlers/list-link.ts @@ -0,0 +1,41 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler'; +import { makeSingleton } from '@singletons'; +import { AddonModFeedback } from '../feedback'; + +/** + * Handler to treat links to feedback list page. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModFeedbackListLinkHandlerService extends CoreContentLinksModuleListHandler { + + name = 'AddonModFeedbackListLinkHandler'; + + constructor() { + super('AddonModFeedback', 'feedback'); + } + + /** + * @inheritdoc + */ + isEnabled(): Promise { + return AddonModFeedback.isPluginEnabled(); + } + +} + +export const AddonModFeedbackListLinkHandler = makeSingleton(AddonModFeedbackListLinkHandlerService); diff --git a/src/addons/mod/feedback/services/handlers/module.ts b/src/addons/mod/feedback/services/handlers/module.ts new file mode 100644 index 000000000..fba3e5862 --- /dev/null +++ b/src/addons/mod/feedback/services/handlers/module.ts @@ -0,0 +1,84 @@ +// (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 { CoreConstants } from '@/core/constants'; +import { Injectable, Type } from '@angular/core'; +import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; +import { CoreCourseModule } from '@features/course/services/course-helper'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; +import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; +import { makeSingleton } from '@singletons'; +import { AddonModFeedback } from '../feedback'; +import { AddonModFeedbackIndexComponent } from '../../components/index'; + +/** + * Handler to support feedback modules. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModFeedbackModuleHandlerService implements CoreCourseModuleHandler { + + static readonly PAGE_NAME = 'mod_feedback'; + + name = 'AddonModFeedback'; + modName = 'feedback'; + + supportedFeatures = { + [CoreConstants.FEATURE_GROUPS]: true, + [CoreConstants.FEATURE_GROUPINGS]: true, + [CoreConstants.FEATURE_MOD_INTRO]: true, + [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true, + [CoreConstants.FEATURE_COMPLETION_HAS_RULES]: true, + [CoreConstants.FEATURE_GRADE_HAS_GRADE]: false, + [CoreConstants.FEATURE_GRADE_OUTCOMES]: false, + [CoreConstants.FEATURE_BACKUP_MOODLE2]: true, + [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true, + }; + + /** + * @inheritdoc + */ + isEnabled(): Promise { + return AddonModFeedback.isPluginEnabled(); + } + + /** + * @inheritdoc + */ + getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData { + return { + icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined), + title: module.name, + class: 'addon-mod_feedback-handler', + showDownloadButton: true, + action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void { + options = options || {}; + options.params = options.params || {}; + Object.assign(options.params, { module }); + const routeParams = '/' + courseId + '/' + module.id; + + CoreNavigator.navigateToSitePath(AddonModFeedbackModuleHandlerService.PAGE_NAME + routeParams, options); + }, + }; + } + + /** + * @inheritdoc + */ + async getMainComponent(): Promise> { + return AddonModFeedbackIndexComponent; + } + +} + +export const AddonModFeedbackModuleHandler = makeSingleton(AddonModFeedbackModuleHandlerService); diff --git a/src/addons/mod/feedback/services/handlers/prefetch.ts b/src/addons/mod/feedback/services/handlers/prefetch.ts new file mode 100644 index 000000000..5ed1b4746 --- /dev/null +++ b/src/addons/mod/feedback/services/handlers/prefetch.ts @@ -0,0 +1,228 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler'; +import { CoreCourseAnyModuleData, CoreCourseCommonModWSOptions } from '@features/course/services/course'; +import { CoreFilepool } from '@services/filepool'; +import { CoreGroups } from '@services/groups'; +import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreTimeUtils } from '@services/utils/time'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreWSFile } from '@services/ws'; +import { makeSingleton } from '@singletons'; +import { + AddonModFeedback, + AddonModFeedbackGetFeedbackAccessInformationWSResponse, + AddonModFeedbackProvider, + AddonModFeedbackWSFeedback, +} from '../feedback'; +import { AddonModFeedbackSync, AddonModFeedbackSyncResult } from '../feedback-sync'; + +/** + * Handler to prefetch feedbacks. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModFeedbackPrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase { + + name = 'AddonModFeedback'; + modName = 'feedback'; + component = AddonModFeedbackProvider.COMPONENT; + updatesNames = /^configuration$|^.*files$|^attemptsfinished|^attemptsunfinished$/; + + /** + * @inheritdoc + */ + async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { + let files: CoreWSFile[] = []; + + const feedback = await AddonModFeedback.getFeedback(courseId, module.id); + + // Get intro files and page after submit files. + files = feedback.pageaftersubmitfiles || []; + files = files.concat(this.getIntroFilesFromInstance(module, feedback)); + + try { + const response = await AddonModFeedback.getItems(feedback.id); + + response.items.forEach((item) => { + files = files.concat(item.itemfiles); + }); + } catch (e) { + // Ignore errors. + } + + return files; + } + + /** + * @inheritdoc + */ + async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { + const feedback = await CoreUtils.ignoreErrors(AddonModFeedback.getFeedback(courseId, module.id)); + + return this.getIntroFilesFromInstance(module, feedback); + } + + /** + * @inheritdoc + */ + invalidateContent(moduleId: number, courseId: number): Promise { + return AddonModFeedback.invalidateContent(moduleId, courseId); + } + + /** + * @inheritdoc + */ + invalidateModule(module: CoreCourseAnyModuleData, courseId: number): Promise { + return AddonModFeedback.invalidateFeedbackData(courseId); + } + + /** + * @inheritdoc + */ + async isDownloadable(module: CoreCourseAnyModuleData, courseId: number): Promise { + const feedback = await AddonModFeedback.getFeedback(courseId, module.id, { + readingStrategy: CoreSitesReadingStrategy.PreferCache, + }); + + const now = CoreTimeUtils.timestamp(); + + // Check time first if available. + if (feedback.timeopen && feedback.timeopen > now) { + return false; + } + if (feedback.timeclose && feedback.timeclose < now) { + return false; + } + + const accessData = await AddonModFeedback.getFeedbackAccessInformation(feedback.id, { cmId: module.id }); + + return accessData.isopen; + } + + /** + * @inheritdoc + */ + isEnabled(): Promise { + return AddonModFeedback.isPluginEnabled(); + } + + /** + * @inheritdoc + */ + prefetch(module: CoreCourseAnyModuleData, courseId?: number): Promise { + return this.prefetchPackage(module, courseId, this.prefetchFeedback.bind(this, module, courseId)); + } + + /** + * Prefetch a feedback. + * + * @param module Module. + * @param courseId Course ID the module belongs to. + * @return Promise resolved when done. + */ + protected async prefetchFeedback(module: CoreCourseAnyModuleData, courseId: number): Promise { + const siteId = CoreSites.getCurrentSiteId(); + const commonOptions = { + readingStrategy: CoreSitesReadingStrategy.OnlyNetwork, + siteId, + }; + const modOptions = { + cmId: module.id, + ...commonOptions, // Include all common options. + }; + + // Prefetch the feedback data. + const feedback = await AddonModFeedback.getFeedback(courseId, module.id, commonOptions); + + let files: CoreWSFile[] = feedback.pageaftersubmitfiles || []; + files = files.concat(this.getIntroFilesFromInstance(module, feedback)); + + const accessData = await AddonModFeedback.getFeedbackAccessInformation(feedback.id, modOptions); + + const promises: Promise[] = []; + + if (accessData.canedititems || accessData.canviewreports) { + // Get all groups analysis. + promises.push(AddonModFeedback.getAnalysis(feedback.id, modOptions)); + promises.push(this.prefetchAllGroupsAnalysis(feedback, accessData, modOptions)); + } + + promises.push(AddonModFeedback.getItems(feedback.id, commonOptions).then((response) => { + response.items.forEach((item) => { + files = files.concat(item.itemfiles); + }); + + return CoreFilepool.addFilesToQueue(siteId, files, this.component, module.id); + })); + + if (accessData.cancomplete && accessData.cansubmit && !accessData.isempty) { + // Send empty data, so it will recover last completed feedback attempt values. + promises.push(AddonModFeedback.processPageOnline(feedback.id, 0, {}, false, siteId).then(() => Promise.all([ + AddonModFeedback.getCurrentValues(feedback.id, modOptions), + AddonModFeedback.getResumePage(feedback.id, modOptions), + ]))); + } + + await Promise.all(promises); + } + + /** + * Prefetch all groups analysis. + * + * @param feedback Feedback. + * @param accessData Access info. + * @param modOptions Options. + */ + protected async prefetchAllGroupsAnalysis( + feedback: AddonModFeedbackWSFeedback, + accessData: AddonModFeedbackGetFeedbackAccessInformationWSResponse, + modOptions: CoreCourseCommonModWSOptions, + ): Promise { + const groupInfo = await CoreGroups.getActivityGroupInfo(feedback.coursemodule, true, undefined, modOptions.siteId, true); + + const promises: Promise[] = []; + + if (!groupInfo.groups || groupInfo.groups.length == 0) { + groupInfo.groups = [{ id: 0, name: '' }]; + } + + groupInfo.groups.forEach((group) => { + const groupOptions = { + groupId: group.id, + ...modOptions, // Include all mod options. + }; + + promises.push(AddonModFeedback.getAnalysis(feedback.id, groupOptions)); + promises.push(AddonModFeedback.getAllResponsesAnalysis(feedback.id, groupOptions)); + + if (!accessData.isanonymous) { + promises.push(AddonModFeedback.getAllNonRespondents(feedback.id, groupOptions)); + } + }); + + await Promise.all(promises); + } + + /** + * @inheritdoc + */ + sync(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise { + return AddonModFeedbackSync.syncFeedback(module.instance!, siteId); + } + +} + +export const AddonModFeedbackPrefetchHandler = makeSingleton(AddonModFeedbackPrefetchHandlerService); diff --git a/src/addons/mod/feedback/services/handlers/print-link.ts b/src/addons/mod/feedback/services/handlers/print-link.ts new file mode 100644 index 000000000..b74bab793 --- /dev/null +++ b/src/addons/mod/feedback/services/handlers/print-link.ts @@ -0,0 +1,80 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { makeSingleton } from '@singletons'; +import { AddonModFeedback } from '../feedback'; +import { AddonModFeedbackModuleHandlerService } from './module'; + +/** + * Content links handler for feedback print questions. + * Match mod/feedback/print.php with a valid feedback id. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModFeedbackPrintLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonModFeedbackPrintLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModFeedback'; + pattern = /\/mod\/feedback\/print\.php.*([?&](id)=\d+)/; + + /** + * @inheritdoc + */ + getActions(siteIds: string[], url: string, params: Record): CoreContentLinksAction[] { + return [{ + action: async (siteId: string) => { + const modal = await CoreDomUtils.showModalLoading(); + + const moduleId = Number(params.id); + + try { + const module = await CoreCourse.getModuleBasicInfo(moduleId, siteId); + + CoreNavigator.navigateToSitePath( + AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${module.course}/${module.id}/form`, + { + params: { + preview: true, + }, + siteId, + }, + ); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'Error opening link.'); + } finally { + modal.dismiss(); + } + }, + }]; + } + + /** + * @inheritdoc + */ + async isEnabled(siteId: string, url: string, params: Record): Promise { + if (typeof params.id == 'undefined') { + return false; + } + + return AddonModFeedback.isPluginEnabled(siteId); + } + +} + +export const AddonModFeedbackPrintLinkHandler = makeSingleton(AddonModFeedbackPrintLinkHandlerService); diff --git a/src/addons/mod/feedback/services/handlers/push-click.ts b/src/addons/mod/feedback/services/handlers/push-click.ts new file mode 100644 index 000000000..e5119b79a --- /dev/null +++ b/src/addons/mod/feedback/services/handlers/push-click.ts @@ -0,0 +1,70 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreCourseHelper } from '@features/course/services/course-helper'; +import { CorePushNotificationsClickHandler } from '@features/pushnotifications/services/push-delegate'; +import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications'; +import { CoreUrlUtils } from '@services/utils/url'; +import { CoreUtils } from '@services/utils/utils'; +import { makeSingleton } from '@singletons'; +import { AddonModFeedback } from '../feedback'; +import { AddonModFeedbackHelper } from '../feedback-helper'; + +/** + * Handler for feedback push notifications clicks. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModFeedbackPushClickHandlerService implements CorePushNotificationsClickHandler { + + name = 'AddonModFeedbackPushClickHandler'; + priority = 200; + featureName = 'CoreCourseModuleDelegate_AddonModFeedback'; + + /** + * @inheritdoc + */ + async handles(notification: CorePushNotificationsNotificationBasicData): Promise { + if (CoreUtils.isTrueOrOne(notification.notif) && notification.moodlecomponent == 'mod_feedback' && + (notification.name == 'submission' || notification.name == 'message')) { + + return AddonModFeedback.isPluginEnabled(notification.site); + } + + return false; + } + + /** + * @inheritdoc + */ + handleClick(notification: AddonModFeedbackPushNotificationData): Promise { + const contextUrlParams = CoreUrlUtils.extractUrlParams(notification.contexturl!); + const courseId = Number(notification.courseid); + const moduleId = Number(contextUrlParams.id); + + if (notification.name == 'submission') { + return AddonModFeedbackHelper.handleShowEntriesLink(contextUrlParams, notification.site); + } else { + return CoreCourseHelper.navigateToModule(moduleId, notification.site, courseId); + } + } + +} + +export const AddonModFeedbackPushClickHandler = makeSingleton(AddonModFeedbackPushClickHandlerService); + +type AddonModFeedbackPushNotificationData = CorePushNotificationsNotificationBasicData & { + contexturl?: string; + courseid?: number | string; +}; diff --git a/src/addons/mod/feedback/services/handlers/show-entries-link.ts b/src/addons/mod/feedback/services/handlers/show-entries-link.ts new file mode 100644 index 000000000..6f91981bc --- /dev/null +++ b/src/addons/mod/feedback/services/handlers/show-entries-link.ts @@ -0,0 +1,58 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; +import { makeSingleton } from '@singletons'; +import { AddonModFeedback } from '../feedback'; +import { AddonModFeedbackHelper } from '../feedback-helper'; + +/** + * Content links handler for feedback show entries questions. + * Match mod/feedback/show_entries.php with a valid feedback id. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModFeedbackShowEntriesLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonModFeedbackShowEntriesLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModFeedback'; + pattern = /\/mod\/feedback\/show_entries\.php.*([?&](id|showcompleted)=\d+)/; + + /** + * @inheritdoc + */ + getActions(siteIds: string[], url: string, params: Record): CoreContentLinksAction[] { + return [{ + action: (siteId: string) => { + AddonModFeedbackHelper.handleShowEntriesLink(params, siteId); + }, + }]; + } + + /** + * @inheritdoc + */ + async isEnabled(siteId: string, url: string, params: Record): Promise { + if (typeof params.id == 'undefined') { + // Cannot treat the URL. + return false; + } + + return AddonModFeedback.isPluginEnabled(siteId); + } + +} + +export const AddonModFeedbackShowEntriesLinkHandler = makeSingleton(AddonModFeedbackShowEntriesLinkHandlerService); diff --git a/src/addons/mod/feedback/services/handlers/show-non-respondents-link.ts b/src/addons/mod/feedback/services/handlers/show-non-respondents-link.ts new file mode 100644 index 000000000..c133301ec --- /dev/null +++ b/src/addons/mod/feedback/services/handlers/show-non-respondents-link.ts @@ -0,0 +1,75 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { makeSingleton } from '@singletons'; +import { AddonModFeedback } from '../feedback'; +import { AddonModFeedbackModuleHandlerService } from './module'; +/** + * Content links handler for feedback show non respondents. + * Match mod/feedback/show_nonrespondents.php with a valid feedback id. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModFeedbackShowNonRespondentsLinkHandlerService extends CoreContentLinksHandlerBase { + + name = 'AddonModFeedbackShowNonRespondentsLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModFeedback'; + pattern = /\/mod\/feedback\/show_nonrespondents\.php.*([?&](id)=\d+)/; + + /** + * @inheritdoc + */ + getActions(siteIds: string[], url: string, params: Record): CoreContentLinksAction[] { + return [{ + action: async (siteId: string) => { + const modal = await CoreDomUtils.showModalLoading(); + + const moduleId = Number(params.id); + + try { + const module = await CoreCourse.getModuleBasicInfo(moduleId, siteId); + + await CoreNavigator.navigateToSitePath( + AddonModFeedbackModuleHandlerService.PAGE_NAME + `/${module.course}/${module.id}/nonrespondents`, + { siteId }, + ); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'Error opening link.'); + } finally { + modal.dismiss(); + } + }, + }]; + } + + /** + * @inheritdoc + */ + async isEnabled(siteId: string, url: string, params: Record): Promise { + if (typeof params.id == 'undefined') { + // Cannot treat the URL. + return false; + } + + return AddonModFeedback.isPluginEnabled(siteId); + } + +} + +export const AddonModFeedbackShowNonRespondentsLinkHandler = makeSingleton(AddonModFeedbackShowNonRespondentsLinkHandlerService); diff --git a/src/addons/mod/feedback/services/handlers/sync-cron.ts b/src/addons/mod/feedback/services/handlers/sync-cron.ts new file mode 100644 index 000000000..bf86e90d2 --- /dev/null +++ b/src/addons/mod/feedback/services/handlers/sync-cron.ts @@ -0,0 +1,51 @@ +// (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 { Injectable } from '@angular/core'; +import { CoreCronHandler } from '@services/cron'; +import { makeSingleton } from '@singletons'; +import { AddonModFeedbackSync } from '../feedback-sync'; + +/** + * Synchronization cron handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModFeedbackSyncCronHandlerService implements CoreCronHandler { + + name = 'AddonModFeedbackSyncCronHandler'; + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param siteId ID of the site affected, undefined for all sites. + * @param force Wether the execution is forced (manual sync). + * @return Promise resolved when done, rejected if failure. + */ + execute(siteId?: string, force?: boolean): Promise { + return AddonModFeedbackSync.syncAllFeedbacks(siteId, force); + } + + /** + * Get the time between consecutive executions. + * + * @return Time between consecutive executions (in ms). + */ + getInterval(): number { + return AddonModFeedbackSync.syncInterval; + } + +} + +export const AddonModFeedbackSyncCronHandler = makeSingleton(AddonModFeedbackSyncCronHandlerService); diff --git a/src/addons/mod/forum/components/edit-post/edit-post.ts b/src/addons/mod/forum/components/edit-post/edit-post.ts index 1070e5c4b..e3a81636c 100644 --- a/src/addons/mod/forum/components/edit-post/edit-post.ts +++ b/src/addons/mod/forum/components/edit-post/edit-post.ts @@ -14,13 +14,14 @@ import { Component, ViewChild, ElementRef, Input, OnInit } from '@angular/core'; import { FormControl } from '@angular/forms'; -import { CoreFileEntry, CoreFileUploader } from '@features/fileuploader/services/fileuploader'; +import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { ModalController, Translate } from '@singletons'; import { AddonModForumData, AddonModForumPost, AddonModForumReply } from '@addons/mod/forum/services/forum'; import { AddonModForumHelper } from '@addons/mod/forum/services/forum-helper'; import { CoreForms } from '@singletons/form'; +import { CoreFileEntry } from '@services/file-helper'; /** * Page that displays a form to edit discussion post. diff --git a/src/addons/mod/forum/components/post/post.ts b/src/addons/mod/forum/components/post/post.ts index e86cfd393..cd394baf3 100644 --- a/src/addons/mod/forum/components/post/post.ts +++ b/src/addons/mod/forum/components/post/post.ts @@ -38,11 +38,10 @@ import { AddonModForumProvider, AddonModForumReply, AddonModForumUpdateDiscussionPostWSOptionsObject, - AddonModForumWSPostAttachment, } from '../../services/forum'; import { CoreTag } from '@features/tag/services/tag'; import { ModalController, PopoverController, Translate } from '@singletons'; -import { CoreFileEntry, CoreFileUploader } from '@features/fileuploader/services/fileuploader'; +import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; import { IonContent } from '@ionic/angular'; import { AddonModForumSync } from '../../services/forum-sync'; import { CoreSync } from '@services/sync'; @@ -54,6 +53,7 @@ import { AddonModForumPostOptionsMenuComponent } from '../post-options-menu/post import { AddonModForumEditPostComponent } from '../edit-post/edit-post'; import { CoreRatingInfo } from '@features/rating/services/rating'; import { CoreForms } from '@singletons/form'; +import { CoreFileEntry } from '@services/file-helper'; /** * Components that shows a discussion post, its attachments and the action buttons allowed (reply, etc.). @@ -186,7 +186,7 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy, OnChanges isEditing?: boolean, subject?: string, message?: string, - files?: (CoreFileEntry | AddonModForumWSPostAttachment)[], + files?: CoreFileEntry[], isPrivate?: boolean, ): void { // Delete the local files from the tmp folder if any. diff --git a/src/addons/mod/forum/services/forum-helper.ts b/src/addons/mod/forum/services/forum-helper.ts index c479f1045..b3a570eef 100644 --- a/src/addons/mod/forum/services/forum-helper.ts +++ b/src/addons/mod/forum/services/forum-helper.ts @@ -14,7 +14,7 @@ import { Injectable } from '@angular/core'; import { FileEntry } from '@ionic-native/file/ngx'; -import { CoreFileEntry, CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; +import { CoreFileUploader, CoreFileUploaderStoreFilesResult } from '@features/fileuploader/services/fileuploader'; import { CoreUser } from '@features/user/services/user'; import { CoreApp } from '@services/app'; import { CoreFile } from '@services/file'; @@ -31,6 +31,7 @@ import { AddonModForumProvider, } from './forum'; import { AddonModForumDiscussionOptions, AddonModForumOffline, AddonModForumOfflineReply } from './forum-offline'; +import { CoreFileEntry } from '@services/file-helper'; /** * Service that provides some features for forums. diff --git a/src/addons/mod/forum/services/forum.ts b/src/addons/mod/forum/services/forum.ts index e860ef5de..ea4a088a3 100644 --- a/src/addons/mod/forum/services/forum.ts +++ b/src/addons/mod/forum/services/forum.ts @@ -16,17 +16,17 @@ import { Injectable } from '@angular/core'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; -import { CoreFileEntry } from '@features/fileuploader/services/fileuploader'; import { CoreRatingInfo } from '@features/rating/services/rating'; import { CoreTagItem } from '@features/tag/services/tag'; import { CoreUser } from '@features/user/services/user'; import { CoreApp } from '@services/app'; +import { CoreFileEntry } from '@services/file-helper'; import { CoreFilepool } from '@services/filepool'; import { CoreGroups } from '@services/groups'; import { CoreSitesCommonWSOptions, CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; -import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; +import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning, CoreWSStoredFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { AddonModForumOffline, AddonModForumOfflineDiscussion, AddonModForumReplyOptions } from './forum-offline'; @@ -1469,7 +1469,7 @@ export type AddonModForumPost = { canreplyprivately?: boolean; // Whether the user can post a private reply. }; attachment?: 0 | 1; - attachments?: (CoreFileEntry | AddonModForumWSPostAttachment)[]; + attachments?: CoreFileEntry[]; messageinlinefiles?: CoreWSExternalFile[]; haswordcount?: boolean; // Haswordcount. wordcount?: number; // Wordcount. @@ -1580,7 +1580,7 @@ export type AddonModForumReply = { id: number; subject: string; message: string; - files: (CoreFileEntry | AddonModForumWSPostAttachment)[]; + files: CoreFileEntry[]; }; /** @@ -1600,37 +1600,6 @@ export type AddonModForumSortOrder = { value: number; }; -/** - * Forum post attachement data returned by web services. - */ -export type AddonModForumWSPostAttachment = { - contextid: number; // Contextid. - component: string; // Component. - filearea: string; // Filearea. - itemid: number; // Itemid. - filepath: string; // Filepath. - filename: string; // Filename. - isdir: boolean; // Isdir. - isimage: boolean; // Isimage. - timemodified: number; // Timemodified. - timecreated: number; // Timecreated. - filesize: number; // Filesize. - author: string; // Author. - license: string; // License. - filenameshort: string; // Filenameshort. - filesizeformatted: string; // Filesizeformatted. - icon: string; // Icon. - timecreatedformatted: string; // Timecreatedformatted. - timemodifiedformatted: string; // Timemodifiedformatted. - url: string; // Url. - urls: { - export?: string; // The URL used to export the attachment. - }; - html: { - plagiarism?: string; // The HTML source for the Plagiarism Response. - }; -}; - /** * Forum post data returned by web services. */ @@ -1693,7 +1662,7 @@ export type AddonModForumWSPost = { markasunread?: string; // The URL used to mark the post as unread. discuss?: string; // Discuss. }; - attachments: AddonModForumWSPostAttachment[]; // Attachments. + attachments: CoreWSStoredFile[]; // Attachments. tags?: { // Tags. id: number; // The ID of the Tag. tagid: number; // The tagid. diff --git a/src/addons/mod/forum/services/handlers/prefetch.ts b/src/addons/mod/forum/services/handlers/prefetch.ts index c303ec8a0..a9c55d2e9 100644 --- a/src/addons/mod/forum/services/handlers/prefetch.ts +++ b/src/addons/mod/forum/services/handlers/prefetch.ts @@ -17,7 +17,7 @@ import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/ import { AddonModForum, AddonModForumData, AddonModForumPost, AddonModForumProvider } from '../forum'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreFilepool } from '@services/filepool'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { CoreCourse, CoreCourseAnyModuleData, CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreUser } from '@features/user/services/user'; import { CoreGroups, CoreGroupsProvider } from '@services/groups'; @@ -44,7 +44,7 @@ export class AddonModForumPrefetchHandlerService extends CoreCourseActivityPrefe * @param single True if we're downloading a single module, false if we're downloading a whole section. * @return Promise resolved with the list of files. */ - async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { + async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { try { const forum = await AddonModForum.getForum(courseId, module.id); @@ -69,13 +69,13 @@ export class AddonModForumPrefetchHandlerService extends CoreCourseActivityPrefe * @param posts Forum posts. * @return Files. */ - protected getPostsFiles(posts: AddonModForumPost[]): CoreWSExternalFile[] { - let files: CoreWSExternalFile[] = []; + protected getPostsFiles(posts: AddonModForumPost[]): CoreWSFile[] { + let files: CoreWSFile[] = []; const getInlineFiles = CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.2'); posts.forEach((post) => { if (post.attachments && post.attachments.length) { - files = files.concat(post.attachments as CoreWSExternalFile[]); + files = files.concat(post.attachments as CoreWSFile[]); } if (getInlineFiles && post.messageinlinefiles && post.messageinlinefiles.length) { files = files.concat(post.messageinlinefiles); diff --git a/src/addons/mod/glossary/services/glossary-helper.ts b/src/addons/mod/glossary/services/glossary-helper.ts index a970e3d77..1e25ddffd 100644 --- a/src/addons/mod/glossary/services/glossary-helper.ts +++ b/src/addons/mod/glossary/services/glossary-helper.ts @@ -20,7 +20,7 @@ import { CoreUtils } from '@services/utils/utils'; import { AddonModGlossaryOffline } from './glossary-offline'; import { AddonModGlossaryNewEntry, AddonModGlossaryNewEntryWithFiles } from './glossary'; import { makeSingleton } from '@singletons'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreFileEntry } from '@services/file-helper'; /** * Helper to gather some common functions for glossary. @@ -68,7 +68,7 @@ export class AddonModGlossaryHelperProvider { */ hasEntryDataChanged( entry: AddonModGlossaryNewEntry, - files: (CoreWSExternalFile | FileEntry)[], + files: CoreFileEntry[], original?: AddonModGlossaryNewEntryWithFiles, ): boolean { if (!original || typeof original.concept == 'undefined') { @@ -98,7 +98,7 @@ export class AddonModGlossaryHelperProvider { glossaryId: number, entryName: string, timeCreated: number, - files: (CoreWSExternalFile | FileEntry)[], + files: CoreFileEntry[], siteId?: string, ): Promise { // Get the folder where to store the files. diff --git a/src/addons/mod/glossary/services/glossary-sync.ts b/src/addons/mod/glossary/services/glossary-sync.ts index 3502a82a5..80a2fbe0d 100644 --- a/src/addons/mod/glossary/services/glossary-sync.ts +++ b/src/addons/mod/glossary/services/glossary-sync.ts @@ -14,7 +14,6 @@ import { ContextLevel } from '@/core/constants'; import { Injectable } from '@angular/core'; -import { FileEntry } from '@ionic-native/file/ngx'; import { CoreSyncBlockedError } from '@classes/base-sync'; import { CoreNetworkError } from '@classes/errors/network-error'; import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync'; @@ -24,13 +23,13 @@ import { CoreApp } from '@services/app'; import { CoreSites } from '@services/sites'; import { CoreSync } from '@services/sync'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { AddonModGlossary, AddonModGlossaryProvider } from './glossary'; import { AddonModGlossaryHelper } from './glossary-helper'; import { AddonModGlossaryOffline, AddonModGlossaryOfflineEntry } from './glossary-offline'; import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; +import { CoreFileEntry } from '@services/file-helper'; /** * Service to sync glossaries. @@ -309,7 +308,7 @@ export class AddonModGlossarySyncProvider extends CoreCourseActivitySyncBaseProv } // Has some attachments to sync. - let files: (CoreWSExternalFile | FileEntry)[] = entry.attachments.online || []; + let files: CoreFileEntry[] = entry.attachments.online || []; if (entry.attachments.offline) { // Has offline files. diff --git a/src/addons/mod/glossary/services/glossary.ts b/src/addons/mod/glossary/services/glossary.ts index b34767f4a..4efd33ccd 100644 --- a/src/addons/mod/glossary/services/glossary.ts +++ b/src/addons/mod/glossary/services/glossary.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { FileEntry } from '@ionic-native/file/ngx'; import { CoreError } from '@classes/errors/error'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; @@ -30,6 +29,7 @@ import { makeSingleton, Translate } from '@singletons'; import { AddonModGlossaryEntryDBRecord, ENTRIES_TABLE_NAME } from './database/glossary'; import { AddonModGlossaryOffline } from './glossary-offline'; import { AddonModGlossaryAutoSyncData, AddonModGlossarySyncProvider } from './glossary-sync'; +import { CoreFileEntry } from '@services/file-helper'; const ROOT_CACHE_KEY = 'mmaModGlossary:'; @@ -1431,7 +1431,7 @@ export type AddonModGlossaryNewEntry = { * Entry to be added, including attachments. */ export type AddonModGlossaryNewEntryWithFiles = AddonModGlossaryNewEntry & { - files: (CoreWSExternalFile | FileEntry)[]; + files: CoreFileEntry[]; }; /** diff --git a/src/addons/mod/glossary/services/handlers/prefetch.ts b/src/addons/mod/glossary/services/handlers/prefetch.ts index cc92048a0..a153527a1 100644 --- a/src/addons/mod/glossary/services/handlers/prefetch.ts +++ b/src/addons/mod/glossary/services/handlers/prefetch.ts @@ -19,7 +19,7 @@ import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/c import { CoreUser } from '@features/user/services/user'; import { CoreFilepool } from '@services/filepool'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonModGlossary, AddonModGlossaryEntry, AddonModGlossaryGlossary, AddonModGlossaryProvider } from '../glossary'; import { AddonModGlossarySync, AddonModGlossarySyncResult } from '../glossary-sync'; @@ -38,7 +38,7 @@ export class AddonModGlossaryPrefetchHandlerService extends CoreCourseActivityPr /** * @inheritdoc */ - async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { + async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { try { const glossary = await AddonModGlossary.getGlossary(courseId, module.id); @@ -68,7 +68,7 @@ export class AddonModGlossaryPrefetchHandlerService extends CoreCourseActivityPr module: CoreCourseAnyModuleData, glossary: AddonModGlossaryGlossary, entries: AddonModGlossaryEntry[], - ): CoreWSExternalFile[] { + ): CoreWSFile[] { let files = this.getIntroFilesFromInstance(module, glossary); const getInlineFiles = CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.2'); diff --git a/src/addons/mod/h5pactivity/components/index/index.ts b/src/addons/mod/h5pactivity/components/index/index.ts index 43c57aaa8..f8dfdccba 100644 --- a/src/addons/mod/h5pactivity/components/index/index.ts +++ b/src/addons/mod/h5pactivity/components/index/index.ts @@ -30,7 +30,7 @@ import { CoreFilepool } from '@services/filepool'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { AddonModH5PActivity, @@ -43,6 +43,7 @@ import { AddonModH5PActivitySyncProvider, AddonModH5PActivitySyncResult, } from '../../services/h5pactivity-sync'; +import { CoreFileHelper } from '@services/file-helper'; /** * Component that displays an H5P activity entry page. @@ -58,7 +59,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv h5pActivity?: AddonModH5PActivityData; // The H5P activity object. accessInfo?: AddonModH5PActivityAccessInfo; // Info about the user capabilities. - deployedFile?: CoreWSExternalFile; // The H5P deployed file. + deployedFile?: CoreWSFile; // The H5P deployed file. stateMessage?: string; // Message about the file state. downloading = false; // Whether the H5P file is being downloaded. @@ -191,10 +192,10 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv siteId: this.siteId, }); - this.fileUrl = this.deployedFile.fileurl; + this.fileUrl = CoreFileHelper.getFileUrl(this.deployedFile); // Listen for changes in the state. - const eventName = await CoreFilepool.getFileEventNameByUrl(this.site.getId(), this.deployedFile.fileurl); + const eventName = await CoreFilepool.getFileEventNameByUrl(this.site.getId(), this.fileUrl); if (!this.observer) { this.observer = CoreEvents.on(eventName, () => { @@ -213,7 +214,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv protected async calculateFileState(): Promise { this.state = await CoreFilepool.getFileStateByUrl( this.site.getId(), - this.deployedFile!.fileurl, + this.fileUrl!, this.deployedFile!.timemodified, ); @@ -317,7 +318,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv try { await CoreFilepool.downloadUrl( this.site.getId(), - this.deployedFile!.fileurl, + this.fileUrl!, false, this.component, this.componentId, diff --git a/src/addons/mod/h5pactivity/services/h5pactivity.ts b/src/addons/mod/h5pactivity/services/h5pactivity.ts index 3b9d8c7d3..fe19c45ac 100644 --- a/src/addons/mod/h5pactivity/services/h5pactivity.ts +++ b/src/addons/mod/h5pactivity/services/h5pactivity.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; -import { CoreWSExternalWarning, CoreWSExternalFile } from '@services/ws'; +import { CoreWSExternalWarning, CoreWSExternalFile, CoreWSFile } from '@services/ws'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils } from '@services/utils/utils'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; @@ -305,7 +305,7 @@ export class AddonModH5PActivityProvider { async getDeployedFile( h5pActivity: AddonModH5PActivityData, options?: AddonModH5PActivityGetDeployedFileOptions, - ): Promise { + ): Promise { if (h5pActivity.deployedfile) { // File already deployed and still valid, use this one. diff --git a/src/addons/mod/h5pactivity/services/handlers/prefetch.ts b/src/addons/mod/h5pactivity/services/handlers/prefetch.ts index 03a41dead..6c6d6cf8e 100644 --- a/src/addons/mod/h5pactivity/services/handlers/prefetch.ts +++ b/src/addons/mod/h5pactivity/services/handlers/prefetch.ts @@ -21,7 +21,7 @@ import { CoreH5P } from '@features/h5p/services/h5p'; import { CoreUser } from '@features/user/services/user'; import { CoreFilepool } from '@services/filepool'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonModH5PActivity, AddonModH5PActivityData, AddonModH5PActivityProvider } from '../h5pactivity'; @@ -39,7 +39,7 @@ export class AddonModH5PActivityPrefetchHandlerService extends CoreCourseActivit /** * @inheritdoc */ - async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { + async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { const h5pActivity = await AddonModH5PActivity.getH5PActivity(courseId, module.id); diff --git a/src/addons/mod/imscp/services/handlers/prefetch.ts b/src/addons/mod/imscp/services/handlers/prefetch.ts index 66b3f835f..fb95f9898 100644 --- a/src/addons/mod/imscp/services/handlers/prefetch.ts +++ b/src/addons/mod/imscp/services/handlers/prefetch.ts @@ -23,7 +23,7 @@ import { import { CoreFilepool } from '@services/filepool'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonModImscp, AddonModImscpProvider } from '../imscp'; @@ -58,7 +58,7 @@ export class AddonModImscpPrefetchHandlerService extends CoreCourseResourcePrefe /** * @inheritdoc */ - async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { + async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { // If not found, use undefined so module description is used. const imscp = await CoreUtils.ignoreErrors(AddonModImscp.getImscp(courseId, module.id)); diff --git a/src/addons/mod/label/services/handlers/prefetch.ts b/src/addons/mod/label/services/handlers/prefetch.ts index 91c583f43..ba7a0b950 100644 --- a/src/addons/mod/label/services/handlers/prefetch.ts +++ b/src/addons/mod/label/services/handlers/prefetch.ts @@ -17,7 +17,7 @@ import { CoreCourseResourcePrefetchHandlerBase } from '@features/course/classes/ import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course'; import { CoreSitesReadingStrategy } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonModLabel, AddonModLabelLabel, AddonModLabelProvider } from '../label'; @@ -36,7 +36,7 @@ export class AddonModLabelPrefetchHandlerService extends CoreCourseResourcePrefe /** * @inheritdoc */ - async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number, ignoreCache?: boolean): Promise { + async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number, ignoreCache?: boolean): Promise { let label: AddonModLabelLabel | undefined; if (AddonModLabel.isGetLabelAvailableForSite()) { diff --git a/src/addons/mod/lesson/services/handlers/prefetch.ts b/src/addons/mod/lesson/services/handlers/prefetch.ts index 7180e172c..def77488d 100644 --- a/src/addons/mod/lesson/services/handlers/prefetch.ts +++ b/src/addons/mod/lesson/services/handlers/prefetch.ts @@ -23,7 +23,7 @@ import { CoreGroups } from '@services/groups'; import { CoreFileSizeSum, CorePluginFileDelegate } from '@services/plugin-file-delegate'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton, ModalController, Translate } from '@singletons'; import { AddonModLessonPasswordModalComponent } from '../../components/password-modal/password-modal'; import { @@ -92,7 +92,7 @@ export class AddonModLessonPrefetchHandlerService extends CoreCourseActivityPref lesson = passwordData.lesson || lesson; // Get intro files and media files. - let files = lesson.mediafiles || []; + let files: CoreWSFile[] = lesson.mediafiles || []; files = files.concat(this.getIntroFilesFromInstance(module, lesson)); const result = await CorePluginFileDelegate.getFilesDownloadSize(files); @@ -289,7 +289,8 @@ export class AddonModLessonPrefetchHandlerService extends CoreCourseActivityPref const promises: Promise[] = []; // Download intro files and media files. - const files = (lesson.mediafiles || []).concat(this.getIntroFilesFromInstance(module, lesson)); + let files: CoreWSFile[] = (lesson.mediafiles || []); + files = files.concat(this.getIntroFilesFromInstance(module, lesson)); promises.push(CoreFilepool.addFilesToQueue(siteId, files, this.component, module.id)); if (AddonModLesson.isLessonOffline(lesson)) { @@ -495,7 +496,7 @@ export class AddonModLessonPrefetchHandlerService extends CoreCourseActivityPref } // Download embedded files in essays. - const files: CoreWSExternalFile[] = []; + const files: CoreWSFile[] = []; attempt.answerpages.forEach((answerPage) => { if (!answerPage.page || answerPage.page.qtype != AddonModLessonProvider.LESSON_PAGE_ESSAY) { return; diff --git a/src/addons/mod/mod.module.ts b/src/addons/mod/mod.module.ts index 8976cdff9..6561d2eda 100644 --- a/src/addons/mod/mod.module.ts +++ b/src/addons/mod/mod.module.ts @@ -34,6 +34,7 @@ import { AddonModChoiceModule } from './choice/choice.module'; import { AddonModWikiModule } from './wiki/wiki.module'; import { AddonModGlossaryModule } from './glossary/glossary.module'; import { AddonModChatModule } from './chat/chat.module'; +import { AddonModFeedbackModule } from './feedback/feedback.module'; @NgModule({ imports: [ @@ -57,6 +58,7 @@ import { AddonModChatModule } from './chat/chat.module'; AddonModWikiModule, AddonModGlossaryModule, AddonModChatModule, + AddonModFeedbackModule, ], }) export class AddonModModule { } diff --git a/src/addons/mod/quiz/services/handlers/prefetch.ts b/src/addons/mod/quiz/services/handlers/prefetch.ts index 76a718017..0528dee8f 100644 --- a/src/addons/mod/quiz/services/handlers/prefetch.ts +++ b/src/addons/mod/quiz/services/handlers/prefetch.ts @@ -23,7 +23,7 @@ import { CoreFilepool } from '@services/filepool'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonModQuizAccessRuleDelegate } from '../access-rules-delegate'; import { @@ -77,7 +77,7 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet * @return Promise resolved with the list of files. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - async getFiles(module: CoreCourseAnyModuleData, courseId: number, single?: boolean): Promise { + async getFiles(module: CoreCourseAnyModuleData, courseId: number, single?: boolean): Promise { try { const quiz = await AddonModQuiz.getQuiz(courseId, module.id); @@ -109,9 +109,9 @@ export class AddonModQuizPrefetchHandlerService extends CoreCourseActivityPrefet quiz: AddonModQuizQuizWSData, attempts: AddonModQuizAttemptWSData[], siteId?: string, - ): Promise { + ): Promise { const getInlineFiles = CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.2'); - let files: CoreWSExternalFile[] = []; + let files: CoreWSFile[] = []; await Promise.all(attempts.map(async (attempt) => { if (!AddonModQuiz.isAttemptFinished(attempt.state)) { diff --git a/src/addons/mod/resource/services/handlers/module.ts b/src/addons/mod/resource/services/handlers/module.ts index 7e05beb73..ea4e3ee50 100644 --- a/src/addons/mod/resource/services/handlers/module.ts +++ b/src/addons/mod/resource/services/handlers/module.ts @@ -22,7 +22,7 @@ import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { AddonModResourceIndexComponent } from '../../components/index'; import { AddonModResource, AddonModResourceCustomData } from '../resource'; @@ -142,7 +142,7 @@ export class AddonModResourceModuleHandlerService implements CoreCourseModuleHan handlerData: CoreCourseModuleHandlerData, ): Promise { const promises: Promise[] = []; - let infoFiles: CoreWSExternalFile[] = []; + let infoFiles: CoreWSFile[] = []; let options: AddonModResourceCustomData = {}; // Check if the button needs to be shown or not. @@ -166,7 +166,7 @@ export class AddonModResourceModuleHandlerService implements CoreCourseModuleHan await Promise.all(promises); - const files: (CoreCourseModuleContentFile | CoreWSExternalFile)[] = module.contents && module.contents.length + const files: (CoreCourseModuleContentFile | CoreWSFile)[] = module.contents && module.contents.length ? module.contents : infoFiles; diff --git a/src/addons/mod/scorm/services/handlers/prefetch.ts b/src/addons/mod/scorm/services/handlers/prefetch.ts index 6c174cf11..634082609 100644 --- a/src/addons/mod/scorm/services/handlers/prefetch.ts +++ b/src/addons/mod/scorm/services/handlers/prefetch.ts @@ -22,7 +22,7 @@ import { CoreFilepool } from '@services/filepool'; import { CoreFileSizeSum } from '@services/plugin-file-delegate'; import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { AddonModScorm, AddonModScormProvider, AddonModScormScorm } from '../scorm'; import { AddonModScormSync } from '../scorm-sync'; @@ -297,7 +297,7 @@ export class AddonModScormPrefetchHandlerService extends CoreCourseActivityPrefe * @param single True if we're downloading a single module, false if we're downloading a whole section. * @return Promise resolved with the list of files. */ - async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { + async getFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { try { const scorm = await this.getScorm(module, courseId); diff --git a/src/addons/mod/scorm/services/scorm.ts b/src/addons/mod/scorm/services/scorm.ts index 5920b727f..4383c6d87 100644 --- a/src/addons/mod/scorm/services/scorm.ts +++ b/src/addons/mod/scorm/services/scorm.ts @@ -25,7 +25,7 @@ import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWS, CoreWSExternalFile, CoreWSExternalWarning, CoreWSPreSets } from '@services/ws'; +import { CoreWS, CoreWSExternalFile, CoreWSExternalWarning, CoreWSFile, CoreWSPreSets } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { AddonModScormOffline } from './scorm-offline'; @@ -983,8 +983,8 @@ export class AddonModScormProvider { * @param scorm SCORM. * @return File list. */ - getScormFileList(scorm: AddonModScormScorm): CoreWSExternalFile[] { - const files: CoreWSExternalFile[] = []; + getScormFileList(scorm: AddonModScormScorm): CoreWSFile[] { + const files: CoreWSFile[] = []; if (!this.isScormUnsupported(scorm) && !scorm.warningMessage) { files.push({ diff --git a/src/addons/mod/survey/services/handlers/prefetch.ts b/src/addons/mod/survey/services/handlers/prefetch.ts index c856f5656..0ccae56eb 100644 --- a/src/addons/mod/survey/services/handlers/prefetch.ts +++ b/src/addons/mod/survey/services/handlers/prefetch.ts @@ -18,7 +18,7 @@ import { CoreCourseAnyModuleData } from '@features/course/services/course'; import { CoreFilepool } from '@services/filepool'; import { CoreSitesReadingStrategy } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonModSurvey, AddonModSurveyProvider } from '../survey'; import { AddonModSurveySync, AddonModSurveySyncResult } from '../survey-sync'; @@ -37,7 +37,7 @@ export class AddonModSurveyPrefetchHandlerService extends CoreCourseActivityPref /** * @inheritdoc */ - async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { + async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number): Promise { const survey = await CoreUtils.ignoreErrors(AddonModSurvey.getSurvey(courseId, module.id)); return this.getIntroFilesFromInstance(module, survey); diff --git a/src/addons/mod/wiki/pages/edit/edit.ts b/src/addons/mod/wiki/pages/edit/edit.ts index 312c77050..ab1497894 100644 --- a/src/addons/mod/wiki/pages/edit/edit.ts +++ b/src/addons/mod/wiki/pages/edit/edit.ts @@ -23,7 +23,7 @@ import { CoreSync } from '@services/sync'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { CoreForms } from '@singletons/form'; @@ -62,7 +62,7 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy, CanLeave { protected blockId?: string; // ID to block the subwiki. protected editing = false; // Whether the user is editing a page (true) or creating a new one (false). protected editOffline = false; // Whether the user is editing an offline page. - protected subwikiFiles: CoreWSExternalFile[] = []; // List of files of the subwiki. + protected subwikiFiles: CoreWSFile[] = []; // List of files of the subwiki. protected originalContent?: string; // The original page content. protected version?: number; // Page version. protected renewLockInterval?: number; // An interval to renew the lock every certain time. diff --git a/src/addons/mod/wiki/services/handlers/prefetch.ts b/src/addons/mod/wiki/services/handlers/prefetch.ts index fee992462..22b6e4922 100644 --- a/src/addons/mod/wiki/services/handlers/prefetch.ts +++ b/src/addons/mod/wiki/services/handlers/prefetch.ts @@ -21,7 +21,7 @@ import { CoreGroups } from '@services/groups'; import { CoreFileSizeSum, CorePluginFileDelegate } from '@services/plugin-file-delegate'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonModWiki, AddonModWikiProvider, AddonModWikiSubwikiPage } from '../wiki'; import { AddonModWikiSync, AddonModWikiSyncWikiResult } from '../wiki-sync'; @@ -103,7 +103,7 @@ export class AddonModWikiPrefetchHandlerService extends CoreCourseActivityPrefet courseId: number, single?: boolean, siteId?: string, - ): Promise { + ): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); try { diff --git a/src/addons/mod/wiki/services/wiki.ts b/src/addons/mod/wiki/services/wiki.ts index da636b222..6dc180136 100644 --- a/src/addons/mod/wiki/services/wiki.ts +++ b/src/addons/mod/wiki/services/wiki.ts @@ -22,7 +22,7 @@ import { CoreApp } from '@services/app'; import { CoreNavigator } from '@services/navigator'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; +import { CoreWSExternalFile, CoreWSExternalWarning, CoreWSFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { CoreEvents } from '@singletons/events'; import { AddonModWikiPageDBRecord } from './database/wiki'; @@ -196,7 +196,7 @@ export class AddonModWikiProvider { * @param options Other options. * @return Promise resolved with subwiki files. */ - async getSubwikiFiles(wikiId: number, options: AddonModWikiGetSubwikiFilesOptions = {}): Promise { + async getSubwikiFiles(wikiId: number, options: AddonModWikiGetSubwikiFilesOptions = {}): Promise { const site = await CoreSites.getSite(options.siteId); const groupId = options.groupId || -1; @@ -426,10 +426,10 @@ export class AddonModWikiProvider { * @param options Other options. * @return Promise resolved with the list of files. */ - async getWikiFileList(wiki: AddonModWikiWiki, options: CoreSitesCommonWSOptions = {}): Promise { + async getWikiFileList(wiki: AddonModWikiWiki, options: CoreSitesCommonWSOptions = {}): Promise { options.siteId = options.siteId || CoreSites.getCurrentSiteId(); - let files: CoreWSExternalFile[] = []; + let files: CoreWSFile[] = []; const modOptions = { cmId: wiki.coursemodule, ...options, // Include all options. diff --git a/src/addons/qtype/ddmarker/services/handlers/ddmarker.ts b/src/addons/qtype/ddmarker/services/handlers/ddmarker.ts index 1bcdbed82..eb74174dc 100644 --- a/src/addons/qtype/ddmarker/services/handlers/ddmarker.ts +++ b/src/addons/qtype/ddmarker/services/handlers/ddmarker.ts @@ -17,7 +17,7 @@ import { Injectable, Type } from '@angular/core'; import { CoreQuestion, CoreQuestionQuestionParsed, CoreQuestionsAnswers } from '@features/question/services/question'; import { CoreQuestionHandler } from '@features/question/services/question-delegate'; import { CoreQuestionHelper, CoreQuestionQuestion } from '@features/question/services/question-helper'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonQtypeDdMarkerComponent } from '../../component/ddmarker'; @@ -135,7 +135,7 @@ export class AddonQtypeDdMarkerHandlerService implements CoreQuestionHandler { * @param usageId Usage ID. * @return List of files or URLs. */ - getAdditionalDownloadableFiles(question: CoreQuestionQuestionParsed, usageId?: number): CoreWSExternalFile[] { + getAdditionalDownloadableFiles(question: CoreQuestionQuestionParsed, usageId?: number): CoreWSFile[] { const treatedQuestion: CoreQuestionQuestion = question; CoreQuestionHelper.extractQuestionScripts(treatedQuestion, usageId); diff --git a/src/addons/qtype/essay/component/essay.ts b/src/addons/qtype/essay/component/essay.ts index 44da3e59b..16e1c75d2 100644 --- a/src/addons/qtype/essay/component/essay.ts +++ b/src/addons/qtype/essay/component/essay.ts @@ -20,9 +20,9 @@ import { CoreFileUploaderStoreFilesResult } from '@features/fileuploader/service import { AddonModQuizEssayQuestion, CoreQuestionBaseComponent } from '@features/question/classes/base-question-component'; import { CoreQuestionHelper } from '@features/question/services/question-helper'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreWSExternalFile } from '@services/ws'; import { CoreFileSession } from '@services/file-session'; import { CoreQuestion } from '@features/question/services/question'; +import { CoreFileEntry } from '@services/file-helper'; /** * Component to render an essay question. */ @@ -33,7 +33,7 @@ import { CoreQuestion } from '@features/question/services/question'; export class AddonQtypeEssayComponent extends CoreQuestionBaseComponent implements OnInit { formControl?: FormControl; - attachments?: (CoreWSExternalFile | FileEntry)[]; + attachments?: CoreFileEntry[]; uploadFilesSupported = false; essayQuestion?: AddonModQuizEssayQuestion; diff --git a/src/addons/qtype/essay/services/handlers/essay.ts b/src/addons/qtype/essay/services/handlers/essay.ts index 053e7d1eb..e651af56b 100644 --- a/src/addons/qtype/essay/services/handlers/essay.ts +++ b/src/addons/qtype/essay/services/handlers/essay.ts @@ -25,7 +25,7 @@ import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { AddonQtypeEssayComponent } from '../../component/essay'; @@ -81,12 +81,12 @@ export class AddonQtypeEssayHandlerService implements CoreQuestionHandler { * @param usageId Usage ID. * @return List of files or URLs. */ - getAdditionalDownloadableFiles(question: CoreQuestionQuestionParsed): CoreWSExternalFile[] { + getAdditionalDownloadableFiles(question: CoreQuestionQuestionParsed): CoreWSFile[] { if (!question.responsefileareas) { return []; } - return question.responsefileareas.reduce((urlsList, area) => urlsList.concat(area.files || []), []); + return question.responsefileareas.reduce((urlsList, area) => urlsList.concat(area.files || []), []); } /** diff --git a/src/app/app.module.ts b/src/app/app.module.ts index fed9d6f73..238f24992 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { COMPILER_OPTIONS, NgModule } from '@angular/core'; +import { APP_INITIALIZER, COMPILER_OPTIONS, NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouteReuseStrategy } from '@angular/router'; @@ -29,6 +29,8 @@ import { AddonsModule } from '@/addons/addons.module'; import { AppComponent } from './app.component'; import { AppRoutingModule } from './app-routing.module'; import { JitCompilerFactory } from '@angular/platform-browser-dynamic'; +import { CoreCronDelegate } from '@services/cron'; +import { CoreSiteInfoCronHandler } from '@services/handlers/site-info-cron'; // For translate loader. AoT requires an exported function for factories. export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { @@ -57,6 +59,14 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, { provide: COMPILER_OPTIONS, useValue: {}, multi: true }, { provide: JitCompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS] }, + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => () => { + CoreCronDelegate.register(CoreSiteInfoCronHandler.instance); + }, + }, ], bootstrap: [AppComponent], }) diff --git a/src/core/components/attachments/attachments.ts b/src/core/components/attachments/attachments.ts index 341da812e..b541e1947 100644 --- a/src/core/components/attachments/attachments.ts +++ b/src/core/components/attachments/attachments.ts @@ -18,11 +18,11 @@ import { FileEntry } from '@ionic-native/file/ngx'; import { CoreFileUploader, CoreFileUploaderTypeList } from '@features/fileuploader/services/fileuploader'; import { CoreSites } from '@services/sites'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreWSExternalFile } from '@services/ws'; import { Translate } from '@singletons'; import { CoreApp } from '@services/app'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuploader-helper'; +import { CoreFileEntry } from '@services/file-helper'; /** * Component to render attachments, allow adding more and delete the current ones. @@ -43,7 +43,7 @@ import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuplo }) export class CoreAttachmentsComponent implements OnInit { - @Input() files?: (CoreWSExternalFile | FileEntry)[]; // List of attachments. New attachments will be added to this array. + @Input() files?: CoreFileEntry[]; // List of attachments. New attachments will be added to this array. @Input() maxSize?: number; // Max size for attachments. -1 means unlimited, 0 means user max size, not defined means unknown. @Input() maxSubmissions?: number; // Max number of attachments. -1 means unlimited, not defined means unknown limit. @Input() component?: string; // Component the downloaded files will be linked to. diff --git a/src/core/components/file/file.ts b/src/core/components/file/file.ts index b821892c1..f595bf994 100644 --- a/src/core/components/file/file.ts +++ b/src/core/components/file/file.ts @@ -25,7 +25,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreTextUtils } from '@services/utils/text'; import { CoreConstants } from '@/core/constants'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; /** * Component to handle a remote file. Shows the file name, icon (depending on mimetype) and a button @@ -38,7 +38,7 @@ import { CoreWSExternalFile } from '@services/ws'; }) export class CoreFileComponent implements OnInit, OnDestroy { - @Input() file?: CoreWSExternalFile; // The file. + @Input() file?: CoreWSFile; // The file. @Input() component?: string; // Component the file belongs to. @Input() componentId?: string | number; // Component ID. @Input() canDelete?: boolean | string; // Whether file can be deleted. @@ -76,7 +76,7 @@ export class CoreFileComponent implements OnInit, OnDestroy { this.alwaysDownload = CoreUtils.isTrueOrOne(this.alwaysDownload); this.canDownload = CoreUtils.isTrueOrOne(this.canDownload); - this.fileUrl = this.file.fileurl; + this.fileUrl = CoreFileHelper.getFileUrl(this.file); this.timemodified = this.file.timemodified || 0; this.siteId = CoreSites.getCurrentSiteId(); this.fileSize = this.file.filesize; @@ -88,12 +88,12 @@ export class CoreFileComponent implements OnInit, OnDestroy { this.showTime = CoreUtils.isTrueOrOne(this.showTime) && this.timemodified > 0; - if (this.file.isexternalfile) { + if ('isexternalfile' in this.file && this.file.isexternalfile) { this.alwaysDownload = true; // Always show the download button in external files. } - this.fileIcon = this.file.mimetype ? CoreMimetypeUtils.getMimetypeIcon(this.file.mimetype) : - CoreMimetypeUtils.getFileIcon(this.fileName); + this.fileIcon = 'mimetype' in this.file && this.file.mimetype ? + CoreMimetypeUtils.getMimetypeIcon(this.file.mimetype) : CoreMimetypeUtils.getFileIcon(this.fileName); if (this.canDownload) { this.calculateState(); diff --git a/src/core/components/files/files.ts b/src/core/components/files/files.ts index 694b764f6..b17358e65 100644 --- a/src/core/components/files/files.ts +++ b/src/core/components/files/files.ts @@ -13,11 +13,10 @@ // limitations under the License. import { Component, Input, OnInit, DoCheck, KeyValueDiffers } from '@angular/core'; -import { FileEntry } from '@ionic-native/file/ngx'; +import { CoreFileEntry } from '@services/file-helper'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; /** * Component to render a file list. @@ -31,7 +30,7 @@ import { CoreWSExternalFile } from '@services/ws'; }) export class CoreFilesComponent implements OnInit, DoCheck { - @Input() files?: (CoreWSExternalFile | FileEntry)[]; // List of files. + @Input() files?: CoreFileEntry[]; // List of files. @Input() component?: string; // Component the downloaded files will be linked to. @Input() componentId?: string | number; // Component ID. @Input() alwaysDownload?: boolean | string; // Whether it should always display the refresh button when the file is downloaded. diff --git a/src/core/components/mark-required/mark-required.scss b/src/core/components/mark-required/mark-required.scss index def9d3ecb..dd1292e5e 100644 --- a/src/core/components/mark-required/mark-required.scss +++ b/src/core/components/mark-required/mark-required.scss @@ -1,8 +1,10 @@ +@import "~theme/globals"; + :host { .core-input-required-asterisk { font-size: 8px; - --padding-start: 4px; line-height: 100%; vertical-align: top; + @include padding-horizontal(4px, null); } } diff --git a/src/core/components/tabs/core-tabs.html b/src/core/components/tabs/core-tabs.html index 947eff9db..033c12d41 100644 --- a/src/core/components/tabs/core-tabs.html +++ b/src/core/components/tabs/core-tabs.html @@ -8,7 +8,7 @@ - diff --git a/src/core/components/tabs/tab.ts b/src/core/components/tabs/tab.ts index 452e07c68..24726538a 100644 --- a/src/core/components/tabs/tab.ts +++ b/src/core/components/tabs/tab.ts @@ -49,19 +49,21 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase { @Input() icon?: string; // The tab icon. @Input() badge?: string; // A badge to add in the tab. @Input() badgeStyle?: string; // The badge color. - @Input() enabled = true; // Whether the tab is enabled. @Input() class?: string; // Class, if needed. - @Input() set show(val: boolean) { // Whether the tab should be shown. Use a setter to detect changes on the value. - if (typeof val != 'undefined') { - const hasChanged = this.isShown != val; - this.isShown = val; + @Input() set enabled(value: boolean) { // Whether the tab should be shown. + value = value === undefined ? true : value; + const hasChanged = this.isEnabled != value; + this.isEnabled = value; - if (this.initialized && hasChanged) { - this.tabs.tabVisibilityChanged(); - } + if (this.initialized && hasChanged) { + this.tabs.tabVisibilityChanged(); } } + get enabled(): boolean { + return this.isEnabled; + } + @Input() id?: string; // An ID to identify the tab. @Output() ionSelect: EventEmitter = new EventEmitter(); @@ -70,9 +72,10 @@ export class CoreTabComponent implements OnInit, OnDestroy, CoreTabBase { element: HTMLElement; // The core-tab element. loaded = false; initialized = false; - isShown = true; tabElement?: HTMLElement | null; + protected isEnabled = true; + constructor( protected tabs: CoreTabsComponent, element: ElementRef, diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index 86bc7dda4..108ae1e6c 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -127,7 +127,7 @@ import { ADDON_MOD_BOOK_SERVICES } from '@addons/mod/book/book.module'; import { ADDON_MOD_CHAT_SERVICES } from '@addons/mod/chat/chat.module'; import { ADDON_MOD_CHOICE_SERVICES } from '@addons/mod/choice/choice.module'; import { ADDON_MOD_DATA_SERVICES } from '@addons/mod/data/data.module'; -// @todo import { ADDON_MOD_FEEDBACK_SERVICES } from '@addons/mod/feedback/feedback.module'; +import { ADDON_MOD_FEEDBACK_SERVICES } from '@addons/mod/feedback/feedback.module'; import { ADDON_MOD_FOLDER_SERVICES } from '@addons/mod/folder/folder.module'; import { ADDON_MOD_FORUM_SERVICES } from '@addons/mod/forum/forum.module'; import { ADDON_MOD_GLOSSARY_SERVICES } from '@addons/mod/glossary/glossary.module'; @@ -293,7 +293,7 @@ export class CoreCompileProvider { ...ADDON_MOD_CHAT_SERVICES, ...ADDON_MOD_CHOICE_SERVICES, ...ADDON_MOD_DATA_SERVICES, - // @todo ...ADDON_MOD_FEEDBACK_SERVICES, + ...ADDON_MOD_FEEDBACK_SERVICES, ...ADDON_MOD_FOLDER_SERVICES, ...ADDON_MOD_FORUM_SERVICES, ...ADDON_MOD_GLOSSARY_SERVICES, diff --git a/src/core/features/course/classes/module-prefetch-handler.ts b/src/core/features/course/classes/module-prefetch-handler.ts index 1df8285e0..e68b13c68 100644 --- a/src/core/features/course/classes/module-prefetch-handler.ts +++ b/src/core/features/course/classes/module-prefetch-handler.ts @@ -15,7 +15,7 @@ import { CoreFilepool } from '@services/filepool'; import { CoreFileSizeSum, CorePluginFileDelegate } from '@services/plugin-file-delegate'; import { CoreSites } from '@services/sites'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { CoreCourse, CoreCourseAnyModuleData, CoreCourseModuleContentFile } from '../services/course'; import { CoreCourseModulePrefetchHandler } from '../services/module-prefetch-delegate'; @@ -154,7 +154,7 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref * @return Promise resolved with the list of files. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - async getFiles(module: CoreCourseAnyModuleData, courseId: number, single?: boolean): Promise { + async getFiles(module: CoreCourseAnyModuleData, courseId: number, single?: boolean): Promise { // To be overridden. return []; } @@ -168,7 +168,7 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref * @return Promise resolved with list of intro files. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number, ignoreCache?: boolean): Promise { + async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number, ignoreCache?: boolean): Promise { return this.getIntroFilesFromInstance(module); } @@ -179,7 +179,7 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref * @param instance The instance to get the intro files (book, assign, ...). If not defined, module will be used. * @return List of intro files. */ - getIntroFilesFromInstance(module: CoreCourseAnyModuleData, instance?: ModuleInstance): CoreWSExternalFile[] { + getIntroFilesFromInstance(module: CoreCourseAnyModuleData, instance?: ModuleInstance): CoreWSFile[] { if (instance) { if (typeof instance.introfiles != 'undefined') { return instance.introfiles; @@ -339,6 +339,6 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref * Properties a module instance should have to be able to retrieve its intro files. */ type ModuleInstance = { - introfiles?: CoreWSExternalFile[]; + introfiles?: CoreWSFile[]; intro?: string; }; diff --git a/src/core/features/course/classes/resource-prefetch-handler.ts b/src/core/features/course/classes/resource-prefetch-handler.ts index b12025a2d..67880f2ae 100644 --- a/src/core/features/course/classes/resource-prefetch-handler.ts +++ b/src/core/features/course/classes/resource-prefetch-handler.ts @@ -18,7 +18,7 @@ import { CoreFilterHelper } from '@features/filter/services/filter-helper'; import { CoreApp } from '@services/app'; import { CoreFilepool } from '@services/filepool'; import { CoreSites } from '@services/sites'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { CoreCourse, CoreCourseAnyModuleData, CoreCourseWSModule } from '../services/course'; import { CoreCourseModulePrefetchHandlerBase } from './module-prefetch-handler'; @@ -144,7 +144,7 @@ export class CoreCourseResourcePrefetchHandlerBase extends CoreCourseModulePrefe * @return Promise resolved with the list of files. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - async getFiles(module: CoreCourseWSModule, courseId: number, single?: boolean): Promise { + async getFiles(module: CoreCourseWSModule, courseId: number, single?: boolean): Promise { // Load module contents if needed. await this.loadContents(module, courseId); diff --git a/src/core/features/course/services/module-prefetch-delegate.ts b/src/core/features/course/services/module-prefetch-delegate.ts index f06387f67..aca8154ae 100644 --- a/src/core/features/course/services/module-prefetch-delegate.ts +++ b/src/core/features/course/services/module-prefetch-delegate.ts @@ -30,7 +30,7 @@ import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { makeSingleton } from '@singletons'; import { CoreEvents, CoreEventSectionStatusChangedData } from '@singletons/events'; import { CoreError } from '@classes/errors/error'; -import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; +import { CoreWSFile, CoreWSExternalWarning } from '@services/ws'; import { CHECK_UPDATES_TIMES_TABLE, CoreCourseCheckUpdatesDBRecord } from './database/module-prefetch'; import { CoreFileSizeSum } from '@services/plugin-file-delegate'; @@ -458,7 +458,7 @@ export class CoreCourseModulePrefetchDelegateService extends CoreDelegate { - const path = await CoreFilepool.getFilePathByUrl(siteId, file.fileurl || ''); + const path = await CoreFilepool.getFilePathByUrl(siteId, CoreFileHelper.getFileUrl(file)); try { const fileSize = await CoreFile.getFileSize(path); @@ -467,7 +467,7 @@ export class CoreCourseModulePrefetchDelegateService extends CoreDelegate { + ): Promise<(CoreWSFile | CoreCourseModuleContentFile)[]> { const handler = this.getPrefetchHandlerFor(module); if (handler?.getFiles) { @@ -1192,7 +1192,7 @@ export class CoreCourseModulePrefetchDelegateService extends CoreDelegate { - await CoreUtils.ignoreErrors(CoreFilepool.removeFileByUrl(siteId, file.fileurl || '')); + await CoreUtils.ignoreErrors(CoreFilepool.removeFileByUrl(siteId, CoreFileHelper.getFileUrl(file))); })); } @@ -1462,7 +1462,7 @@ export interface CoreCourseModulePrefetchHandler extends CoreDelegateHandler { * @param courseId Course ID the module belongs to. * @return List of files, or promise resolved with the files. */ - getFiles?(module: CoreCourseAnyModuleData, courseId: number): Promise<(CoreWSExternalFile | CoreCourseModuleContentFile)[]>; + getFiles?(module: CoreCourseAnyModuleData, courseId: number): Promise<(CoreWSFile | CoreCourseModuleContentFile)[]>; /** * Check if a certain module has updates based on the result of check updates. diff --git a/src/core/features/fileuploader/services/fileuploader-delegate.ts b/src/core/features/fileuploader/services/fileuploader-delegate.ts index 6bcb5146e..6614590a9 100644 --- a/src/core/features/fileuploader/services/fileuploader-delegate.ts +++ b/src/core/features/fileuploader/services/fileuploader-delegate.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { FileEntry } from '@ionic-native/file'; +import { FileEntry } from '@ionic-native/file/ngx'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { CoreEvents } from '@singletons/events'; diff --git a/src/core/features/fileuploader/services/fileuploader.ts b/src/core/features/fileuploader/services/fileuploader.ts index 342815f33..c9546f807 100644 --- a/src/core/features/fileuploader/services/fileuploader.ts +++ b/src/core/features/fileuploader/services/fileuploader.ts @@ -26,12 +26,13 @@ import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile, CoreWSFileUploadOptions, CoreWSUploadFileResult } from '@services/ws'; +import { CoreWSFile, CoreWSFileUploadOptions, CoreWSUploadFileResult } from '@services/ws'; import { makeSingleton, Translate, MediaCapture, ModalController, Camera } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreEmulatorCaptureMediaComponent } from '@features/emulator/components/capture-media/capture-media'; import { CoreError } from '@classes/errors/error'; import { CoreSite } from '@classes/site'; +import { CoreFileEntry, CoreFileHelper } from '@services/file-helper'; /** * File upload options. @@ -80,7 +81,7 @@ export class CoreFileUploaderProvider { * @param b Second file list. * @return Whether both lists are different. */ - areFileListDifferent(a: (CoreWSExternalFile | FileEntry)[], b: (CoreWSExternalFile | FileEntry)[]): boolean { + areFileListDifferent(a: CoreFileEntry[], b: CoreFileEntry[]): boolean { a = a || []; b = b || []; if (a.length != b.length) { @@ -194,7 +195,7 @@ export class CoreFileUploaderProvider { * * @param files List of files. */ - clearTmpFiles(files: (CoreWSExternalFile | FileEntry)[]): void { + clearTmpFiles(files: (CoreWSFile | FileEntry)[]): void { // Delete the local files. files.forEach((file) => { if ('remove' in file) { @@ -275,15 +276,16 @@ export class CoreFileUploaderProvider { * @return List of files to delete. */ getFilesToDelete( - originalFiles: CoreWSExternalFile[], - currentFiles: (CoreWSExternalFile | FileEntry)[], + originalFiles: CoreWSFile[], + currentFiles: CoreFileEntry[], ): { filepath: string; filename: string }[] { const filesToDelete: { filepath: string; filename: string }[] = []; currentFiles = currentFiles || []; originalFiles.forEach((file) => { - const stillInList = currentFiles.some((currentFile) => ( currentFile).fileurl == file.fileurl); + const stillInList = currentFiles.some((currentFile) => + CoreFileHelper.getFileUrl( currentFile) == CoreFileHelper.getFileUrl(file)); if (!stillInList) { filesToDelete.push({ @@ -391,8 +393,8 @@ export class CoreFileUploaderProvider { async getStoredFilesFromOfflineFilesObject( filesObject: CoreFileUploaderStoreFilesResult, folderPath: string, - ): Promise<(CoreWSExternalFile | FileEntry)[]> { - let files: (CoreWSExternalFile | FileEntry)[] = []; + ): Promise { + let files: CoreFileEntry[] = []; if (filesObject.online.length > 0) { files = CoreUtils.clone(filesObject.online); @@ -550,7 +552,7 @@ export class CoreFileUploaderProvider { */ async storeFilesToUpload( folderPath: string, - files: (CoreWSExternalFile | FileEntry)[], + files: CoreFileEntry[], ): Promise { const result: CoreFileUploaderStoreFilesResult = { online: [], @@ -569,7 +571,7 @@ export class CoreFileUploaderProvider { // It's an online file, add it to the result and ignore it. result.online.push({ filename: file.filename, - fileurl: file.fileurl, + fileurl: CoreFileHelper.getFileUrl(file), }); } else if (file.fullPath?.indexOf(folderPath) != -1) { // File already in the submission folder. @@ -629,7 +631,7 @@ export class CoreFileUploaderProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the itemId. */ - async uploadFiles(itemId: number, files: (CoreWSExternalFile | FileEntry)[], siteId?: string): Promise { + async uploadFiles(itemId: number, files: CoreFileEntry[], siteId?: string): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); if (!files || !files.length) { @@ -637,7 +639,7 @@ export class CoreFileUploaderProvider { } // Index the online files by name. - const usedNames: {[name: string]: (CoreWSExternalFile | FileEntry)} = {}; + const usedNames: {[name: string]: CoreFileEntry} = {}; const filesToUpload: FileEntry[] = []; files.forEach((file) => { if (CoreUtils.isFileEntry(file)) { @@ -674,7 +676,7 @@ export class CoreFileUploaderProvider { * @return Promise resolved with the itemId. */ async uploadOrReuploadFile( - file: CoreWSExternalFile | FileEntry, + file: CoreFileEntry, itemId?: number, component?: string, componentId?: string | number, @@ -697,7 +699,7 @@ export class CoreFileUploaderProvider { const path = await CoreFilepool.downloadUrl( siteId, - file.fileurl, + CoreFileHelper.getFileUrl(file), false, component, componentId, @@ -734,7 +736,7 @@ export class CoreFileUploaderProvider { * @return Promise resolved with the itemId. */ async uploadOrReuploadFiles( - files: (CoreWSExternalFile | FileEntry)[], + files: CoreFileEntry[], component?: string, componentId?: string | number, siteId?: string, @@ -766,7 +768,7 @@ export class CoreFileUploaderProvider { export const CoreFileUploader = makeSingleton(CoreFileUploaderProvider); export type CoreFileUploaderStoreFilesResult = { - online: CoreWSExternalFile[]; // List of online files. + online: CoreWSFile[]; // List of online files. offline: number; // Number of offline files. }; @@ -779,5 +781,3 @@ export type CoreFileUploaderTypeListInfoEntry = { name?: string; extlist: string; }; - -export type CoreFileEntry = CoreWSExternalFile | FileEntry; diff --git a/src/core/features/h5p/classes/helper.ts b/src/core/features/h5p/classes/helper.ts index 4054bcb01..9724e29a3 100644 --- a/src/core/features/h5p/classes/helper.ts +++ b/src/core/features/h5p/classes/helper.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { FileEntry } from '@ionic-native/file'; +import { FileEntry } from '@ionic-native/file/ngx'; import { CoreFile, CoreFileProvider } from '@services/file'; import { CoreSites } from '@services/sites'; diff --git a/src/core/features/h5p/services/h5p.ts b/src/core/features/h5p/services/h5p.ts index 9a6bc3b0d..7bf8cf078 100644 --- a/src/core/features/h5p/services/h5p.ts +++ b/src/core/features/h5p/services/h5p.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreSites } from '@services/sites'; -import { CoreWSExternalWarning, CoreWSExternalFile } from '@services/ws'; +import { CoreWSExternalWarning, CoreWSExternalFile, CoreWSFile } from '@services/ws'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreQueueRunner } from '@classes/queue-runner'; @@ -95,7 +95,7 @@ export class CoreH5PProvider { options?: CoreH5PGetTrustedFileOptions, ignoreCache?: boolean, siteId?: string, - ): Promise { + ): Promise { options = options || {}; diff --git a/src/core/features/h5p/services/handlers/pluginfile.ts b/src/core/features/h5p/services/handlers/pluginfile.ts index 879710aec..48723ae70 100644 --- a/src/core/features/h5p/services/handlers/pluginfile.ts +++ b/src/core/features/h5p/services/handlers/pluginfile.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { FileEntry } from '@ionic-native/file'; +import { FileEntry } from '@ionic-native/file/ngx'; import { CoreFilepoolOnProgressCallback } from '@services/filepool'; import { CorePluginFileDownloadableResult, CorePluginFileHandler } from '@services/plugin-file-delegate'; @@ -21,10 +21,11 @@ import { CoreSites } from '@services/sites'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { CoreH5P } from '../h5p'; import { Translate, makeSingleton } from '@singletons'; import { CoreH5PHelper } from '../../classes/helper'; +import { CoreFileHelper } from '@services/file-helper'; /** * Handler to treat H5P files. @@ -54,15 +55,17 @@ export class CoreH5PPluginFileHandlerService implements CorePluginFileHandler { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the file to use. Rejected if cannot download. */ - async getDownloadableFile(file: CoreWSExternalFile, siteId?: string): Promise { + async getDownloadableFile(file: CoreWSFile, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); - if (site.containsUrl(file.fileurl) && file.fileurl.match(/pluginfile\.php\/[^/]+\/core_h5p\/export\//i)) { + const fileUrl = CoreFileHelper.getFileUrl(file); + + if (site.containsUrl(fileUrl) && fileUrl.match(/pluginfile\.php\/[^/]+\/core_h5p\/export\//i)) { // It's already a deployed file, use it. return file; } - return CoreH5P.getTrustedH5PFile(file.fileurl, {}, false, siteId); + return CoreH5P.getTrustedH5PFile(fileUrl, {}, false, siteId); } /** @@ -94,7 +97,7 @@ export class CoreH5PPluginFileHandlerService implements CorePluginFileHandler { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the size. */ - async getFileSize(file: CoreWSExternalFile, siteId?: string): Promise { + async getFileSize(file: CoreWSFile, siteId?: string): Promise { try { const trustedFile = await this.getDownloadableFile(file, siteId); @@ -125,7 +128,7 @@ export class CoreH5PPluginFileHandlerService implements CorePluginFileHandler { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with a boolean and a reason why it isn't downloadable if needed. */ - async isFileDownloadable(file: CoreWSExternalFile, siteId?: string): Promise { + async isFileDownloadable(file: CoreWSFile, siteId?: string): Promise { const offlineDisabled = await CoreH5P.isOfflineDisabled(siteId); if (offlineDisabled) { @@ -146,8 +149,8 @@ export class CoreH5PPluginFileHandlerService implements CorePluginFileHandler { * @param file The file data. * @return Whether the file should be treated by this handler. */ - shouldHandleFile(file: CoreWSExternalFile): boolean { - return CoreMimetypeUtils.guessExtensionFromUrl(file.fileurl) == 'h5p'; + shouldHandleFile(file: CoreWSFile): boolean { + return CoreMimetypeUtils.guessExtensionFromUrl(CoreFileHelper.getFileUrl(file)) == 'h5p'; } /** diff --git a/src/core/features/question/classes/base-question-component.ts b/src/core/features/question/classes/base-question-component.ts index f22757bb5..7be8ed33f 100644 --- a/src/core/features/question/classes/base-question-component.ts +++ b/src/core/features/question/classes/base-question-component.ts @@ -18,7 +18,7 @@ import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUrlUtils } from '@services/utils/url'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { CoreLogger } from '@singletons/logger'; import { CoreQuestionBehaviourButton, CoreQuestionHelper, CoreQuestionQuestion } from '../services/question-helper'; @@ -715,7 +715,7 @@ export type AddonModQuizEssayQuestion = AddonModQuizQuestionBasicData & { isPlainText?: boolean; // Whether the answer is plain text. hasInlineText?: boolean; // // Whether the answer has inline text answer?: string; // Question answer text. - attachments?: CoreWSExternalFile[]; // Question answer attachments. + attachments?: CoreWSFile[]; // Question answer attachments. hasDraftFiles?: boolean; // Whether the question has draft files. textarea?: AddonModQuizQuestionTextarea; // Textarea data. formatInput?: { name: string; value: string }; // Format input data. diff --git a/src/core/features/question/services/question-delegate.ts b/src/core/features/question/services/question-delegate.ts index 4810ca620..ff3d1bc01 100644 --- a/src/core/features/question/services/question-delegate.ts +++ b/src/core/features/question/services/question-delegate.ts @@ -15,7 +15,7 @@ import { Injectable, Type } from '@angular/core'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { makeSingleton } from '@singletons'; import { CoreQuestionDefaultHandler } from './handlers/default-question'; import { CoreQuestionQuestionParsed, CoreQuestionsAnswers } from './question'; @@ -143,7 +143,7 @@ export interface CoreQuestionHandler extends CoreDelegateHandler { * @param usageId Usage ID. * @return List of files or URLs. */ - getAdditionalDownloadableFiles?(question: CoreQuestionQuestionParsed, usageId?: number): CoreWSExternalFile[]; + getAdditionalDownloadableFiles?(question: CoreQuestionQuestionParsed, usageId?: number): CoreWSFile[]; /** * Clear temporary data after the data has been saved. @@ -393,7 +393,7 @@ export class CoreQuestionDelegateService extends CoreDelegate { let content = anchor.innerHTML; @@ -464,7 +465,7 @@ export class CoreQuestionHelperProvider { * @param areaName Name of the area, e.g. 'attachments'. * @return List of files. */ - getResponseFileAreaFiles(question: CoreQuestionQuestion, areaName: string): CoreWSExternalFile[] { + getResponseFileAreaFiles(question: CoreQuestionQuestion, areaName: string): CoreWSFile[] { if (!question.responsefileareas) { return []; } @@ -642,22 +643,23 @@ export class CoreQuestionHelperProvider { await Promise.all(files.map(async (file) => { const timemodified = file.timemodified || 0; + const fileUrl = CoreFileHelper.getFileUrl(file); - if (treated[file.fileurl]) { + if (treated[fileUrl]) { return; } - treated[file.fileurl] = true; + treated[fileUrl] = true; - if (!site.canDownloadFiles() && CoreUrlUtils.isPluginFileUrl(file.fileurl)) { + if (!site.canDownloadFiles() && CoreUrlUtils.isPluginFileUrl(fileUrl)) { return; } - if (file.fileurl.indexOf('theme/image.php') > -1 && file.fileurl.indexOf('flagged') > -1) { + if (fileUrl.indexOf('theme/image.php') > -1 && fileUrl.indexOf('flagged') > -1) { // Ignore flag images. return; } - await CoreFilepool.addToQueueByUrl(site.getId(), file.fileurl, component, componentId, timemodified); + await CoreFilepool.addToQueueByUrl(site.getId(), fileUrl, component, componentId, timemodified); })); } diff --git a/src/core/features/sharedfiles/components/list-modal/list-modal.ts b/src/core/features/sharedfiles/components/list-modal/list-modal.ts index c06f27796..f3ba2f873 100644 --- a/src/core/features/sharedfiles/components/list-modal/list-modal.ts +++ b/src/core/features/sharedfiles/components/list-modal/list-modal.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, OnInit, Input } from '@angular/core'; -import { FileEntry } from '@ionic-native/file'; +import { FileEntry } from '@ionic-native/file/ngx'; import { CoreFile } from '@services/file'; import { ModalController, Translate } from '@singletons'; diff --git a/src/core/features/sharedfiles/components/list/list.ts b/src/core/features/sharedfiles/components/list/list.ts index d915ba50f..936c2b71c 100644 --- a/src/core/features/sharedfiles/components/list/list.ts +++ b/src/core/features/sharedfiles/components/list/list.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core'; -import { FileEntry, DirectoryEntry } from '@ionic-native/file'; +import { FileEntry, DirectoryEntry } from '@ionic-native/file/ngx'; import { IonRefresher } from '@ionic/angular'; import { Md5 } from 'ts-md5'; diff --git a/src/core/features/sharedfiles/pages/choose-site/choose-site.ts b/src/core/features/sharedfiles/pages/choose-site/choose-site.ts index a802e1efa..defc4a7bc 100644 --- a/src/core/features/sharedfiles/pages/choose-site/choose-site.ts +++ b/src/core/features/sharedfiles/pages/choose-site/choose-site.ts @@ -14,7 +14,7 @@ import { Component, OnInit } from '@angular/core'; import { CoreSharedFilesHelper } from '@features/sharedfiles/services/sharedfiles-helper'; -import { FileEntry } from '@ionic-native/file'; +import { FileEntry } from '@ionic-native/file/ngx'; import { CoreFile } from '@services/file'; import { CoreNavigator } from '@services/navigator'; import { CoreSiteBasicInfo, CoreSites } from '@services/sites'; diff --git a/src/core/features/sharedfiles/services/sharedfiles-helper.ts b/src/core/features/sharedfiles/services/sharedfiles-helper.ts index 704c0fb9a..6b6f86230 100644 --- a/src/core/features/sharedfiles/services/sharedfiles-helper.ts +++ b/src/core/features/sharedfiles/services/sharedfiles-helper.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { FileEntry } from '@ionic-native/file'; +import { FileEntry } from '@ionic-native/file/ngx'; import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreFileUploader } from '@features/fileuploader/services/fileuploader'; diff --git a/src/core/features/sharedfiles/services/sharedfiles.ts b/src/core/features/sharedfiles/services/sharedfiles.ts index a774aea78..8384b6de2 100644 --- a/src/core/features/sharedfiles/services/sharedfiles.ts +++ b/src/core/features/sharedfiles/services/sharedfiles.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { FileEntry, DirectoryEntry } from '@ionic-native/file'; +import { FileEntry, DirectoryEntry } from '@ionic-native/file/ngx'; import { Md5 } from 'ts-md5/dist/md5'; import { SQLiteDB } from '@classes/sqlitedb'; diff --git a/src/core/features/viewer/components/text/text.ts b/src/core/features/viewer/components/text/text.ts index 2a88fdff1..cd72a31f6 100644 --- a/src/core/features/viewer/components/text/text.ts +++ b/src/core/features/viewer/components/text/text.ts @@ -13,10 +13,9 @@ // limitations under the License. import { Component, Input } from '@angular/core'; -import { FileEntry } from '@ionic-native/file/ngx'; +import { CoreFileEntry } from '@services/file-helper'; import { CoreUtils } from '@services/utils/utils'; -import { CoreWSExternalFile } from '@services/ws'; import { ModalController } from '@singletons'; /** @@ -32,7 +31,7 @@ export class CoreViewerTextComponent { @Input() content?: string; // Modal content. @Input() component?: string; // Component to use in format-text. @Input() componentId?: string | number; // Component ID to use in format-text. - @Input() files?: (CoreWSExternalFile | FileEntry)[]; // List of files. + @Input() files?: CoreFileEntry[]; // List of files. @Input() filter?: boolean; // Whether to filter the text. @Input() contextLevel?: string; // The context level. @Input() instanceId?: number; // The instance ID related to the context. diff --git a/src/core/services/file-helper.ts b/src/core/services/file-helper.ts index 426568b15..aa1745388 100644 --- a/src/core/services/file-helper.ts +++ b/src/core/services/file-helper.ts @@ -13,13 +13,13 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { FileEntry } from '@ionic-native/file'; +import { FileEntry } from '@ionic-native/file/ngx'; import { CoreApp } from '@services/app'; import { CoreFile } from '@services/file'; import { CoreFilepool } from '@services/filepool'; import { CoreSites } from '@services/sites'; -import { CoreWS, CoreWSExternalFile } from '@services/ws'; +import { CoreWS, CoreWSFile } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; @@ -46,7 +46,7 @@ export class CoreFileHelperProvider { * @return Resolved on success. */ async downloadAndOpenFile( - file: CoreWSExternalFile, + file: CoreWSFile, component?: string, componentId?: string | number, state?: string, @@ -55,7 +55,7 @@ export class CoreFileHelperProvider { ): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); - const fileUrl = file.fileurl; + const fileUrl = CoreFileHelper.getFileUrl(file); const timemodified = this.getFileTimemodified(file); if (!this.isOpenableInApp(file)) { @@ -119,7 +119,7 @@ export class CoreFileHelperProvider { * @return Resolved with the URL to use on success. */ protected async downloadFileIfNeeded( - file: CoreWSExternalFile, + file: CoreWSFile, fileUrl: string, component?: string, componentId?: string | number, @@ -215,7 +215,7 @@ export class CoreFileHelperProvider { componentId?: string | number, timemodified?: number, onProgress?: (event: ProgressEvent) => void, - file?: CoreWSExternalFile, + file?: CoreWSFile, siteId?: string, ): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); @@ -255,10 +255,10 @@ export class CoreFileHelperProvider { * Get the file's URL. * * @param file The file. - * @deprecated since 3.9.5. Get directly the fileurl instead. + * @return File URL. */ - getFileUrl(file: CoreWSExternalFile): string | undefined { - return file.fileurl; + getFileUrl(file: CoreWSFile): string { + return 'fileurl' in file ? file.fileurl : file.url; } /** @@ -266,7 +266,7 @@ export class CoreFileHelperProvider { * * @param file The file. */ - getFileTimemodified(file: CoreWSExternalFile): number { + getFileTimemodified(file: CoreWSFile): number { return file.timemodified || 0; } @@ -286,8 +286,8 @@ export class CoreFileHelperProvider { * @param file The file to check. * @return Whether the file should be opened in browser. */ - shouldOpenInBrowser(file: CoreWSExternalFile): boolean { - if (!file || !file.isexternalfile || !file.mimetype) { + shouldOpenInBrowser(file: CoreWSFile): boolean { + if (!file || !('isexternalfile' in file) || !file.isexternalfile || !file.mimetype) { return false; } @@ -312,7 +312,7 @@ export class CoreFileHelperProvider { * @param files The files to check. * @return Total files size. */ - async getTotalFilesSize(files: (CoreWSExternalFile | FileEntry)[]): Promise { + async getTotalFilesSize(files: CoreFileEntry[]): Promise { let totalSize = 0; for (const file of files) { @@ -328,14 +328,14 @@ export class CoreFileHelperProvider { * @param file The file to check. * @return File size. */ - async getFileSize(file: CoreWSExternalFile | FileEntry): Promise { + async getFileSize(file: CoreFileEntry): Promise { if ('filesize' in file && (file.filesize || file.filesize === 0)) { return file.filesize; } // If it's a remote file. First check if we have the file downloaded since it's more reliable. if ('filename' in file) { - const fileUrl = file.fileurl; + const fileUrl = CoreFileHelper.getFileUrl(file); try { const siteId = CoreSites.getCurrentSiteId(); @@ -422,3 +422,5 @@ export class CoreFileHelperProvider { export const CoreFileHelper = makeSingleton(CoreFileHelperProvider); export type CoreFileHelperOnProgress = (event?: ProgressEvent | { calculating: true }) => void; + +export type CoreFileEntry = CoreWSFile | FileEntry; diff --git a/src/core/services/file-session.ts b/src/core/services/file-session.ts index 6f9caeace..5bdea6efb 100644 --- a/src/core/services/file-session.ts +++ b/src/core/services/file-session.ts @@ -13,10 +13,9 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { FileEntry } from '@ionic-native/file'; +import { CoreFileEntry } from '@services/file-helper'; import { CoreSites } from '@services/sites'; -import { CoreWSExternalFile } from '@services/ws'; import { makeSingleton } from '@singletons'; /** @@ -29,7 +28,7 @@ import { makeSingleton } from '@singletons'; @Injectable({ providedIn: 'root' }) export class CoreFileSessionProvider { - protected files: {[siteId: string]: {[component: string]: {[id: string]: (CoreWSExternalFile | FileEntry)[]}}} = {}; + protected files: {[siteId: string]: {[component: string]: {[id: string]: CoreFileEntry[]}}} = {}; /** * Add a file to the session. @@ -39,7 +38,7 @@ export class CoreFileSessionProvider { * @param file File to add. * @param siteId Site ID. If not defined, current site. */ - addFile(component: string, id: string | number, file: CoreWSExternalFile | FileEntry, siteId?: string): void { + addFile(component: string, id: string | number, file: CoreFileEntry, siteId?: string): void { siteId = siteId || CoreSites.getCurrentSiteId(); this.initFileArea(component, id, siteId); @@ -69,7 +68,7 @@ export class CoreFileSessionProvider { * @param siteId Site ID. If not defined, current site. * @return Array of files in session. */ - getFiles(component: string, id: string | number, siteId?: string): (CoreWSExternalFile | FileEntry)[] { + getFiles(component: string, id: string | number, siteId?: string): CoreFileEntry[] { siteId = siteId || CoreSites.getCurrentSiteId(); if (this.files[siteId] && this.files[siteId][component] && this.files[siteId][component][id]) { return this.files[siteId][component][id]; @@ -107,7 +106,7 @@ export class CoreFileSessionProvider { * @param file File to remove. The instance should be exactly the same as the one stored in session. * @param siteId Site ID. If not defined, current site. */ - removeFile(component: string, id: string | number, file: CoreWSExternalFile | FileEntry, siteId?: string): void { + removeFile(component: string, id: string | number, file: CoreFileEntry, siteId?: string): void { siteId = siteId || CoreSites.getCurrentSiteId(); if (this.files[siteId] && this.files[siteId][component] && this.files[siteId][component][id]) { const position = this.files[siteId][component][id].indexOf(file); @@ -141,7 +140,7 @@ export class CoreFileSessionProvider { * @param newFiles Files to set. * @param siteId Site ID. If not defined, current site. */ - setFiles(component: string, id: string | number, newFiles: (CoreWSExternalFile | FileEntry)[], siteId?: string): void { + setFiles(component: string, id: string | number, newFiles: CoreFileEntry[], siteId?: string): void { siteId = siteId || CoreSites.getCurrentSiteId(); this.initFileArea(component, id, siteId); diff --git a/src/core/services/file.ts b/src/core/services/file.ts index dc09c9bbc..9923d7a46 100644 --- a/src/core/services/file.ts +++ b/src/core/services/file.ts @@ -14,10 +14,9 @@ import { Injectable } from '@angular/core'; -import { FileEntry, DirectoryEntry, Entry, Metadata, IFile } from '@ionic-native/file'; +import { FileEntry, DirectoryEntry, Entry, Metadata, IFile } from '@ionic-native/file/ngx'; import { CoreApp } from '@services/app'; -import { CoreWSExternalFile } from '@services/ws'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; @@ -26,6 +25,7 @@ import { CoreError } from '@classes/errors/error'; import { CoreLogger } from '@singletons/logger'; import { makeSingleton, File, Zip, Platform, WebView } from '@singletons'; +import { CoreFileEntry } from '@services/file-helper'; /** * Progress event used when writing a file data into a file. @@ -1177,7 +1177,7 @@ export class CoreFileProvider { * @param files List of used files. * @return Promise resolved when done, rejected if failure. */ - async removeUnusedFiles(dirPath: string, files: (CoreWSExternalFile | FileEntry)[]): Promise { + async removeUnusedFiles(dirPath: string, files: CoreFileEntry[]): Promise { // Get the directory contents. try { const contents = await this.getDirectoryContents(dirPath); @@ -1297,7 +1297,7 @@ export class CoreFileProvider { * @param file The file. * @return The file name. */ - getFileName(file: CoreWSExternalFile | FileEntry): string | undefined { + getFileName(file: CoreFileEntry): string | undefined { return CoreUtils.isFileEntry(file) ? file.name : file.filename; } diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 80739f24b..b203ae787 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -20,7 +20,7 @@ import { CoreEventPackageStatusChanged, CoreEvents } from '@singletons/events'; import { CoreFile } from '@services/file'; import { CorePluginFileDelegate } from '@services/plugin-file-delegate'; import { CoreSites } from '@services/sites'; -import { CoreWS, CoreWSExternalFile } from '@services/ws'; +import { CoreWS, CoreWSExternalFile, CoreWSFile } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; @@ -46,6 +46,7 @@ import { CoreFilepoolQueueEntry, CoreFilepoolQueueDBEntry, } from '@services/database/filepool'; +import { CoreFileHelper } from './file-helper'; /* * Factory for handling downloading files and retrieve downloaded files. @@ -171,7 +172,7 @@ export class CoreFilepoolProvider { */ async addFileLinkByUrl(siteId: string, fileUrl: string, component: string, componentId?: string | number): Promise { const file = await this.fixPluginfileURL(siteId, fileUrl); - const fileId = this.getFileIdByUrl(file.fileurl); + const fileId = this.getFileIdByUrl(CoreFileHelper.getFileUrl(file)); await this.addFileLink(siteId, fileId, component, componentId); } @@ -199,7 +200,7 @@ export class CoreFilepoolProvider { * @param componentId An ID to use in conjunction with the component (optional). * @return Resolved on success. */ - addFilesToQueue(siteId: string, files: CoreWSExternalFile[], component?: string, componentId?: string | number): Promise { + addFilesToQueue(siteId: string, files: CoreWSFile[], component?: string, componentId?: string | number): Promise { return this.downloadOrPrefetchFiles(siteId, files, true, false, component, componentId); } @@ -342,7 +343,7 @@ export class CoreFilepoolProvider { // Fix the URL and use the fixed data. const file = await this.fixPluginfileURL(siteId, fileUrl); - fileUrl = file.fileurl; + fileUrl = CoreFileHelper.getFileUrl(file); timemodified = file.timemodified || timemodified; } @@ -735,7 +736,7 @@ export class CoreFilepoolProvider { */ downloadOrPrefetchFiles( siteId: string, - files: CoreWSExternalFile[], + files: CoreWSFile[], prefetch: boolean, ignoreStale?: boolean, component?: string, @@ -746,11 +747,11 @@ export class CoreFilepoolProvider { // Download files. files.forEach((file) => { - const url = file.fileurl; + const url = CoreFileHelper.getFileUrl(file); const timemodified = file.timemodified; const options = { - isexternalfile: file.isexternalfile, - repositorytype: file.repositorytype, + isexternalfile: 'isexternalfile' in file ? file.isexternalfile : undefined, + repositorytype: 'repositorytype' in file ? file.repositorytype : undefined, }; let path: string | undefined; @@ -799,7 +800,7 @@ export class CoreFilepoolProvider { */ downloadOrPrefetchPackage( siteId: string, - fileList: CoreWSExternalFile[], + fileList: CoreWSFile[], prefetch: boolean, component: string, componentId?: string | number, @@ -822,10 +823,10 @@ export class CoreFilepoolProvider { let packageLoaded = 0; fileList.forEach((file) => { - const fileUrl = file.fileurl; + const fileUrl = CoreFileHelper.getFileUrl(file); const options = { - isexternalfile: file.isexternalfile, - repositorytype: file.repositorytype, + isexternalfile: 'isexternalfile' in file ? file.isexternalfile : undefined, + repositorytype: 'repositorytype' in file ? file.repositorytype : undefined, }; let path: string | undefined; let promise: Promise; @@ -924,7 +925,7 @@ export class CoreFilepoolProvider { */ downloadPackage( siteId: string, - fileList: CoreWSExternalFile[], + fileList: CoreWSFile[], component: string, componentId?: string | number, extra?: string, @@ -973,7 +974,7 @@ export class CoreFilepoolProvider { } const file = await this.fixPluginfileURL(siteId, fileUrl); - fileUrl = file.fileurl; + fileUrl = CoreFileHelper.getFileUrl(file); timemodified = file.timemodified || timemodified; options = Object.assign({}, options); // Create a copy to prevent modifying the original object. @@ -1158,11 +1159,15 @@ export class CoreFilepoolProvider { * @param timemodified The timemodified of the file. * @return Promise resolved with the file data to use. */ - protected async fixPluginfileURL(siteId: string, fileUrl: string, timemodified: number = 0): Promise { + protected async fixPluginfileURL(siteId: string, fileUrl: string, timemodified: number = 0): Promise { const file = await CorePluginFileDelegate.getDownloadableFile({ fileurl: fileUrl, timemodified }); const site = await CoreSites.getSite(siteId); - file.fileurl = await site.checkAndFixPluginfileURL(file.fileurl); + if ('fileurl' in file) { + file.fileurl = await site.checkAndFixPluginfileURL(file.fileurl); + } else { + file.url = await site.checkAndFixPluginfileURL(file.url); + } return file; } @@ -1206,7 +1211,7 @@ export class CoreFilepoolProvider { } const file = await this.fixPluginfileURL(siteId, fileUrl); - const fileId = this.getFileIdByUrl(file.fileurl); + const fileId = this.getFileIdByUrl(CoreFileHelper.getFileUrl(file)); const filePath = await this.getFilePath(siteId, fileId, ''); const dirEntry = await CoreFile.getDir(filePath); @@ -1244,7 +1249,7 @@ export class CoreFilepoolProvider { */ getFileEventNameByUrl(siteId: string, fileUrl: string): Promise { return this.fixPluginfileURL(siteId, fileUrl).then((file) => { - const fileId = this.getFileIdByUrl(file.fileurl); + const fileId = this.getFileIdByUrl(CoreFileHelper.getFileUrl(file)); return this.getFileEventName(siteId, fileId); }); @@ -1342,7 +1347,7 @@ export class CoreFilepoolProvider { */ async getFilePathByUrl(siteId: string, fileUrl: string): Promise { const file = await this.fixPluginfileURL(siteId, fileUrl); - const fileId = this.getFileIdByUrl(file.fileurl); + const fileId = this.getFileIdByUrl(CoreFileHelper.getFileUrl(file)); return this.getFilePath(siteId, fileId); } @@ -1433,7 +1438,7 @@ export class CoreFilepoolProvider { filePath?: string, revision?: number, ): Promise { - let file: CoreWSExternalFile; + let file: CoreWSFile; try { file = await this.fixPluginfileURL(siteId, fileUrl, timemodified); @@ -1441,7 +1446,7 @@ export class CoreFilepoolProvider { return CoreConstants.NOT_DOWNLOADABLE; } - fileUrl = file.fileurl; + fileUrl = CoreFileHelper.getFileUrl(file); timemodified = file.timemodified || timemodified; revision = revision || this.getRevisionFromUrl(fileUrl); const fileId = this.getFileIdByUrl(fileUrl); @@ -1527,7 +1532,7 @@ export class CoreFilepoolProvider { const file = await this.fixPluginfileURL(siteId, fileUrl, timemodified); - fileUrl = file.fileurl; + fileUrl = CoreFileHelper.getFileUrl(file); timemodified = file.timemodified || timemodified; revision = revision || this.getRevisionFromUrl(fileUrl); const fileId = this.getFileIdByUrl(fileUrl); @@ -1635,7 +1640,7 @@ export class CoreFilepoolProvider { } const file = await this.fixPluginfileURL(siteId, fileUrl); - const fileId = this.getFileIdByUrl(file.fileurl); + const fileId = this.getFileIdByUrl(CoreFileHelper.getFileUrl(file)); return this.getInternalUrlById(siteId, fileId); } @@ -1693,7 +1698,7 @@ export class CoreFilepoolProvider { */ getPackageDirPathByUrl(siteId: string, url: string): Promise { return this.fixPluginfileURL(siteId, url).then((file) => { - const dirName = this.getPackageDirNameByUrl(file.fileurl); + const dirName = this.getPackageDirNameByUrl(CoreFileHelper.getFileUrl(file)); return this.getFilePath(siteId, dirName, ''); }); @@ -1712,7 +1717,7 @@ export class CoreFilepoolProvider { } const file = await this.fixPluginfileURL(siteId, url); - const dirName = this.getPackageDirNameByUrl(file.fileurl); + const dirName = this.getPackageDirNameByUrl(CoreFileHelper.getFileUrl(file)); const dirPath = await this.getFilePath(siteId, dirName, ''); const dirEntry = await CoreFile.getDir(dirPath); @@ -1890,12 +1895,14 @@ export class CoreFilepoolProvider { * @param files Package files. * @return Highest revision. */ - getRevisionFromFileList(files: CoreWSExternalFile[]): number { + getRevisionFromFileList(files: CoreWSFile[]): number { let revision = 0; files.forEach((file) => { - if (file.fileurl) { - const r = this.getRevisionFromUrl(file.fileurl); + const fileUrl = CoreFileHelper.getFileUrl(file); + + if (fileUrl) { + const r = this.getRevisionFromUrl(fileUrl); if (r > revision) { revision = r; } @@ -1981,7 +1988,7 @@ export class CoreFilepoolProvider { * @param files List of files. * @return Time modified. */ - getTimemodifiedFromFileList(files: CoreWSExternalFile[]): number { + getTimemodifiedFromFileList(files: CoreWSFile[]): number { let timemodified = 0; files.forEach((file) => { @@ -2163,7 +2170,7 @@ export class CoreFilepoolProvider { */ async invalidateFileByUrl(siteId: string, fileUrl: string): Promise { const file = await this.fixPluginfileURL(siteId, fileUrl); - const fileId = this.getFileIdByUrl(file.fileurl); + const fileId = this.getFileIdByUrl(CoreFileHelper.getFileUrl(file)); const db = await CoreSites.getSiteDb(siteId); @@ -2250,7 +2257,7 @@ export class CoreFilepoolProvider { */ async isFileDownloadingByUrl(siteId: string, fileUrl: string): Promise { const file = await this.fixPluginfileURL(siteId, fileUrl); - const fileId = this.getFileIdByUrl(file.fileurl); + const fileId = this.getFileIdByUrl(CoreFileHelper.getFileUrl(file)); await this.hasFileInQueue(siteId, fileId); } @@ -2403,7 +2410,7 @@ export class CoreFilepoolProvider { */ prefetchPackage( siteId: string, - fileList: CoreWSExternalFile[], + fileList: CoreWSFile[], component: string, componentId?: string | number, extra?: string, @@ -2691,7 +2698,7 @@ export class CoreFilepoolProvider { */ async removeFileByUrl(siteId: string, fileUrl: string): Promise { const file = await this.fixPluginfileURL(siteId, fileUrl); - const fileId = this.getFileIdByUrl(file.fileurl); + const fileId = this.getFileIdByUrl(CoreFileHelper.getFileUrl(file)); await this.removeFileById(siteId, fileId); } diff --git a/src/core/services/handlers/site-info-cron.ts b/src/core/services/handlers/site-info-cron.ts index 6a066f30c..9e55a85a6 100644 --- a/src/core/services/handlers/site-info-cron.ts +++ b/src/core/services/handlers/site-info-cron.ts @@ -15,12 +15,13 @@ import { Injectable } from '@angular/core'; import { CoreCronHandler } from '@services/cron'; import { CoreSites } from '@services/sites'; +import { makeSingleton } from '@singletons'; /** * Cron handler to update site info every certain time. */ -@Injectable() -export class CoreSiteInfoCronHandler implements CoreCronHandler { +@Injectable({ providedIn: 'root' }) +export class CoreSiteInfoCronHandlerService implements CoreCronHandler { name = 'CoreSiteInfoCronHandler'; @@ -60,3 +61,5 @@ export class CoreSiteInfoCronHandler implements CoreCronHandler { } } + +export const CoreSiteInfoCronHandler = makeSingleton(CoreSiteInfoCronHandlerService); diff --git a/src/core/services/plugin-file-delegate.ts b/src/core/services/plugin-file-delegate.ts index 425c67e6e..944278ee6 100644 --- a/src/core/services/plugin-file-delegate.ts +++ b/src/core/services/plugin-file-delegate.ts @@ -13,14 +13,15 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { FileEntry } from '@ionic-native/file'; +import { FileEntry } from '@ionic-native/file/ngx'; import { CoreFilepool, CoreFilepoolOnProgressCallback } from '@services/filepool'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { CoreConstants } from '@/core/constants'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { makeSingleton } from '@singletons'; import { CoreSites } from './sites'; +import { CoreFileHelper } from './file-helper'; /** * Delegate to register pluginfile information handlers. @@ -57,7 +58,7 @@ export class CorePluginFileDelegateService extends CoreDelegate { + getDownloadableFile(file: CoreWSFile, siteId?: string): Promise { const handler = this.getHandlerForFile(file); return this.getHandlerDownloadableFile(file, handler, siteId); @@ -72,10 +73,10 @@ export class CorePluginFileDelegateService extends CoreDelegate { + ): Promise { const isDownloadable = await this.isFileDownloadable(file, siteId); if (!isDownloadable.downloadable) { @@ -134,13 +135,13 @@ export class CorePluginFileDelegateService extends CoreDelegate { + async getFilesDownloadSize(files: CoreWSFile[], siteId?: string): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); - const filteredFiles = []; + const filteredFiles = []; await Promise.all(files.map(async (file) => { - const state = await CoreFilepool.getFileStateByUrl(siteId!, file.fileurl, file.timemodified); + const state = await CoreFilepool.getFileStateByUrl(siteId!, CoreFileHelper.getFileUrl(file), file.timemodified); if (state != CoreConstants.DOWNLOADED && state != CoreConstants.NOT_DOWNLOADABLE) { filteredFiles.push(file); @@ -157,7 +158,7 @@ export class CorePluginFileDelegateService extends CoreDelegate { + async getFilesSize(files: CoreWSFile[], siteId?: string): Promise { const result = { size: 0, total: true, @@ -184,7 +185,7 @@ export class CorePluginFileDelegateService extends CoreDelegate { + async getFileSize(file: CoreWSFile, siteId?: string): Promise { const isDownloadable = await this.isFileDownloadable(file, siteId); if (!isDownloadable.downloadable) { @@ -218,7 +219,7 @@ export class CorePluginFileDelegateService extends CoreDelegate { + async isFileDownloadable(file: CoreWSFile, siteId?: string): Promise { const handler = this.getHandlerForFile(file); if (handler && handler.isFileDownloadable) { @@ -337,7 +338,7 @@ export interface CorePluginFileHandler extends CoreDelegateHandler { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the file to use. Rejected if cannot download. */ - getDownloadableFile?(file: CoreWSExternalFile, siteId?: string): Promise; + getDownloadableFile?(file: CoreWSFile, siteId?: string): Promise; /** * Given an HTML element, get the URLs of the files that should be downloaded and weren't treated by @@ -355,7 +356,7 @@ export interface CorePluginFileHandler extends CoreDelegateHandler { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the size. */ - getFileSize?(file: CoreWSExternalFile, siteId?: string): Promise; + getFileSize?(file: CoreWSFile, siteId?: string): Promise; /** * Check if a file is downloadable. @@ -364,7 +365,7 @@ export interface CorePluginFileHandler extends CoreDelegateHandler { * @param siteId Site ID. If not defined, current site. * @return Promise resolved with a boolean and a reason why it isn't downloadable if needed. */ - isFileDownloadable?(file: CoreWSExternalFile, siteId?: string): Promise; + isFileDownloadable?(file: CoreWSFile, siteId?: string): Promise; /** * Check whether the file should be treated by this handler. It is used in functions where the component isn't used. @@ -372,7 +373,7 @@ export interface CorePluginFileHandler extends CoreDelegateHandler { * @param file The file data. * @return Whether the file should be treated by this handler. */ - shouldHandleFile?(file: CoreWSExternalFile): boolean; + shouldHandleFile?(file: CoreWSFile): boolean; /** * Treat a downloaded file. diff --git a/src/core/services/utils/mimetype.ts b/src/core/services/utils/mimetype.ts index 3296d5d43..92ec8ab48 100644 --- a/src/core/services/utils/mimetype.ts +++ b/src/core/services/utils/mimetype.ts @@ -13,17 +13,18 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { FileEntry } from '@ionic-native/file'; +import { FileEntry } from '@ionic-native/file/ngx'; import { CoreFile } from '@services/file'; import { CoreTextUtils } from '@services/utils/text'; import { makeSingleton, Translate } from '@singletons'; import { CoreLogger } from '@singletons/logger'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { CoreUtils } from '@services/utils/utils'; import extToMime from '@/assets/exttomime.json'; import mimeToExt from '@/assets/mimetoext.json'; +import { CoreFileEntry, CoreFileHelper } from '@services/file-helper'; interface MimeTypeInfo { type: string; @@ -163,7 +164,7 @@ export class CoreMimetypeUtilsProvider { * @param file File object. * @param path Alternative path that will override fileurl from file object. */ - getEmbeddedHtml(file: CoreWSExternalFile | FileEntry, path?: string): string { + getEmbeddedHtml(file: CoreFileEntry, path?: string): string { const filename = CoreUtils.isFileEntry(file) ? (file as FileEntry).name : file.filename; const extension = !CoreUtils.isFileEntry(file) && file.mimetype ? this.getExtension(file.mimetype) @@ -173,7 +174,7 @@ export class CoreMimetypeUtilsProvider { : (extension && this.getMimeType(extension)); // @todo linting: See if this can be removed - (file as CoreWSExternalFile).mimetype = mimeType; + (file as CoreWSFile).mimetype = mimeType; if (extension && this.canBeEmbedded(extension)) { const embedType = this.getExtensionType(extension); @@ -181,7 +182,7 @@ export class CoreMimetypeUtilsProvider { // @todo linting: See if this can be removed (file as { embedType?: string }).embedType = embedType; - path = path ?? (CoreUtils.isFileEntry(file) ? file.toURL() : file.fileurl); + path = path ?? (CoreUtils.isFileEntry(file) ? file.toURL() : CoreFileHelper.getFileUrl(file)); path = path && CoreFile.convertFileSrc(path); switch (embedType) { @@ -401,7 +402,7 @@ export class CoreMimetypeUtilsProvider { * @param capitalise If true, capitalises first character of result. * @return Type description. */ - getMimetypeDescription(obj: FileEntry | CoreWSExternalFile | string, capitalise?: boolean): string { + getMimetypeDescription(obj: CoreFileEntry | string, capitalise?: boolean): string { const langPrefix = 'assets.mimetypes.'; let filename: string | undefined = ''; let mimetype: string | undefined = ''; diff --git a/src/core/services/utils/text.ts b/src/core/services/utils/text.ts index 34682af36..a7f8013ca 100644 --- a/src/core/services/utils/text.ts +++ b/src/core/services/utils/text.ts @@ -20,9 +20,10 @@ import { CoreApp } from '@services/app'; import { CoreLang } from '@services/lang'; import { CoreAnyError, CoreError } from '@classes/errors/error'; import { makeSingleton, ModalController, Translate } from '@singletons'; -import { CoreWSExternalFile } from '@services/ws'; +import { CoreWSFile } from '@services/ws'; import { Locutus } from '@singletons/locutus'; import { CoreViewerTextComponent } from '@features/viewer/components/text/text'; +import { CoreFileHelper } from '@services/file-helper'; /** * Different type of errors the app can treat. @@ -462,7 +463,7 @@ export class CoreTextUtilsProvider { text: string, component?: string, componentId?: string | number, - files?: CoreWSExternalFile[], + files?: CoreWSFile[], filter?: boolean, contextLevel?: string, instanceId?: number, @@ -555,9 +556,9 @@ export class CoreTextUtilsProvider { * @param files Files to extract the URL from. They need to have the URL in a 'url' or 'fileurl' attribute. * @return Pluginfile URL, undefined if no files found. */ - getTextPluginfileUrl(files: CoreWSExternalFile[]): string | undefined { + getTextPluginfileUrl(files: CoreWSFile[]): string | undefined { if (files?.length) { - const url = files[0].fileurl; + const url = CoreFileHelper.getFileUrl(files[0]); // Remove text after last slash (encoded or not). return url?.substr(0, Math.max(url.lastIndexOf('/'), url.lastIndexOf('%2F'))); @@ -759,7 +760,7 @@ export class CoreTextUtilsProvider { replaceDraftfileUrls( siteUrl: string, text: string, - files: CoreWSExternalFile[], + files: CoreWSFile[], ): { text: string; replaceMap?: {[url: string]: string} } { if (!text || !files || !files.length) { @@ -776,7 +777,7 @@ export class CoreTextUtilsProvider { // Index the pluginfile URLs by file name. const pluginfileMap: {[name: string]: string} = {}; files.forEach((file) => { - pluginfileMap[file.filename!] = file.fileurl; + pluginfileMap[file.filename!] = CoreFileHelper.getFileUrl(file); }); // Replace each draftfile with the corresponding pluginfile URL. @@ -812,7 +813,7 @@ export class CoreTextUtilsProvider { * @param files Files to extract the pluginfile URL from. They need to have the URL in a url or fileurl attribute. * @return Treated text. */ - replacePluginfileUrls(text: string, files: CoreWSExternalFile[]): string { + replacePluginfileUrls(text: string, files: CoreWSFile[]): string { if (text && typeof text == 'string') { const fileURL = this.getTextPluginfileUrl(files); if (fileURL) { @@ -830,7 +831,7 @@ export class CoreTextUtilsProvider { * @param replaceMap Map of the replacements that were done. * @return Treated text. */ - restoreDraftfileUrls(siteUrl: string, treatedText: string, originalText: string, files: CoreWSExternalFile[]): string { + restoreDraftfileUrls(siteUrl: string, treatedText: string, originalText: string, files: CoreWSFile[]): string { if (!treatedText || !files || !files.length) { return treatedText; } @@ -848,7 +849,7 @@ export class CoreTextUtilsProvider { return; // Original URL not found, skip. } - treatedText = treatedText.replace(new RegExp(this.escapeForRegex(file.fileurl), 'g'), matches[0]); + treatedText = treatedText.replace(new RegExp(this.escapeForRegex(CoreFileHelper.getFileUrl(file)), 'g'), matches[0]); }); return treatedText; @@ -861,7 +862,7 @@ export class CoreTextUtilsProvider { * @param files Files to extract the pluginfile URL from. They need to have the URL in a url or fileurl attribute. * @return Treated text. */ - restorePluginfileUrls(text: string, files: CoreWSExternalFile[]): string { + restorePluginfileUrls(text: string, files: CoreWSFile[]): string { if (text && typeof text == 'string') { const fileURL = this.getTextPluginfileUrl(files); if (fileURL) { @@ -1103,7 +1104,7 @@ export class CoreTextUtilsProvider { export type CoreTextUtilsViewTextOptions = { component?: string; // Component to link the embedded files to. componentId?: string | number; // An ID to use in conjunction with the component. - files?: CoreWSExternalFile[]; // List of files to display along with the text. + files?: CoreWSFile[]; // List of files to display along with the text. filter?: boolean; // Whether the text should be filtered. contextLevel?: string; // The context level. instanceId?: number; // The instance ID related to the context. diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index bf83806e2..4e0ddb726 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -14,14 +14,14 @@ import { Injectable, NgZone } from '@angular/core'; import { InAppBrowserObject, InAppBrowserOptions } from '@ionic-native/in-app-browser'; -import { FileEntry } from '@ionic-native/file'; +import { FileEntry } from '@ionic-native/file/ngx'; import { Subscription } from 'rxjs'; import { CoreApp } from '@services/app'; import { CoreEvents } from '@singletons/events'; import { CoreFile } from '@services/file'; import { CoreLang } from '@services/lang'; -import { CoreWS, CoreWSExternalFile } from '@services/ws'; +import { CoreWS, CoreWSFile } from '@services/ws'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreTextUtils } from '@services/utils/text'; @@ -31,6 +31,7 @@ import { CoreLogger } from '@singletons/logger'; import { CoreFileSizeSum } from '@services/plugin-file-delegate'; import { CoreViewerQRScannerComponent } from '@features/viewer/components/qr-scanner/qr-scanner'; import { CoreCanceledError } from '@classes/errors/cancelederror'; +import { CoreFileEntry } from '@services/file-helper'; type TreeNode = T & { children: TreeNode[] }; @@ -379,11 +380,7 @@ export class CoreUtilsProvider { /** * Execute promises one depending on the previous. * - * @param orderedPromisesData Data to be executed including the following values: - * - func: Function to be executed. - * - context: Context to pass to the function. This allows using "this" inside the function. - * - params: Array of data to be sent to the function. - * - blocking: Boolean. If promise should block the following. + * @param orderedPromisesData Data to be executed. * @return Promise resolved when all promises are resolved. */ executeOrderedPromises(orderedPromisesData: OrderedPromiseData[]): Promise { @@ -748,7 +745,7 @@ export class CoreUtilsProvider { * @param file File. * @return Type guard indicating if the file is a FileEntry. */ - isFileEntry(file: FileEntry | CoreWSExternalFile): file is FileEntry { + isFileEntry(file: CoreFileEntry): file is FileEntry { return 'isFile' in file; } @@ -768,7 +765,7 @@ export class CoreUtilsProvider { * @param files List of files. * @return String with error message if repeated, false if no repeated. */ - hasRepeatedFilenames(files: (FileEntry | CoreWSExternalFile)[]): string | false { + hasRepeatedFilenames(files: CoreFileEntry[]): string | false { if (!files || !files.length) { return false; } @@ -1368,7 +1365,7 @@ export class CoreUtilsProvider { * @return File size and a boolean to indicate if it is the total size or only partial. * @deprecated since 3.8.0. Use CorePluginFileDelegate.getFilesSize instead. */ - sumFileSizes(files: CoreWSExternalFile[]): CoreFileSizeSum { + sumFileSizes(files: CoreWSFile[]): CoreFileSizeSum { const result = { size: 0, total: true, diff --git a/src/core/services/ws.ts b/src/core/services/ws.ts index 912cd65e9..13992fb2a 100644 --- a/src/core/services/ws.ts +++ b/src/core/services/ws.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { HttpResponse, HttpParams } from '@angular/common/http'; -import { FileEntry } from '@ionic-native/file'; +import { FileEntry } from '@ionic-native/file/ngx'; import { FileUploadOptions } from '@ionic-native/file-transfer/ngx'; import { Md5 } from 'ts-md5/dist/md5'; import { Observable } from 'rxjs'; @@ -1087,6 +1087,43 @@ export type CoreWSExternalFile = { repositorytype?: string; // The repository type for external files. }; +/** + * Structure of files returned by stored_file_exporter. + */ +export type CoreWSStoredFile = { + contextid: number; // Contextid. + component: string; // Component. + filearea: string; // Filearea. + itemid: number; // Itemid. + filepath: string; // Filepath. + filename: string; // Filename. + isdir: boolean; // Isdir. + isimage: boolean; // Isimage. + timemodified: number; // Timemodified. + timecreated: number; // Timecreated. + filesize: number; // Filesize. + author: string; // Author. + license: string; // License. + filenameshort: string; // Filenameshort. + filesizeformatted: string; // Filesizeformatted. + icon: string; // Icon. + timecreatedformatted: string; // Timecreatedformatted. + timemodifiedformatted: string; // Timemodifiedformatted. + url: string; // Url. + urls: { + export?: string; // The URL used to export the attachment. + }; + html: { + plagiarism?: string; // The HTML source for the Plagiarism Response. + }; + mimetype: undefined; // File mimetype. @todo Not implemented yet in Moodle, see MDL-71354. +}; + +/** + * Common file structures returned by WS. + */ +export type CoreWSFile = CoreWSExternalFile | CoreWSStoredFile; + /** * Data returned by date_exporter. */ diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index af5fa751f..49955b3bf 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -487,3 +487,7 @@ ion-button.core-button-select { @include padding(null, null, null, 15px * $i + 16px); } } + +textarea:not([core-auto-rows]) { + height: 200px; +}