MOBILE-3636 grades: Add grade types

main
Pau Ferrer Ocaña 2021-02-10 11:37:12 +01:00
parent a4225b8c02
commit 22395607ae
5 changed files with 132 additions and 70 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

View File

@ -522,7 +522,14 @@ export class CoreCourseProvider {
* @param siteId Site ID. If not defined, current site. * @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the module's grade info. * @return Promise resolved with the module's grade info.
*/ */
async getModuleBasicGradeInfo(moduleId: number, siteId?: string): Promise<CoreCourseModuleGradeInfo | false> { async getModuleBasicGradeInfo(moduleId: number, siteId?: string): Promise<CoreCourseModuleGradeInfo | undefined> {
const site = await CoreSites.instance.getSite(siteId);
if (!site || !site.isVersionGreaterEqualThan('3.2')) {
// On 3.1 won't get grading info and will return undefined. See check bellow.
return;
}
const info = await this.getModuleBasicInfo(moduleId, siteId); const info = await this.getModuleBasicInfo(moduleId, siteId);
const grade: CoreCourseModuleGradeInfo = { const grade: CoreCourseModuleGradeInfo = {
@ -539,10 +546,11 @@ export class CoreCourseProvider {
typeof grade.advancedgrading != 'undefined' || typeof grade.advancedgrading != 'undefined' ||
typeof grade.outcomes != 'undefined' typeof grade.outcomes != 'undefined'
) { ) {
// On 3.1 won't get grading info and will return undefined.
return grade; return grade;
} }
return false;
} }
/** /**
@ -1461,22 +1469,32 @@ export type CoreCourseModuleContentFile = {
}; };
/** /**
* Course module basic info type. * Course module basic info type. 3.2 onwards.
*/ */
export type CoreCourseModuleGradeInfo = { export type CoreCourseModuleGradeInfo = {
grade?: number; // Grade (max value or scale id). grade?: number; // Grade (max value or scale id).
scale?: string; // Scale items (if used). scale?: string; // Scale items (if used).
gradepass?: string; // Grade to pass (float). gradepass?: string; // Grade to pass (float).
gradecat?: number; // Grade category. gradecat?: number; // Grade category.
advancedgrading?: { // Advanced grading settings. advancedgrading?: CoreCourseModuleAdvancedGradingSetting[]; // Advanced grading settings.
area: string; // Gradable area name. outcomes?: CoreCourseModuleGradeOutcome[];
method: string; // Grading method. };
}[];
outcomes?: { // Outcomes information. /**
id: string; // Outcome id. * Advanced grading settings.
name: string; // Outcome full name. */
scale: string; // Scale items. export type CoreCourseModuleAdvancedGradingSetting = {
}[]; area: string; // Gradable area name.
method: string; // Grading method.
};
/**
* Grade outcome information.
*/
export type CoreCourseModuleGradeOutcome = {
id: string; // Outcome id.
name: string; // Outcome full name.
scale: string; // Scale items.
}; };
/** /**

View File

@ -31,6 +31,7 @@ import { CoreMenuItem, CoreUtils } from '@services/utils/utils';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
import { CoreError } from '@classes/errors/error';
/** /**
* Service that provides some features regarding grades information. * Service that provides some features regarding grades information.
@ -51,16 +52,18 @@ export class CoreGradesHelperProvider {
* @return Formatted row object. * @return Formatted row object.
*/ */
protected formatGradeRow(tableRow: CoreGradesTableRow): CoreGradesFormattedRow { protected formatGradeRow(tableRow: CoreGradesTableRow): CoreGradesFormattedRow {
const row = {}; const row: CoreGradesFormattedRow = {
rowclass: '',
};
for (const name in tableRow) { for (const name in tableRow) {
if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) { if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) {
let content = String(tableRow[name].content); let content = String(tableRow[name].content);
if (name == 'itemname') { if (name == 'itemname') {
this.setRowIcon(row, content); this.setRowIcon(row, content);
row['link'] = this.getModuleLink(content); row.link = this.getModuleLink(content);
row['rowclass'] += tableRow[name]!.class.indexOf('hidden') >= 0 ? ' hidden' : ''; row.rowclass += tableRow[name]!.class.indexOf('hidden') >= 0 ? ' hidden' : '';
row['rowclass'] += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : ''; row.rowclass += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
content = content.replace(/<\/span>/gi, '\n'); content = content.replace(/<\/span>/gi, '\n');
content = CoreTextUtils.instance.cleanTags(content); content = CoreTextUtils.instance.cleanTags(content);
@ -86,20 +89,20 @@ export class CoreGradesHelperProvider {
* @return Formatted row object. * @return Formatted row object.
*/ */
protected formatGradeRowForTable(tableRow: CoreGradesTableRow): CoreGradesFormattedRowForTable { protected formatGradeRowForTable(tableRow: CoreGradesTableRow): CoreGradesFormattedRowForTable {
const row = {}; const row: CoreGradesFormattedRowForTable = {};
for (let name in tableRow) { for (let name in tableRow) {
if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) { if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) {
let content = String(tableRow[name].content); let content = String(tableRow[name].content);
if (name == 'itemname') { if (name == 'itemname') {
row['id'] = parseInt(tableRow[name]!.id.split('_')[1], 10); row.id = parseInt(tableRow[name]!.id.split('_')[1], 10);
row['colspan'] = tableRow[name]!.colspan; row.colspan = tableRow[name]!.colspan;
row['rowspan'] = (tableRow['leader'] && tableRow['leader'].rowspan) || 1; row.rowspan = (tableRow.leader && tableRow.leader.rowspan) || 1;
this.setRowIcon(row, content); this.setRowIcon(row, content);
row['rowclass'] = tableRow[name]!.class.indexOf('leveleven') < 0 ? 'odd' : 'even'; row.rowclass = tableRow[name]!.class.indexOf('leveleven') < 0 ? 'odd' : 'even';
row['rowclass'] += tableRow[name]!.class.indexOf('hidden') >= 0 ? ' hidden' : ''; row.rowclass += tableRow[name]!.class.indexOf('hidden') >= 0 ? ' hidden' : '';
row['rowclass'] += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : ''; row.rowclass += tableRow[name]!.class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
content = content.replace(/<\/span>/gi, '\n'); content = content.replace(/<\/span>/gi, '\n');
content = CoreTextUtils.instance.cleanTags(content); content = CoreTextUtils.instance.cleanTags(content);
@ -202,14 +205,14 @@ export class CoreGradesHelperProvider {
*/ */
async getGradesCourseData(grades: CoreGradesGradeOverview[]): Promise<CoreGradesGradeOverviewWithCourseData[]> { async getGradesCourseData(grades: CoreGradesGradeOverview[]): Promise<CoreGradesGradeOverviewWithCourseData[]> {
// Obtain courses from cache to prevent network requests. // Obtain courses from cache to prevent network requests.
let coursesWereMissing; let coursesWereMissing = false;
try { try {
const courses = await CoreCourses.instance.getUserCourses(undefined, undefined, CoreSitesReadingStrategy.OnlyCache); const courses = await CoreCourses.instance.getUserCourses(undefined, undefined, CoreSitesReadingStrategy.OnlyCache);
const coursesMap = CoreUtils.instance.arrayToObject(courses, 'id'); const coursesMap = CoreUtils.instance.arrayToObject(courses, 'id');
coursesWereMissing = this.addCourseData(grades, coursesMap); coursesWereMissing = this.addCourseData(grades, coursesMap);
} catch (error) { } catch {
coursesWereMissing = true; coursesWereMissing = true;
} }
@ -278,7 +281,7 @@ export class CoreGradesHelperProvider {
const grades = await CoreGrades.instance.getCourseGradesTable(courseId, userId, siteId, ignoreCache); const grades = await CoreGrades.instance.getCourseGradesTable(courseId, userId, siteId, ignoreCache);
if (!grades) { if (!grades) {
throw new Error('Couldn\'t get grade item'); throw new CoreError('Couldn\'t get grade item');
} }
return this.getGradesTableRow(grades, gradeId); return this.getGradesTableRow(grades, gradeId);
@ -325,15 +328,15 @@ export class CoreGradesHelperProvider {
groupId?: number, groupId?: number,
siteId?: string, siteId?: string,
ignoreCache: boolean = false, ignoreCache: boolean = false,
): Promise<CoreGradesFormattedItem> { ): Promise<CoreGradesFormattedItem[] | CoreGradesFormattedRow[]> {
const grades = await CoreGrades.instance.getGradeItems(courseId, userId, groupId, siteId, ignoreCache); const grades = await CoreGrades.instance.getGradeItems(courseId, userId, groupId, siteId, ignoreCache);
if (!grades) { if (!grades) {
throw new Error('Couldn\'t get grade module items'); throw new CoreError('Couldn\'t get grade module items');
} }
if ('tabledata' in grades) { if ('tabledata' in grades) {
// Table format. // 3.1 Table format.
return this.getModuleGradesTableRows(grades, moduleId); return this.getModuleGradesTableRows(grades, moduleId);
} }
@ -347,18 +350,16 @@ export class CoreGradesHelperProvider {
* @param selectedGrade Selected grade label. * @param selectedGrade Selected grade label.
* @return Selected grade value. * @return Selected grade value.
*/ */
getGradeValueFromLabel(grades: CoreMenuItem[], selectedGrade: string): number { getGradeValueFromLabel(grades: CoreMenuItem[], selectedGrade?: string): number {
if (!grades || !selectedGrade) { if (!grades || !selectedGrade) {
return 0; return 0;
} }
for (const x in grades) { const grade = grades.find((grade) => grade.label == selectedGrade);
if (grades[x].label == selectedGrade) {
return grades[x].value < 0 ? 0 : grades[x].value;
}
}
return 0; return !grade || grade.value < 0
? 0
: grade.value;
} }
/** /**
@ -457,15 +458,15 @@ export class CoreGradesHelperProvider {
siteId = site.id; siteId = site.id;
currentUserId = site.getUserId(); currentUserId = site.getUserId();
if (moduleId) { if (!moduleId) {
// Try to open the module grade directly. Check if it's possible. throw new CoreError('Invalid moduleId');
const grades = await CoreGrades.instance.isGradeItemsAvalaible(siteId); }
if (!grades) { // Try to open the module grade directly. Check if it's possible.
throw new Error(); const grades = await CoreGrades.instance.isGradeItemsAvalaible(siteId);
}
} else { if (!grades) {
throw new Error(); throw new CoreError('No grades found.');
} }
try { try {
@ -476,7 +477,7 @@ export class CoreGradesHelperProvider {
const item = Array.isArray(items) && items.find((item) => moduleId == item.cmid); const item = Array.isArray(items) && items.find((item) => moduleId == item.cmid);
if (!item) { if (!item) {
throw new Error(); throw new CoreError('Grade item not found.');
} }
// Open the item directly. // Open the item directly.
@ -560,46 +561,49 @@ export class CoreGradesHelperProvider {
* @param text HTML where the image will be rendered. * @param text HTML where the image will be rendered.
* @return Row object with the image. * @return Row object with the image.
*/ */
protected setRowIcon(row: CoreGradesFormattedRowForTable, text: string): CoreGradesFormattedRowForTable { protected setRowIcon(
row: CoreGradesFormattedRowForTable | CoreGradesFormattedRow,
text: string,
): CoreGradesFormattedRowForTable {
text = text.replace('%2F', '/').replace('%2f', '/'); text = text.replace('%2F', '/').replace('%2f', '/');
if (text.indexOf('/agg_mean') > -1) { if (text.indexOf('/agg_mean') > -1) {
row['itemtype'] = 'agg_mean'; row.itemtype = 'agg_mean';
row['image'] = 'assets/img/grades/agg_mean.png'; row.image = 'assets/img/grades/agg_mean.png';
} else if (text.indexOf('/agg_sum') > -1) { } else if (text.indexOf('/agg_sum') > -1) {
row['itemtype'] = 'agg_sum'; row.itemtype = 'agg_sum';
row['image'] = 'assets/img/grades/agg_sum.png'; row.image = 'assets/img/grades/agg_sum.png';
} else if (text.indexOf('/outcomes') > -1 || text.indexOf('fa-tasks') > -1) { } else if (text.indexOf('/outcomes') > -1 || text.indexOf('fa-tasks') > -1) {
row['itemtype'] = 'outcome'; row.itemtype = 'outcome';
row['icon'] = 'fa-tasks'; row.icon = 'fas-chart-pie';
} else if (text.indexOf('i/folder') > -1 || text.indexOf('fa-folder') > -1) { } else if (text.indexOf('i/folder') > -1 || text.indexOf('fa-folder') > -1) {
row['itemtype'] = 'category'; row.itemtype = 'category';
row['icon'] = 'fa-folder'; row.icon = 'fas-cubes';
} else if (text.indexOf('/manual_item') > -1 || text.indexOf('fa-square-o') > -1) { } else if (text.indexOf('/manual_item') > -1 || text.indexOf('fa-square-o') > -1) {
row['itemtype'] = 'manual'; row.itemtype = 'manual';
row['icon'] = 'fa-square-o'; row.icon = 'far-square';
} else if (text.indexOf('/mod/') > -1) { } else if (text.indexOf('/mod/') > -1) {
const module = text.match(/mod\/([^/]*)\//); const module = text.match(/mod\/([^/]*)\//);
if (typeof module?.[1] != 'undefined') { if (typeof module?.[1] != 'undefined') {
row['itemtype'] = 'mod'; row.itemtype = 'mod';
row['itemmodule'] = module[1]; row.itemmodule = module[1];
row['image'] = CoreCourse.instance.getModuleIconSrc( row.image = CoreCourse.instance.getModuleIconSrc(
module[1], module[1],
CoreDomUtils.instance.convertToElement(text).querySelector('img')?.getAttribute('src') ?? undefined, CoreDomUtils.instance.convertToElement(text).querySelector('img')?.getAttribute('src') ?? undefined,
); );
} }
} else { } else {
if (row['rowspan'] && row['rowspan'] > 1) { if (row.rowspan && row.rowspan > 1) {
row['itemtype'] = 'category'; row.itemtype = 'category';
row['icon'] = 'fa-folder'; row.icon = 'fas-cubes';
} else if (text.indexOf('src=') > -1) { } else if (text.indexOf('src=') > -1) {
row['itemtype'] = 'unknown'; row.itemtype = 'unknown';
const src = text.match(/src="([^"]*)"/); const src = text.match(/src="([^"]*)"/);
row['image'] = src?.[1]; row.image = src?.[1];
} else if (text.indexOf('<i ') > -1) { } else if (text.indexOf('<i ') > -1) {
row['itemtype'] = 'unknown'; row.itemtype = 'unknown';
const src = text.match(/<i class="(?:[^"]*?\s)?(fa-[a-z0-9-]+)/); const src = text.match(/<i class="(?:[^"]*?\s)?(fa-[a-z0-9-]+)/);
row['icon'] = src ? src[1] : ''; row.icon = src ? src[1] : '';
} }
} }
@ -665,15 +669,53 @@ export class CoreGradesHelperProvider {
return Promise.resolve([]); return Promise.resolve([]);
} }
/**
* Type guard to check if the param is a CoreGradesGradeItem.
*
* @param item Param to check.
* @return Whether the param is a CoreGradesGradeItem.
*/
isGradeItem(item: CoreGradesGradeItem | CoreGradesFormattedRow): item is CoreGradesGradeItem {
return 'outcomeid' in item;
}
} }
export class CoreGradesHelper extends makeSingleton(CoreGradesHelperProvider) {} export class CoreGradesHelper extends makeSingleton(CoreGradesHelperProvider) {}
// @todo formatted data types. // @todo formatted data types.
export type CoreGradesFormattedRow = any;
export type CoreGradesFormattedRowForTable = any; export type CoreGradesFormattedRowForTable = any;
export type CoreGradesFormattedItem = any;
export type CoreGradesFormattedTableColumn = any; export type CoreGradesFormattedTableColumn = any;
export type CoreGradesFormattedItem = CoreGradesGradeItem & {
weight?: string; // Weight.
grade?: string; // The grade formatted.
range?: string; // Range formatted.
percentage?: string; // Percentage.
lettergrade?: string; // Letter grade.
average?: string; // Grade average.
};
export type CoreGradesFormattedRow = {
icon?: string;
link?: string | false;
rowclass?: string;
itemtype?: string;
image?: string;
itemmodule?: string;
rowspan?: number;
itemname?: string; // The item returned data.
weight?: string; // Weight column.
grade?: string; // Grade column.
range?: string;// Range column.
percentage?: string; // Percentage column.
lettergrade?: string; // Lettergrade column.
rank?: string; // Rank column.
average?: string; // Average column.
feedback?: string; // Feedback column.
contributiontocoursetotal?: string; // Contributiontocoursetotal column.
};
export type CoreGradesFormattedTableRow = CoreGradesFormattedTableRowFilled | CoreGradesFormattedTableRowEmpty; export type CoreGradesFormattedTableRow = CoreGradesFormattedTableRowFilled | CoreGradesFormattedTableRowEmpty;
export type CoreGradesFormattedTable = { export type CoreGradesFormattedTable = {
columns: CoreGradesFormattedTableColumn[]; columns: CoreGradesFormattedTableColumn[];

View File

@ -339,8 +339,10 @@ export class CoreGradesProvider {
* @return True if ws is avalaible, false otherwise. * @return True if ws is avalaible, false otherwise.
* @since Moodle 3.2 * @since Moodle 3.2
*/ */
isGradeItemsAvalaible(siteId?: string): Promise<boolean> { async isGradeItemsAvalaible(siteId?: string): Promise<boolean> {
return CoreSites.instance.getSite(siteId).then((site) => site.wsAvailable('gradereport_user_get_grade_items')); const site = await CoreSites.instance.getSite(siteId);
return site.wsAvailable('gradereport_user_get_grade_items');
} }
/** /**