// (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 { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { makeSingleton, Translate } from '@singletons/core.singletons'; import { CoreWSExternalWarning } from '@services/ws'; import { CoreCourseBase } from '@/types/global'; /* * Service to handle groups. */ @Injectable() export class CoreGroupsProvider { // Group mode constants. static readonly NOGROUPS = 0; static readonly SEPARATEGROUPS = 1; static readonly VISIBLEGROUPS = 2; protected readonly ROOT_CACHE_KEY = 'mmGroups:'; /** * Check if group mode of an activity is enabled. * * @param cmId Course module ID. * @param siteId Site ID. If not defined, current site. * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @return Promise resolved with true if the activity has groups, resolved with false otherwise. */ async activityHasGroups(cmId: number, siteId?: string, ignoreCache?: boolean): Promise { try { const groupmode = await this.getActivityGroupMode(cmId, siteId, ignoreCache); return groupmode === CoreGroupsProvider.SEPARATEGROUPS || groupmode === CoreGroupsProvider.VISIBLEGROUPS; } catch (error) { return false; } } /** * Get the groups allowed in an activity. * * @param cmId Course module ID. * @param userId User ID. If not defined, use current user. * @param siteId Site ID. If not defined, current site. * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @return Promise resolved when the groups are retrieved. */ async getActivityAllowedGroups(cmId: number, userId?: number, siteId?: string, ignoreCache?: boolean): Promise { const site = await CoreSites.instance.getSite(siteId); userId = userId || site.getUserId(); const params = { cmid: cmId, userid: userId, }; const preSets: CoreSiteWSPreSets = { cacheKey: this.getActivityAllowedGroupsCacheKey(cmId, userId), updateFrequency: CoreSite.FREQUENCY_RARELY, }; if (ignoreCache) { preSets.getFromCache = false; preSets.emergencyCache = false; } const response = await site.read('core_group_get_activity_allowed_groups', params, preSets); if (!response || !response.groups) { throw null; } return response; } /** * Get cache key for group mode WS calls. * * @param cmId Course module ID. * @param userId User ID. * @return Cache key. */ protected getActivityAllowedGroupsCacheKey(cmId: number, userId: number): string { return this.ROOT_CACHE_KEY + 'allowedgroups:' + cmId + ':' + userId; } /** * Get the groups allowed in an activity if they are allowed. * * @param cmId Course module ID. * @param userId User ID. If not defined, use current user. * @param siteId Site ID. If not defined, current site. * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @return Promise resolved when the groups are retrieved. If not allowed, empty array will be returned. */ async getActivityAllowedGroupsIfEnabled(cmId: number, userId?: number, siteId?: string, ignoreCache?: boolean): Promise { siteId = siteId || CoreSites.instance.getCurrentSiteId(); // Get real groupmode, in case it's forced by the course. const hasGroups = await this.activityHasGroups(cmId, siteId, ignoreCache); if (hasGroups) { // Get the groups available for the user. return this.getActivityAllowedGroups(cmId, userId, siteId, ignoreCache); } return { groups: [], }; } /** * Helper function to get activity group info (group mode and list of groups). * * @param cmId Course module ID. * @param addAllParts Deprecated. * @param userId User ID. If not defined, use current user. * @param siteId Site ID. If not defined, current site. * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @return Promise resolved with the group info. */ async getActivityGroupInfo(cmId: number, addAllParts?: boolean, userId?: number, siteId?: string, ignoreCache?: boolean): Promise { const groupInfo: CoreGroupInfo = { groups: [], }; const groupMode = await this.getActivityGroupMode(cmId, siteId, ignoreCache); groupInfo.separateGroups = groupMode === CoreGroupsProvider.SEPARATEGROUPS; groupInfo.visibleGroups = groupMode === CoreGroupsProvider.VISIBLEGROUPS; let result: CoreGroupGetActivityAllowedGroupsResponse; if (groupInfo.separateGroups || groupInfo.visibleGroups) { result = await this.getActivityAllowedGroups(cmId, userId, siteId, ignoreCache); } else { result = { groups: [], }; } if (result.groups.length <= 0) { groupInfo.separateGroups = false; groupInfo.visibleGroups = false; groupInfo.defaultGroupId = 0; } else { // The "canaccessallgroups" field was added in 3.4. Add all participants for visible groups in previous versions. if (result.canaccessallgroups || (typeof result.canaccessallgroups == 'undefined' && groupInfo.visibleGroups)) { groupInfo.groups.push({ id: 0, name: Translate.instance.instant('core.allparticipants') }); groupInfo.defaultGroupId = 0; } else { groupInfo.defaultGroupId = result.groups[0].id; } groupInfo.groups = groupInfo.groups.concat(result.groups); } return groupInfo; } /** * Get the group mode of an activity. * * @param cmId Course module ID. * @param siteId Site ID. If not defined, current site. * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @return Promise resolved when the group mode is retrieved. */ async getActivityGroupMode(cmId: number, siteId?: string, ignoreCache?: boolean): Promise { const site = await CoreSites.instance.getSite(siteId); const params = { cmid: cmId, }; const preSets: CoreSiteWSPreSets = { cacheKey: this.getActivityGroupModeCacheKey(cmId), updateFrequency: CoreSite.FREQUENCY_RARELY, }; if (ignoreCache) { preSets.getFromCache = false; preSets.emergencyCache = false; } const response = await site.read('core_group_get_activity_groupmode', params, preSets); if (!response || typeof response.groupmode == 'undefined') { throw null; } return response.groupmode; } /** * Get cache key for group mode WS calls. * * @param cmId Course module ID. * @return Cache key. */ protected getActivityGroupModeCacheKey(cmId: number): string { return this.ROOT_CACHE_KEY + 'groupmode:' + cmId; } /** * Get user groups in all the user enrolled courses. * * @param siteId Site to get the groups from. If not defined, use current site. * @return Promise resolved when the groups are retrieved. */ async getAllUserGroups(siteId?: string): Promise { const site = await CoreSites.instance.getSite(siteId); siteId = siteId || site.getId(); if (site.isVersionGreaterEqualThan('3.6')) { return this.getUserGroupsInCourse(0, siteId); } // @todo Get courses. } /** * Get user groups in all the supplied courses. * * @param courses List of courses or course ids to get the groups from. * @param siteId Site to get the groups from. If not defined, use current site. * @param userId ID of the user. If not defined, use the userId related to siteId. * @return Promise resolved when the groups are retrieved. */ async getUserGroups(courses: CoreCourseBase[] | number[], siteId?: string, userId?: number): Promise { // Get all courses one by one. const promises = this.getCourseIds(courses).map((courseId) => this.getUserGroupsInCourse(courseId, siteId, userId)); const courseGroups = await Promise.all(promises); return [].concat(...courseGroups); } /** * Get user groups in a course. * * @param courseId ID of the course. 0 to get all enrolled courses groups (Moodle version > 3.6). * @param siteId Site to get the groups from. If not defined, use current site. * @param userId ID of the user. If not defined, use ID related to siteid. * @return Promise resolved when the groups are retrieved. */ async getUserGroupsInCourse(courseId: number, siteId?: string, userId?: number): Promise { const site = await CoreSites.instance.getSite(siteId); userId = userId || site.getUserId(); const data = { userid: userId, courseid: courseId, }; const preSets = { cacheKey: this.getUserGroupsInCourseCacheKey(courseId, userId), updateFrequency: CoreSite.FREQUENCY_RARELY, }; const response = await site.read('core_group_get_course_user_groups', data, preSets); if (!response || !response.groups) { throw null; } return response.groups; } /** * Get prefix cache key for user groups in course WS calls. * * @return Prefix Cache key. */ protected getUserGroupsInCoursePrefixCacheKey(): string { return this.ROOT_CACHE_KEY + 'courseGroups:'; } /** * Get cache key for user groups in course WS calls. * * @param courseId Course ID. * @param userId User ID. * @return Cache key. */ protected getUserGroupsInCourseCacheKey(courseId: number, userId: number): string { return this.getUserGroupsInCoursePrefixCacheKey() + courseId + ':' + userId; } /** * Invalidates activity allowed groups. * * @param cmId Course module ID. * @param userId User ID. If not defined, use current user. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateActivityAllowedGroups(cmId: number, userId?: number, siteId?: string): Promise { const site = await CoreSites.instance.getSite(siteId); userId = userId || site.getUserId(); await site.invalidateWsCacheForKey(this.getActivityAllowedGroupsCacheKey(cmId, userId)); } /** * Invalidates activity group mode. * * @param cmId Course module ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateActivityGroupMode(cmId: number, siteId?: string): Promise { const site = await CoreSites.instance.getSite(siteId); await site.invalidateWsCacheForKey(this.getActivityGroupModeCacheKey(cmId)); } /** * Invalidates all activity group info: mode and allowed groups. * * @param cmId Course module ID. * @param userId User ID. If not defined, use current user. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateActivityGroupInfo(cmId: number, userId?: number, siteId?: string): Promise { const promises = []; promises.push(this.invalidateActivityAllowedGroups(cmId, userId, siteId)); promises.push(this.invalidateActivityGroupMode(cmId, siteId)); await Promise.all(promises); } /** * Invalidates user groups in all user enrolled courses. * * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateAllUserGroups(siteId?: string): Promise { const site = await CoreSites.instance.getSite(siteId); if (site.isVersionGreaterEqualThan('3.6')) { return this.invalidateUserGroupsInCourse(0, siteId); } await site.invalidateWsCacheForKeyStartingWith(this.getUserGroupsInCoursePrefixCacheKey()); } /** * Invalidates user groups in courses. * * @param courses List of courses or course ids. * @param siteId Site ID. If not defined, current site. * @param userId User ID. If not defined, use current user. * @return Promise resolved when the data is invalidated. */ async invalidateUserGroups(courses: CoreCourseBase[] | number[], siteId?: string, userId?: number): Promise { const site = await CoreSites.instance.getSite(siteId); userId = userId || site.getUserId(); const promises = this.getCourseIds(courses).map((courseId) => this.invalidateUserGroupsInCourse(courseId, site.id, userId)); await Promise.all(promises); } /** * Invalidates user groups in course. * * @param courseId ID of the course. 0 to get all enrolled courses groups (Moodle version > 3.6). * @param siteId Site ID. If not defined, current site. * @param userId User ID. If not defined, use current user. * @return Promise resolved when the data is invalidated. */ async invalidateUserGroupsInCourse(courseId: number, siteId?: string, userId?: number): Promise { const site = await CoreSites.instance.getSite(siteId); userId = userId || site.getUserId(); await site.invalidateWsCacheForKey(this.getUserGroupsInCourseCacheKey(courseId, userId)); } /** * Validate a group ID. If the group is not visible by the user, it will return the first group ID. * * @param groupId Group ID to validate. * @param groupInfo Group info. * @return Group ID to use. */ validateGroupId(groupId: number, groupInfo: CoreGroupInfo): number { if (groupId > 0 && groupInfo && groupInfo.groups && groupInfo.groups.length > 0) { // Check if the group is in the list of groups. if (groupInfo.groups.some((group) => groupId == group.id)) { return groupId; } } return groupInfo.defaultGroupId; } protected getCourseIds(courses: CoreCourseBase[] | number[]): number[] { return courses.length > 0 && typeof courses[0] === 'object' ? (courses as CoreCourseBase[]).map((course) => course.id) : courses as number[]; } } export class CoreGroups extends makeSingleton(CoreGroupsProvider) {} /** * Specific group info. */ export type CoreGroup = { id: number; // Group ID. name: string; // Multilang compatible name, course unique'. description?: string; // Group description text. descriptionformat?: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). idnumber?: string; // Id number. courseid?: number; // Coure Id. }; /** * Group info for an activity. */ export type CoreGroupInfo = { /** * List of groups. */ groups?: CoreGroup[]; /** * Whether it's separate groups. */ separateGroups?: boolean; /** * Whether it's visible groups. */ visibleGroups?: boolean; /** * The group ID to use by default. If all participants is visible, 0 will be used. First group ID otherwise. */ defaultGroupId?: number; }; /** * WS core_group_get_activity_allowed_groups response type. */ export type CoreGroupGetActivityAllowedGroupsResponse = { groups: CoreGroup[]; // List of groups. canaccessallgroups?: boolean; // Whether the user will be able to access all the activity groups. warnings?: CoreWSExternalWarning[]; };