MOBILE-3931 module: Add grades and course to module summary

main
Pau Ferrer Ocaña 2022-02-16 21:30:05 +01:00
parent 71788e83bf
commit 6a862730e2
8 changed files with 221 additions and 12 deletions

View File

@ -1741,6 +1741,7 @@
"core.grades.fail": "grades",
"core.grades.feedback": "grades",
"core.grades.grade": "grades",
"core.grades.gradebook": "grades",
"core.grades.gradeitem": "grades",
"core.grades.gradepass": "grades",
"core.grades.grades": "grades",

View File

@ -24,6 +24,17 @@
<ion-icon name="fas-external-link-alt" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="course" (click)="openCourse()" button [detail]="true">
<ion-label>
<p class="item-heading">{{ 'core.course' | translate}}</p>
<p>
<core-format-text [text]="course.displayname || course.fullname" contextLevel="course" [contextInstanceId]="courseId">
</core-format-text>
</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="module && description && displayOptions.displayDescription">
<ion-label>
<core-format-text [text]="description" [component]="component" [componentId]="componentId" contextLevel="module"
@ -32,7 +43,7 @@
</ion-label>
</ion-item>
<ion-item button class="ion-margin" *ngIf="prefetchText && displayOptions.displayPrefetch" class="ion-text-wrap">
<ion-item button *ngIf="prefetchText && displayOptions.displayPrefetch" class="ion-text-wrap">
<ion-label>
<p class="item-heading ion-text-wrap">{{ prefetchText }}</p>
<p *ngIf="downloadTimeReadable">{{ downloadTimeReadable }}</p>
@ -45,7 +56,7 @@
<ion-spinner *ngIf="prefetchStatusIcon == 'spinner'" slot="end" aria-hidden="true"></ion-spinner>
</ion-item>
<ion-item button class="ion-margin" *ngIf="sizeReadable && displayOptions.displaySize" class="ion-text-wrap">
<ion-item button *ngIf="sizeReadable && displayOptions.displaySize" class="ion-text-wrap">
<ion-label>
<p class="item-heading ion-text-wrap">{{ 'addon.storagemanager.totalspaceusage' | translate }}</p>
<ion-badge color="light">{{ sizeReadable | coreBytesToSize }}</ion-badge>
@ -57,7 +68,102 @@
<ion-spinner *ngIf="removeFilesLoading" slot="end" aria-hidden="true"></ion-spinner>
</ion-item>
<ion-button *ngIf="blog && displayOptions.displayBlog" class="ion-margin" (click)="gotoBlog()" expand="block" fill="outline">
<ion-card *ngIf="displayOptions.displayGrades && grades?.length > 0">
<ion-list>
<ion-item lines="full" class="ion-text-wrap">
<ion-label>
<h2>{{ 'core.grades.gradebook' | translate }}</h2>
</ion-label>
</ion-item>
<ng-container *ngFor="let grade of grades">
<ion-item button *ngIf="grade.gradeitem" class="ion-text-wrap divider" (click)="toggleGrade(grade)"
[attr.aria-label]="(grade.expanded ? 'core.collapse' : 'core.expand') | translate"
[attr.aria-expanded]="grade.expanded" [attr.aria-controls]="'grade-'+grade.id" role="heading" detail="false">
<ion-icon name="fas-chevron-right" flip-rtl slot="start" aria-hidden="true" class="expandable-status-icon"
[class.expandable-status-icon-expanded]="grade.expanded">
</ion-icon>
<ion-label>
<p class="item-heading" *ngIf="!grade.itemmodule">
<core-format-text [text]="grade.gradeitem" contextLevel="course" [contextInstanceId]="courseId">
</core-format-text>
</p>
<p class="item-heading" *ngIf="grade.itemmodule">
{{ 'core.grades.grade' | translate}}
</p>
<p *ngIf="grade.grade" [innerHTML]="grade.grade"></p>
</ion-label>
<ion-icon *ngIf="grade.icon" name="{{grade.icon}}" slot="end" [attr.aria-label]="grade.iconAlt">
</ion-icon>
<img *ngIf="grade.image && !grade.itemmodule" [src]="grade.image" slot="end" [alt]="grade.iconAlt" />
<ion-icon *ngIf="grade.image && grade.itemmodule" name="fas-chart-bar" slot="end" [attr.aria-label]="grade.iconAlt">
</ion-icon>
</ion-item>
<div *ngIf="grade.expanded" [id]="'grade-'+grade.id">
<ion-item class="ion-text-wrap" *ngIf="grade.weight?.length > 0 && grade.weight != '-'">
<ion-label>
<p class="item-heading">{{ 'core.grades.weight' | translate}}</p>
<p [innerHTML]="grade.weight"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="grade.range?.length > 0 && grade.range != '-'">
<ion-label>
<p class="item-heading">{{ 'core.grades.range' | translate}}</p>
<p [innerHTML]="grade.range"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="grade.percentage?.length > 0 && grade.percentage != '-'">
<ion-label>
<p class="item-heading">{{ 'core.grades.percentage' | translate}}</p>
<p [innerHTML]="grade.percentage"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="grade.lettergrade?.length > 0 && grade.lettergrade != '-'">
<ion-label>
<p class="item-heading">{{ 'core.grades.lettergrade' | translate}}</p>
<p [innerHTML]="grade.lettergrade"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="grade.rank?.length > 0 && grade.rank != '-'">
<ion-label>
<p class="item-heading">{{ 'core.grades.rank' | translate}}</p>
<p [innerHTML]="grade.rank"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="grade.average?.length > 0 && grade.average != '-'">
<ion-label>
<p class="item-heading">{{ 'core.grades.average' | translate}}</p>
<p [innerHTML]="grade.average"></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="grade.feedback?.length > 0 && grade.feedback != '-'">
<ion-label>
<p class="item-heading">{{ 'core.grades.feedback' | translate}}</p>
<p>
<core-format-text [maxHeight]="120" [text]="grade.feedback" contextLevel="course"
[contextInstanceId]="courseId">
</core-format-text>
</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap"
*ngIf="grade.contributiontocoursetotal?.length > 0 && grade.contributiontocoursetotal != '-'">
<ion-label>
<p class="item-heading">{{ 'core.grades.contributiontocoursetotal' | translate}}</p>
<p [innerHTML]="grade.contributiontocoursetotal"></p>
</ion-label>
</ion-item>
</div>
</ng-container>
</ion-list>
</ion-card>
<ion-button *ngIf="blog && displayOptions.displayBlog" (click)="gotoBlog()" expand="block" fill="outline">
<ion-icon name="far-newspaper" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
{{ 'addon.blog.blog' | translate }}

