MOBILE-3636 grades: Add grade types
parent
a4225b8c02
commit
22395607ae
Binary file not shown.
After Width: | Height: | Size: 341 B |
Binary file not shown.
After Width: | Height: | Size: 318 B |
|
@ -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.
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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[];
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue