diff --git a/src/core/features/filter/services/filter-delegate.ts b/src/core/features/filter/services/filter-delegate.ts index 29ed0f245dd..124b60600f0 100644 --- a/src/core/features/filter/services/filter-delegate.ts +++ b/src/core/features/filter/services/filter-delegate.ts @@ -15,7 +15,7 @@ import { Injectable, ViewContainerRef } from '@angular/core'; import { CoreSites } from '@services/sites'; -import { CoreFilter, CoreFilterFilter, CoreFilterFormatTextOptions } from './filter'; +import { CoreFilter, CoreFilterFilter, CoreFilterFormatTextOptions, CoreFilterStateValue } from './filter'; import { CoreFilterDefaultHandler } from './handlers/default-filter'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { CoreSite } from '@classes/sites/site'; @@ -169,7 +169,7 @@ export class CoreFilterDelegateService extends CoreDelegate { filter: handler.filterName, inheritedstate: 1, instanceid: instanceId, - localstate: 1, + localstate: CoreFilterStateValue.ON, }); } @@ -245,7 +245,10 @@ export class CoreFilterDelegateService extends CoreDelegate { skipFilters?: string[], ): boolean { - if (filter.localstate == -1 || (filter.localstate == 0 && filter.inheritedstate == -1)) { + if ( + filter.localstate === CoreFilterStateValue.OFF || + (filter.localstate === CoreFilterStateValue.INHERIT && filter.inheritedstate === CoreFilterStateValue.OFF) + ) { // Filter is disabled, ignore it. return false; } @@ -255,7 +258,7 @@ export class CoreFilterDelegateService extends CoreDelegate { return false; } - if (skipFilters && skipFilters.indexOf(filter.filter) != -1) { + if (skipFilters && skipFilters.indexOf(filter.filter) !== -1) { // Skip this filter. return false; } diff --git a/src/core/features/filter/services/filter-helper.ts b/src/core/features/filter/services/filter-helper.ts index a9dff313c9d..fd615deba19 100644 --- a/src/core/features/filter/services/filter-helper.ts +++ b/src/core/features/filter/services/filter-helper.ts @@ -23,6 +23,8 @@ import { CoreFilterFormatTextOptions, CoreFilterClassifiedFilters, CoreFiltersGetAvailableInContextWSParamContext, + CoreFilterStateValue, + CoreFilterAllStates, } from './filter'; import { CoreCourse } from '@features/course/services/course'; import { CoreCourses } from '@features/courses/services/courses'; @@ -198,6 +200,11 @@ export class CoreFilterHelperProvider { return await CoreFilterDelegate.getEnabledFilters(contextLevel, instanceId); } + const filters = await this.getFiltersInContextUsingAllStates(contextLevel, instanceId, options, site); + if (filters) { + return filters; + } + const courseId = options.courseId; let hasFilters = true; @@ -238,6 +245,95 @@ export class CoreFilterHelperProvider { } } + /** + * Get filters in context using the all states data. + * + * @param contextLevel The context level. + * @param instanceId Instance ID related to the context. + * @param options Options. + * @param site Site. + * @returns Filters, undefined if all states cannot be used. + */ + protected async getFiltersInContextUsingAllStates( + contextLevel: ContextLevel, + instanceId: number, + options: CoreFilterFormatTextOptions = {}, + site?: CoreSite, + ): Promise { + site = site || CoreSites.getCurrentSite(); + + if (!CoreFilter.canGetAllStatesInSite(site)) { + return; + } + + const allStates = await CoreFilter.getAllStates({ siteId: site?.getId() }); + if ( + contextLevel !== ContextLevel.SYSTEM && + contextLevel !== ContextLevel.COURSECAT && + this.hasCategoryOverride(allStates) + ) { + // A category has an override, we cannot calculate the right filters for child contexts. + return; + } + + const contexts = CoreFilter.getContextsTreeList(contextLevel, instanceId, { courseId: options.courseId }); + const contextId = Object.values(allStates[contextLevel]?.[instanceId] ?? {})[0]?.contextid; + + const filters: Record = {}; + contexts.reverse().forEach((context) => { + const isParentContext = context.contextLevel !== contextLevel; + const filtersInContext = allStates[context.contextLevel]?.[context.instanceId]; + if (!filtersInContext) { + return; + } + + for (const name in filtersInContext) { + const filterInContext = filtersInContext[name]; + if (filterInContext.localstate === CoreFilterStateValue.DISABLED) { + // Ignore disabled filters to make it consistent with available in context. + continue; + } + + filters[name] = { + contextlevel: contextLevel, + instanceid: instanceId, + contextid: contextId, + filter: name, + localstate: isParentContext ? CoreFilterStateValue.INHERIT : filterInContext.localstate, + inheritedstate: isParentContext ? + filterInContext.localstate : + filters[name]?.inheritedstate ?? filterInContext.localstate, + }; + } + }); + + return Object.values(filters); + } + + /** + * Check if there is an override for a category in the states of all filters. + * + * @param states States to check. + * @returns True if has category override, false otherwise. + */ + protected hasCategoryOverride(states: CoreFilterAllStates): boolean { + if (!states[ContextLevel.COURSECAT]) { + return false; + } + + for (const instanceId in states[ContextLevel.COURSECAT]) { + for (const name in states[ContextLevel.COURSECAT][instanceId]) { + if ( + states[ContextLevel.COURSECAT][instanceId][name].localstate !== states[ContextLevel.SYSTEM][0][name].localstate + ) { + return true; + } + } + } + + return false; + } + /** * Get filters and format text. * diff --git a/src/core/features/filter/services/filter.ts b/src/core/features/filter/services/filter.ts index ad25ecc2900..7767dd5a304 100644 --- a/src/core/features/filter/services/filter.ts +++ b/src/core/features/filter/services/filter.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreNetwork } from '@services/network'; -import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; +import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; import { CoreSite } from '@classes/sites/site'; import { CoreWSExternalWarning } from '@services/ws'; import { CoreTextUtils } from '@services/utils/text'; @@ -50,6 +50,8 @@ export class CoreFilterProvider { }; } = {}; + // @todo: Check if an "all states" memory cache is needed or not. + constructor() { this.logger = CoreLogger.getInstance('CoreFilterProvider'); @@ -62,11 +64,37 @@ export class CoreFilterProvider { }); } + /** + * Check if getting all states is available in site. + * + * @param siteId Site ID. If not defined, current site. + * @returns Whether it's available. + * @since 4.4 + */ + async canGetAllStates(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return this.canGetAllStatesInSite(site); + } + + /** + * Check if getting all states is available in site. + * + * @param site Site. If not defined, current site. + * @returns Whether it's available. + * @since 4.4 + */ + canGetAllStatesInSite(site?: CoreSite): boolean { + site = site || CoreSites.getCurrentSite(); + + return !!(site?.wsAvailable('core_filters_get_all_states')); + } + /** * Returns whether or not we can get the available filters: the WS is available and the feature isn't disabled. * * @param siteId Site ID. If not defined, current site. - * @returns Promise resolved with boolean: whethe can get filters. + * @returns Whether can get filters. */ async canGetFilters(siteId?: string): Promise { const disabled = await this.checkFiltersDisabled(siteId); @@ -78,7 +106,7 @@ export class CoreFilterProvider { * Returns whether or not we can get the available filters: the WS is available and the feature isn't disabled. * * @param site Site. If not defined, current site. - * @returns Promise resolved with boolean: whethe can get filters. + * @returns Whether can get filters. */ canGetFiltersInSite(site?: CoreSite): boolean { return !this.checkFiltersDisabledInSite(site); @@ -153,7 +181,7 @@ export class CoreFilterProvider { // Simulate the system context based on the inherited data. filter.contextlevel = ContextLevel.SYSTEM; filter.instanceid = 0; - filter.contextid = -1; + filter.contextid = undefined; filter.localstate = filter.inheritedstate; } @@ -241,6 +269,53 @@ export class CoreFilterProvider { return text; } + /** + * Get cache key for get all states WS call. + * + * @returns Cache key. + */ + protected getAllStatesCacheKey(): string { + return this.ROOT_CACHE_KEY + 'allStates'; + } + + /** + * Get all the states for filters. + * + * @param options Options. + * @returns Promise resolved with the filters classified by context. + * @since 4.4 + */ + async getAllStates(options: CoreSitesCommonWSOptions = {}): Promise { + const site = await CoreSites.getSite(options.siteId); + + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getAllStatesCacheKey(), + updateFrequency: CoreSite.FREQUENCY_RARELY, + // Use stale while revalidate by default, but always use the first value. If data is updated it will be stored in DB. + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy ?? CoreSitesReadingStrategy.STALE_WHILE_REVALIDATE), + }; + + const result = await site.read('core_filters_get_all_states', {}, preSets); + + const classified: CoreFilterAllStates = {}; + + result.filters.forEach((filter) => { + classified[filter.contextlevel] = classified[filter.contextlevel] || {}; + classified[filter.contextlevel][filter.instanceid] = classified[filter.contextlevel][filter.instanceid] || {}; + + classified[filter.contextlevel][filter.instanceid][filter.filter] = { + contextlevel: filter.contextlevel, + instanceid: filter.instanceid, + contextid: filter.contextid, + filter: filter.filter, + localstate: filter.state, + inheritedstate: filter.state, + }; + }); + + return classified; + } + /** * Get cache key for available in contexts WS calls. * @@ -370,6 +445,45 @@ export class CoreFilterProvider { } } + /** + * Given a context, return the list of contexts used in the filters inheritance tree, from bottom to top. + * E.g. when using module, it will return the module context, course context (if course ID is supplied), category context + * (if categoy ID is supplied) and system context. + * + * @param contextLevel Context level. + * @param instanceId Instance ID. + * @param options Options + * @returns List of contexts. + */ + getContextsTreeList( + contextLevel: ContextLevel, + instanceId: number, + options: {courseId?: number; categoryId?: number} = {}, + ): { contextLevel: ContextLevel; instanceId: number }[] { + // Make sure context has been converted. + const newContext = CoreFilter.convertContext(contextLevel, instanceId, options); + contextLevel = newContext.contextLevel; + instanceId = newContext.instanceId; + + const contexts = [ + { contextLevel, instanceId }, + ]; + + if (contextLevel === ContextLevel.MODULE && options.courseId) { + contexts.push({ contextLevel: ContextLevel.COURSE, instanceId: options.courseId }); + } + + if ((contextLevel === ContextLevel.MODULE || contextLevel === ContextLevel.COURSE) && options.categoryId) { + contexts.push({ contextLevel: ContextLevel.COURSECAT, instanceId: options.categoryId }); + } + + if (contextLevel !== ContextLevel.SYSTEM) { + contexts.push({ contextLevel: ContextLevel.SYSTEM, instanceId: 0 }); + } + + return contexts; + } + /** * Invalidates all available in context WS calls. * @@ -382,6 +496,18 @@ export class CoreFilterProvider { await site.invalidateWsCacheForKeyStartingWith(this.getAvailableInContextsPrefixCacheKey()); } + /** + * Invalidates get all states WS call. + * + * @param siteId Site ID (empty for current site). + * @returns Promise resolved when the data is invalidated. + */ + async invalidateAllStates(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getAllStatesCacheKey()); + } + /** * Invalidates available in context WS call. * @@ -499,15 +625,15 @@ export type CoreFiltersGetAvailableInContextWSParamContext = { }; /** - * Filter object returned by core_filters_get_available_in_context. + * Filter data. */ export type CoreFilterFilter = { contextlevel: ContextLevel; // The context level where the filters are: (coursecat, course, module). instanceid: number; // The instance id of item associated with the context. - contextid: number; // The context id. + contextid?: number; // The context id. It will be undefined in cases where it cannot be calculated in the app. filter: string; // Filter plugin name. - localstate: number; // Filter state: 1 for on, -1 for off, 0 if inherit. - inheritedstate: number; // 1 or 0 to use when localstate is set to inherit. + localstate: CoreFilterStateValue; // Filter state: 1 for on, -1 for off, 0 if inherit. + inheritedstate: CoreFilterStateValue; // 1 or 0 to use when localstate is set to inherit. }; /** @@ -518,6 +644,36 @@ export type CoreFilterGetAvailableInContextResult = { warnings: CoreWSExternalWarning[]; // List of warnings. }; +/** + * Filter state returned by core_filters_get_all_states. + */ +export type CoreFilterState = { + contextlevel: ContextLevel; // The context level where the filters are: (coursecat, course, module). + instanceid: number; // The instance id of item associated with the context. + contextid: number; // The context id. + filter: string; // Filter plugin name. + state: CoreFilterStateValue; // Filter state: 1 for on, -1 for off, -9999 if disabled. + sortorder: number; // Sort order. +}; + +/** + * Context levels enumeration. + */ +export const enum CoreFilterStateValue { + ON = 1, + INHERIT = 0, + OFF = -1, + DISABLED = -9999, +} + +/** + * Result of core_filters_get_all_states. + */ +export type CoreFilterGetAllStatesWSResponse = { + filters: CoreFilterState[]; // Filter state. + warnings: CoreWSExternalWarning[]; // List of warnings. +}; + /** * Options that can be passed to format text. */ @@ -541,3 +697,14 @@ export type CoreFilterClassifiedFilters = { [instanceid: number]: CoreFilterFilter[]; }; }; + +/** + * All filter states classified by context, instance and filter name. + */ +export type CoreFilterAllStates = { + [contextlevel: string]: { + [instanceid: number]: { + [filtername: string]: CoreFilterFilter; + }; + }; +};