// (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 { CoreNetwork } from '@services/network'; 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, Translate } 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 (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 && 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('_'); const item = (parts[1] || '').replace(/\[.*\]/, ''); // Remove [0] and similar. return { ...valueEntry, typ: parts[0], item: Number(item), }; }); offlineValuesArray.forEach((value) => { if (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(Translate.instant('core.course.modulenotfound')); } /** * 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.PREFER_CACHE, 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)); } /** * 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.PREFER_CACHE, 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 (!CoreNetwork.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; };