615 lines
24 KiB
TypeScript

// (C) Copyright 2015 Martin Dougiamas
//
// 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 { NavController } from 'ionic-angular';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreGradesProvider } from './grades';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
/**
* Service that provides some features regarding grades information.
*/
@Injectable()
export class CoreGradesHelperProvider {
protected logger;
constructor(logger: CoreLoggerProvider, private coursesProvider: CoreCoursesProvider,
private gradesProvider: CoreGradesProvider, private sitesProvider: CoreSitesProvider,
private textUtils: CoreTextUtilsProvider, private courseProvider: CoreCourseProvider,
private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider, private utils: CoreUtilsProvider,
private linkHelper: CoreContentLinksHelperProvider, private courseHelper: CoreCourseHelperProvider) {
this.logger = logger.getInstance('CoreGradesHelperProvider');
}
/**
* Formats a row from the grades table te be rendered in a page.
*
* @param {any} tableRow JSON object representing row of grades table data.
* @return {any} Formatted row object.
*/
protected formatGradeRow(tableRow: any): any {
const row = {};
for (const name in tableRow) {
if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) {
let content = String(tableRow[name].content);
if (name == 'itemname') {
this.setRowIcon(row, content);
row['link'] = this.getModuleLink(content);
row['rowclass'] += tableRow[name].class.indexOf('hidden') >= 0 ? ' hidden' : '';
row['rowclass'] += tableRow[name].class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
content = content.replace(/<\/span>/gi, '\n');
content = this.textUtils.cleanTags(content);
} else {
content = this.textUtils.replaceNewLines(content, '<br>');
}
if (content == '&nbsp;') {
content = '';
}
row[name] = content.trim();
}
}
return row;
}
/**
* Formats a row from the grades table to be rendered in one table.
*
* @param {any} tableRow JSON object representing row of grades table data.
* @return {any} Formatted row object.
*/
protected formatGradeRowForTable(tableRow: any): any {
const row = {};
for (let name in tableRow) {
if (typeof tableRow[name].content != 'undefined' && tableRow[name].content !== null) {
let content = String(tableRow[name].content);
if (name == 'itemname') {
row['id'] = parseInt(tableRow[name].id.split('_')[1], 10);
row['colspan'] = tableRow[name].colspan;
row['rowspan'] = (tableRow['leader'] && tableRow['leader'].rowspan) || 1;
this.setRowIcon(row, content);
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('dimmed_text') >= 0 ? ' dimmed_text' : '';
content = content.replace(/<\/span>/gi, '\n');
content = this.textUtils.cleanTags(content);
name = 'gradeitem';
} else {
content = this.textUtils.replaceNewLines(content, '<br>');
}
if (content == '&nbsp;') {
content = '';
}
row[name] = content.trim();
}
}
return row;
}
/**
* Removes suffix formatted to compatibilize data from table and items.
*
* @param {any} item Grade item to format.
* @return {any} Grade item formatted.
*/
protected formatGradeItem(item: any): any {
for (const name in item) {
const index = name.indexOf('formatted');
if (index > 0) {
item[name.substr(0, index)] = item[name];
}
}
return item;
}
/**
* Formats the response of gradereport_user_get_grades_table to be rendered.
*
* @param {any} table JSON object representing a table with data.
* @return {any} Formatted HTML table.
*/
formatGradesTable(table: any): any {
const maxDepth = table.maxdepth,
formatted = {
columns: [],
rows: []
},
// Columns, in order.
columns = {
gradeitem: true,
weight: false,
grade: false,
range: false,
percentage: false,
lettergrade: false,
rank: false,
average: false,
feedback: false,
contributiontocoursetotal: false
};
formatted.rows = table.tabledata.map((row: any) => {
return this.formatGradeRowForTable(row);
});
// Get a row with some info.
let normalRow = formatted.rows.find((e) => {
return e.itemtype != 'leader' && (typeof e.grade != 'undefined' || typeof e.percentage != 'undefined');
});
// Decide if grades or percentage is being shown on phones.
if (normalRow && typeof normalRow.grade != 'undefined') {
columns.grade = true;
} else if (normalRow && typeof normalRow.percentage != 'undefined') {
columns.percentage = true;
} else {
normalRow = formatted.rows.find((e) => {
return e.itemtype != 'leader';
});
columns.grade = true;
}
for (const colName in columns) {
if (typeof normalRow[colName] != 'undefined') {
formatted.columns.push({
name: colName,
colspan: colName == 'gradeitem' ? maxDepth : 1,
hiddenPhone: !columns[colName]
});
}
}
return formatted;
}
/**
* Get course data for grades since they only have courseid.
*
* @param {any} grades Grades to get the data for.
* @return {Promise<any>} Promise always resolved. Resolve param is the formatted grades.
*/
getGradesCourseData(grades: any): Promise<any> {
// Using cache for performance reasons.
return this.coursesProvider.getUserCourses(true).then((courses) => {
const indexedCourses = {};
courses.forEach((course) => {
indexedCourses[course.id] = course;
});
grades.forEach((grade) => {
if (typeof indexedCourses[grade.courseid] != 'undefined') {
grade.courseFullName = indexedCourses[grade.courseid].fullname;
}
});
return grades;
});
}
/**
* Get an specific grade item.
*
* @param {number} courseId ID of the course to get the grades from.
* @param {number} gradeId Grade ID.
* @param {number} [userId] ID of the user to get the grades from. If not defined use site's current user.
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down).
* @return {Promise<any>} Promise to be resolved when the grades are retrieved.
*/
getGradeItem(courseId: number, gradeId: number, userId?: number, siteId?: string, ignoreCache: boolean = false): Promise<any> {
return this.gradesProvider.getCourseGradesTable(courseId, userId, siteId, ignoreCache).then((grades) => {
if (grades) {
return this.getGradesTableRow(grades, gradeId);
}
return Promise.reject(null);
});
}
/**
* Returns the label of the selected grade.
*
* @param {any[]} grades Array with objects with value and label.
* @param {number} selectedGrade Selected grade value.
* @return {string} Selected grade label.
*/
getGradeLabelFromValue(grades: any[], selectedGrade: number): string {
selectedGrade = Number(selectedGrade);
if (!grades || !selectedGrade || selectedGrade <= 0) {
return '';
}
for (const x in grades) {
if (grades[x].value == selectedGrade) {
return grades[x].label;
}
}
return '';
}
/**
* Get the grade items for a certain module. Keep in mind that may have more than one item to include outcomes and scales.
*
* @param {number} courseId ID of the course to get the grades from.
* @param {number} moduleId Module ID.
* @param {number} [userId] ID of the user to get the grades from. If not defined use site's current user.
* @param {number} [groupId] ID of the group to get the grades from. Not used for old gradebook table.
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down).
* @return {Promise<any>} Promise to be resolved when the grades are retrieved.
*/
getGradeModuleItems(courseId: number, moduleId: number, userId?: number, groupId?: number, siteId?: string,
ignoreCache: boolean = false): Promise<any> {
return this.gradesProvider.getGradeItems(courseId, userId, groupId, siteId, ignoreCache).then((grades) => {
if (grades) {
if (typeof grades.tabledata != 'undefined') {
// Table format.
return this.getModuleGradesTableRows(grades, moduleId);
} else {
return grades.filter((item) => {
return item.cmid == moduleId;
}).map((item) => {
return this.formatGradeItem(item);
});
}
}
return Promise.reject(null);
});
}
/**
* Returns the value of the selected grade.
*
* @param {any[]} grades Array with objects with value and label.
* @param {string} selectedGrade Selected grade label.
* @return {number} Selected grade value.
*/
getGradeValueFromLabel(grades: any[], selectedGrade: string): number {
if (!grades || !selectedGrade) {
return 0;
}
for (const x in grades) {
if (grades[x].label == selectedGrade) {
return grades[x].value < 0 ? 0 : grades[x].value;
}
}
return 0;
}
/**
* Gets the link to the module for the selected grade.
*
* @param {string} text HTML where the link is present.
* @return {string | false} URL linking to the module.
*/
protected getModuleLink(text: string): string | false {
const el = this.domUtils.toDom(text)[0],
link = el.attributes['href'] ? el.attributes['href'].value : false;
if (!link || link.indexOf('/mod/') < 0) {
return false;
}
return link;
}
/**
* Get a row from the grades table.
*
* @param {any} table JSON object representing a table with data.
* @param {number} gradeId Grade Object identifier.
* @return {any} Formatted HTML table.
*/
getGradesTableRow(table: any, gradeId: number): any {
if (table.tabledata) {
const selectedRow = table.tabledata.find((row) => {
return row.itemname && row.itemname.id && row.itemname.id.substr(0, 3) == 'row' &&
parseInt(row.itemname.id.split('_')[1], 10) == gradeId;
});
if (selectedRow) {
return this.formatGradeRow(selectedRow);
}
}
return '';
}
/**
* Get the rows related to a module from the grades table.
*
* @param {any} table JSON object representing a table with data.
* @param {number} moduleId Grade Object identifier.
* @return {any} Formatted HTML table.
*/
getModuleGradesTableRows(table: any, moduleId: number): any {
if (table.tabledata) {
// Find href containing "/mod/xxx/xxx.php".
const regex = /href="([^"]*\/mod\/[^"|^\/]*\/[^"|^\.]*\.php[^"]*)/;
return table.tabledata.filter((row) => {
if (row.itemname && row.itemname.content) {
const matches = row.itemname.content.match(regex);
if (matches && matches.length) {
const hrefParams = this.urlUtils.extractUrlParams(matches[1]);
return hrefParams && hrefParams.id == moduleId;
}
}
return false;
}).map((row) => {
return this.formatGradeRow(row);
});
}
return [];
}
/**
* Go to view grades.
*
* @param {number} courseId Course ID t oview.
* @param {number} [userId] User to view. If not defined, current user.
* @param {number} [moduleId] Module to view. If not defined, view all course grades.
* @param {NavController} [navCtrl] NavController to use.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when done.
*/
goToGrades(courseId: number, userId?: number, moduleId?: number, navCtrl?: NavController, siteId?: string): Promise<any> {
const modal = this.domUtils.showModalLoading();
let currentUserId;
return this.sitesProvider.getSite(siteId).then((site) => {
siteId = site.id;
currentUserId = site.getUserId();
if (moduleId) {
// Try to open the module grade directly. Check if it's possible.
return this.gradesProvider.isGradeItemsAvalaible(siteId).then((getGrades) => {
if (!getGrades) {
return Promise.reject(null);
}
});
} else {
return Promise.reject(null);
}
}).then(() => {
// Can get grades. Do it.
return this.gradesProvider.getGradeItems(courseId, userId, undefined, siteId).then((items) => {
// Find the item of the module.
const item = items.find((item) => {
return moduleId == item.cmid;
});
if (item) {
// Open the item directly.
const pageParams: any = {
courseId: courseId,
userId: userId,
gradeId: item.id
};
return this.linkHelper.goInSite(navCtrl, 'CoreGradesGradePage', pageParams, siteId).catch(() => {
// Ignore errors.
});
}
return Promise.reject(null);
});
}).catch(() => {
// Cannot get grade items or there's no need to.
if (userId && userId != currentUserId) {
// View another user grades. Open the grades page directly.
const pageParams = {
course: {id: courseId},
userId: userId
};
return this.linkHelper.goInSite(navCtrl, 'CoreGradesCoursePage', pageParams, siteId).catch(() => {
// Ignore errors.
});
}
// View own grades. Check if we already are in the course index page.
if (this.courseProvider.currentViewIsCourse(navCtrl, courseId)) {
// Current view is this course, just select the grades tab.
this.courseProvider.selectCourseTab('CoreGrades');
return;
}
// Open the course with the grades tab selected.
return this.courseHelper.getCourse(courseId, siteId).then((result) => {
const pageParams: any = {
course: result.course,
selectedTab: 'CoreGrades'
};
return this.linkHelper.goInSite(navCtrl, 'CoreCourseSectionPage', pageParams, siteId).catch(() => {
// Ignore errors.
});
});
}).catch(() => {
// Cannot get course for some reason, just open the grades page.
return this.linkHelper.goInSite(navCtrl, 'CoreGradesCoursePage', {course: {id: courseId}}, siteId);
}).finally(() => {
modal.dismiss();
});
}
/**
* Invalidate the grade items for a certain module.
*
* @param {number} courseId ID of the course to invalidate the grades.
* @param {number} [userId] ID of the user to invalidate. If not defined use site's current user.
* @param {number} [groupId] ID of the group to invalidate. Not used for old gradebook table.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise} Promise to be resolved when the grades are invalidated.
*/
invalidateGradeModuleItems(courseId: number, userId?: number, groupId?: number, siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
return this.sitesProvider.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
return this.gradesProvider.isGradeItemsAvalaible(siteId).then((enabled) => {
if (enabled) {
return this.gradesProvider.invalidateCourseGradesItemsData(courseId, userId, groupId, siteId);
} else {
return this.gradesProvider.invalidateCourseGradesData(courseId, userId, siteId);
}
});
});
}
/**
* Parses the image and sets it to the row.
*
* @param {any} row Formatted grade row object.
* @param {string} text HTML where the image will be rendered.
* @return {any} Row object with the image.
*/
protected setRowIcon(row: any, text: string): any {
text = text.replace('%2F', '/').replace('%2f', '/');
if (text.indexOf('/agg_mean') > -1) {
row['itemtype'] = 'agg_mean';
row['image'] = 'assets/img/grades/agg_mean.png';
} else if (text.indexOf('/agg_sum') > -1) {
row['itemtype'] = 'agg_sum';
row['image'] = 'assets/img/grades/agg_sum.png';
} else if (text.indexOf('/outcomes') > -1 || text.indexOf('fa-tasks') > -1) {
row['itemtype'] = 'outcome';
row['icon'] = 'fa-tasks';
} else if (text.indexOf('i/folder') > -1 || text.indexOf('fa-folder') > -1) {
row['itemtype'] = 'category';
row['icon'] = 'fa-folder';
} else if (text.indexOf('/manual_item') > -1 || text.indexOf('fa-square-o') > -1) {
row['itemtype'] = 'manual';
row['icon'] = 'fa-square-o';
} else if (text.indexOf('/mod/') > -1) {
const module = text.match(/mod\/([^\/]*)\//);
if (typeof module[1] != 'undefined') {
row['itemtype'] = 'mod';
row['itemmodule'] = module[1];
row['image'] = this.courseProvider.getModuleIconSrc(module[1],
this.domUtils.convertToElement(text).querySelector('img').getAttribute('src'));
}
} else {
if (row['rowspan'] && row['rowspan'] > 1) {
row['itemtype'] = 'category';
row['icon'] = 'fa-folder';
} else if (text.indexOf('src=') > -1) {
row['itemtype'] = 'unknown';
const src = text.match(/src="([^"]*)"/);
row['image'] = src[1];
} else if (text.indexOf('<i ') > -1) {
row['itemtype'] = 'unknown';
const src = text.match(/<i class="(?:[^"]*?\s)?(fa-[a-z0-9-]+)/);
row['icon'] = src ? src[1] : '';
}
}
return row;
}
/**
* Creates an array that represents all the current grades that can be chosen using the given grading type.
* Negative numbers are scales, zero is no grade, and positive numbers are maximum grades.
*
* Taken from make_grades_menu on moodlelib.php
*
* @param {number} gradingType If positive, max grade you can provide. If negative, scale Id.
* @param {number} [moduleId] Module ID. Used to retrieve the scale items when they are not passed as parameter.
* If the user does not have permision to manage the activity an empty list is returned.
* @param {string} [defaultLabel] Element that will become default option, if not defined, it won't be added.
* @param {any} [defaultValue] Element that will become default option value. Default ''.
* @param {string} [scale] Scale csv list String. If not provided, it will take it from the module grade info.
* @return {Promise<any[]>} Array with objects with value and label to create a propper HTML select.
*/
makeGradesMenu(gradingType: number, moduleId?: number, defaultLabel: string = '', defaultValue: any = '', scale?: string):
Promise<any[]> {
if (gradingType < 0) {
if (scale) {
return Promise.resolve(this.utils.makeMenuFromList(scale, defaultLabel, undefined, defaultValue));
} else if (moduleId) {
return this.courseProvider.getModuleBasicGradeInfo(moduleId).then((gradeInfo) => {
if (gradeInfo.scale) {
return this.utils.makeMenuFromList(gradeInfo.scale, defaultLabel, undefined, defaultValue);
}
return [];
});
} else {
return Promise.resolve([]);
}
}
if (gradingType > 0) {
const grades = [];
if (defaultLabel) {
// Key as string to avoid resorting of the object.
grades.push({
label: defaultLabel,
value: defaultValue
});
}
for (let i = gradingType; i >= 0; i--) {
grades.push({
label: i + ' / ' + gradingType,
value: i
});
}
return Promise.resolve(grades);
}
return Promise.resolve([]);
}
}