View File

@ -17,8 +17,12 @@ import { AddonBlog } from '@addons/blog/services/blog';
import { AddonBlogMainMenuHandlerService } from '@addons/blog/services/handlers/mainmenu';
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Params } from '@angular/router';
import { CoreCourse } from '@features/course/services/course';
import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses';
import { CoreGradesFormattedRow, CoreGradesFormattedTableRow, CoreGradesHelper } from '@features/grades/services/grades-helper';
import { CoreApp } from '@services/app';
import { CoreFilepool } from '@services/filepool';
import { CoreNavigator } from '@services/navigator';
@ -61,13 +65,12 @@ export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy {
sizeReadable?: string;
downloadTimeReadable?: string; // Last download time in a readable format.
size = 0;
grades?: CoreGradesFormattedRow[];
blog = false; // If blog is available.
isOnline = false; // If the app is online or not.
course?: CoreEnrolledCourseData;
protected onlineSubscription: Subscription; // It will observe the status of the network connection.
protected packageStatusObserver?: CoreEventObserver; // Observer of package status.
protected fileStatusObserver?: CoreEventObserver; // Observer of file status.
protected siteId: string;
@ -103,8 +106,15 @@ export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy {
displayPrefetch: true,
displaySize: true,
displayBlog: true,
displayGrades: true,
}, this.displayOptions);
this.displayOptions.displayGrades = this.displayOptions.displayGrades &&
CoreCourseModuleDelegate.supportsFeature(this.module.modname, CoreConstants.FEATURE_GRADE_HAS_GRADE, true);
this.displayOptions.displayDescription = this.displayOptions.displayDescription &&
CoreCourseModuleDelegate.supportsFeature(this.module.modname, CoreConstants.FEATURE_SHOW_DESCRIPTION, true);
this.fetchContent();
if (this.component) {
@ -164,7 +174,11 @@ export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy {
this.blog = await AddonBlog.isPluginEnabled();
await this.getPackageStatus();
await Promise.all([
this.getPackageStatus(),
this.fetchGrades(),
this.fetchCourse(),
]);
this.loaded = true;
}
@ -174,7 +188,7 @@ export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy {
*
* @param refresh If prefetch info has to be refreshed.
*/
async getPackageStatus(refresh = false): Promise<void> {
protected async getPackageStatus(refresh = false): Promise<void> {
if (!this.module) {
return;
}
@ -214,6 +228,50 @@ export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy {
await CoreNavigator.navigateToSitePath(AddonBlogMainMenuHandlerService.PAGE_NAME, { params });
}
/**
* Fetch grade module info.
*/
protected async fetchGrades(): Promise<void> {
if (!this.displayOptions.displayGrades) {
return;
}
this.grades = await CoreGradesHelper.getModuleGrades(this.courseId, this.moduleId);
}
/**
* Toggle grades expand.
*
* @param grade Row to expand.
*/
toggleGrade(grade: CoreGradesFormattedTableRow): void {
grade.expanded = !grade.expanded;
}
/**
* Fetch course.
*/
protected async fetchCourse(): Promise<void> {
this.course = await CoreCourses.getUserCourse(this.courseId, true);
}
/**
* Open course.
*/
openCourse(): void {
if (!this.course) {
return;
}
CoreCourse.openCourse(
this.course,
{
replace: true,
animationDirection: 'back',
},
);
}
/**
* Prefetch the module.
*/
@ -327,4 +385,5 @@ export type CoreCourseModuleSummaryDisplayOptions = {
displayPrefetch?: boolean;
displaySize?: boolean;
displayBlog?: boolean;
displayGrades?: boolean;
};

View File

@ -130,9 +130,13 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
navOptions.params = navOptions.params || {};
Object.assign(navOptions.params, { course: course });
// Don't return the .push promise, we don't want to display a loading modal during the page transition.
const currentTab = CoreNavigator.getCurrentMainMenuTab();
const routeDepth = CoreNavigator.getRouteDepth(`/main/${currentTab}/course/${course.id}`);
// When replace is true, disable route depth.
let routeDepth = 0;
if (!navOptions.replace) {
// Don't return the .push promise, we don't want to display a loading modal during the page transition.
const currentTab = CoreNavigator.getCurrentMainMenuTab();
routeDepth = CoreNavigator.getRouteDepth(`/main/${currentTab}/course/${course.id}`);
}
const deepPath = '/deep'.repeat(routeDepth);
CoreNavigator.navigateToSitePath(`course${deepPath}/${course.id}`, navOptions);

View File

@ -9,6 +9,7 @@
"fail": "Fail",
"feedback": "Feedback",
"grade": "Grade",
"gradebook": "Gradebook",
"gradeitem": "Grade item",
"gradepass": "Grade to pass",
"grades": "Grades",

View File

@ -457,6 +457,38 @@ export class CoreGradesHelperProvider {
}).map((row) => this.formatGradeRow(row)));
}
/**
* Get module grades to display.
*
* @param courseId Course Id.
* @param moduleId Module Id.
* @return Formatted table rows.
*/
async getModuleGrades(courseId: number, moduleId: number): Promise<CoreGradesFormattedTableRow[] > {
const table = await CoreGrades.getCourseGradesTable(courseId);
if (!table.tabledata) {
return [];
}
// Find href containing "/mod/xxx/xxx.php".
const regex = /href="([^"]*\/mod\/[^"|^/]*\/[^"|^.]*\.php[^"]*)/;
return await Promise.all(table.tabledata.filter((row) => {
if (row.itemname && row.itemname.content) {
const matches = row.itemname.content.match(regex);
if (matches && matches.length) {
const hrefParams = CoreUrlUtils.extractUrlParams(matches[1]);
return hrefParams && parseInt(hrefParams.id) === moduleId;
}
}
return false;
}).map((row) => this.formatGradeRowForTable(row)));
}
/**
* Go to view grades.
*

View File

@ -90,6 +90,9 @@ export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, C
displayRefresh = true;
displayPrefetch = true;
displaySize = true;
displayGrades = false;
// @TODO: // Currently display blogs is not an option since it may change soon adding new summary handlers.
displayBlog = false;
ptrEnabled = true;
isDestroyed = false;
@ -128,6 +131,7 @@ export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, C
this.displayRefresh = !CoreUtils.isFalseOrZero(handlerSchema.displayrefresh);
this.displayPrefetch = !CoreUtils.isFalseOrZero(handlerSchema.displayprefetch);
this.displaySize = !CoreUtils.isFalseOrZero(handlerSchema.displaysize);
this.displayGrades = CoreUtils.isTrueOrOne(handlerSchema.displaygrades); // False by default.
this.ptrEnabled = !CoreUtils.isFalseOrZero(handlerSchema.ptrenabled);
}
@ -196,7 +200,8 @@ export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, C
displayRefresh: this.displayRefresh,
displayPrefetch: this.displayPrefetch,
displaySize: this.displaySize,
displayBlog: false,
displayBlog: this.displayBlog,
displayGrades: this.displayGrades,
},
},
});

View File

@ -871,6 +871,7 @@ export type CoreSitePluginsCourseModuleHandlerData = CoreSitePluginsHandlerCommo
displayrefresh?: boolean;
displayprefetch?: boolean;
displaysize?: boolean;
displaygrades?: boolean;
coursepagemethod?: string;
ptrenabled?: boolean;
supportedfeatures?: Record<string, unknown>;