MOBILE-3661 grades: Migrate grades tab
parent
6d148229f1
commit
cd6e93b9d1
|
@ -18,9 +18,9 @@ import { CoreScreen } from '@services/screen';
|
|||
import { Subscription } from 'rxjs';
|
||||
|
||||
enum CoreSplitViewMode {
|
||||
Default = '', // Shows both menu and content.
|
||||
MenuOnly = 'menu-only', // Hides content.
|
||||
ContentOnly = 'content-only', // Hides menu.
|
||||
MenuAndContent = 'menu-and-content', // Shows both menu and content.
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
@ -32,8 +32,8 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
|
|||
|
||||
@ViewChild(IonRouterOutlet) outlet!: IonRouterOutlet;
|
||||
@HostBinding('class') classes = '';
|
||||
isNested = false;
|
||||
|
||||
private isNestedSplitView = false;
|
||||
private subscriptions?: Subscription[];
|
||||
|
||||
constructor(private element: ElementRef<HTMLElement>) {}
|
||||
|
@ -42,7 +42,7 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
|
|||
* @inheritdoc
|
||||
*/
|
||||
ngAfterViewInit(): void {
|
||||
this.isNestedSplitView = !!this.element.nativeElement.parentElement?.closest('core-split-view');
|
||||
this.isNested = !!this.element.nativeElement.parentElement?.closest('core-split-view');
|
||||
this.subscriptions = [
|
||||
this.outlet.activateEvents.subscribe(() => this.updateClasses()),
|
||||
this.outlet.deactivateEvents.subscribe(() => this.updateClasses()),
|
||||
|
@ -63,7 +63,13 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
|
|||
* Update host classes.
|
||||
*/
|
||||
private updateClasses(): void {
|
||||
this.classes = this.getCurrentMode();
|
||||
const classes: string[] = [this.getCurrentMode()];
|
||||
|
||||
if (this.isNested) {
|
||||
classes.push('nested');
|
||||
}
|
||||
|
||||
this.classes = classes.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -73,7 +79,7 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
|
|||
* @return Split view mode.
|
||||
*/
|
||||
private getCurrentMode(): CoreSplitViewMode {
|
||||
if (this.isNestedSplitView) {
|
||||
if (this.isNested) {
|
||||
return CoreSplitViewMode.MenuOnly;
|
||||
}
|
||||
|
||||
|
@ -83,7 +89,7 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
|
|||
: CoreSplitViewMode.MenuOnly;
|
||||
}
|
||||
|
||||
return CoreSplitViewMode.Default;
|
||||
return CoreSplitViewMode.MenuAndContent;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -140,8 +140,9 @@ export class CoreCourseProvider {
|
|||
* @param courseId Course ID.
|
||||
* @return Whether the current view is a certain course.
|
||||
*/
|
||||
currentViewIsCourse(): boolean {
|
||||
// @ todo add params and logic.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
currentViewIsCourse(navCtrl: any, courseId: number): boolean {
|
||||
// @todo implement
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import { CoreCourseModule } from './course/course.module';
|
|||
import { CoreCoursesModule } from './courses/courses.module';
|
||||
import { CoreEmulatorModule } from './emulator/emulator.module';
|
||||
import { CoreFileUploaderModule } from './fileuploader/fileuploader.module';
|
||||
import { CoreGradesModule } from './grades/grades.module';
|
||||
import { CoreH5PModule } from './h5p/h5p.module';
|
||||
import { CoreLoginModule } from './login/login.module';
|
||||
import { CoreMainMenuModule } from './mainmenu/mainmenu.module';
|
||||
|
@ -35,6 +36,7 @@ import { CoreViewerModule } from './viewer/viewer.module';
|
|||
CoreCoursesModule,
|
||||
CoreEmulatorModule,
|
||||
CoreFileUploaderModule,
|
||||
CoreGradesModule,
|
||||
CoreLoginModule,
|
||||
CoreMainMenuModule,
|
||||
CoreSettingsModule,
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
// (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 { CommonModule } from '@angular/common';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreScreen } from '@services/screen';
|
||||
|
||||
import { CoreGradesCoursePage } from './pages/course/course';
|
||||
import { CoreGradesCoursesPage } from './pages/courses/courses';
|
||||
import { CoreGradesGradePage } from './pages/grade/grade';
|
||||
import { conditionalRoutes } from '@/app/app-routing.module';
|
||||
|
||||
const mobileRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CoreGradesCoursesPage,
|
||||
},
|
||||
{
|
||||
path: ':courseId',
|
||||
component: CoreGradesCoursePage,
|
||||
},
|
||||
{
|
||||
path: ':courseId/:gradeId',
|
||||
component: CoreGradesGradePage,
|
||||
},
|
||||
];
|
||||
|
||||
const tabletRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CoreGradesCoursesPage,
|
||||
children: [
|
||||
{
|
||||
path: ':courseId',
|
||||
component: CoreGradesCoursePage,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: ':courseId',
|
||||
component: CoreGradesCoursePage,
|
||||
children: [
|
||||
{
|
||||
path: ':gradeId',
|
||||
component: CoreGradesGradePage,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const routes: Routes = [
|
||||
...conditionalRoutes(mobileRoutes, () => CoreScreen.instance.isMobile),
|
||||
...conditionalRoutes(tabletRoutes, () => CoreScreen.instance.isTablet),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreSharedModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreGradesCoursesPage,
|
||||
CoreGradesCoursePage,
|
||||
CoreGradesGradePage,
|
||||
],
|
||||
})
|
||||
export class CoreGradesLazyModule {}
|
|
@ -0,0 +1,42 @@
|
|||
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||
import { Routes } from '@angular/router';
|
||||
import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module';
|
||||
import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate';
|
||||
import CoreGradesMainMenuHandler, { CoreGradesMainMenuHandlerService } from './services/handlers/mainmenu';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: CoreGradesMainMenuHandlerService.PAGE_NAME,
|
||||
loadChildren: () => import('@features/grades/grades-lazy.module').then(m => m.CoreGradesLazyModule),
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [CoreMainMenuRoutingModule.forChild({ children: routes })],
|
||||
exports: [CoreMainMenuRoutingModule],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
deps: [],
|
||||
useValue: () => {
|
||||
CoreMainMenuDelegate.instance.registerHandler(CoreGradesMainMenuHandler.instance);
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
export class CoreGradesModule {}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"average": "Average",
|
||||
"badgrade": "Supplied grade is invalid",
|
||||
"contributiontocoursetotal": "Contribution to course total",
|
||||
"feedback": "Feedback",
|
||||
"grade": "Grade",
|
||||
"gradeitem": "Grade item",
|
||||
"grades": "Grades",
|
||||
"lettergrade": "Letter grade",
|
||||
"nogradesreturned": "No grades returned",
|
||||
"nooutcome": "No outcome",
|
||||
"percentage": "Percentage",
|
||||
"range": "Range",
|
||||
"rank": "Rank",
|
||||
"weight": "Weight"
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'core.grades.grades' | translate }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<core-split-view>
|
||||
<ion-refresher slot="fixed" [disabled]="!gradesTableLoaded" (ionRefresh)="refreshGradesTable($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="gradesTableLoaded" class="safe-area-page">
|
||||
<core-empty-box *ngIf="!gradesTable" icon="stats" [message]="'core.grades.nogradesreturned' | translate">
|
||||
</core-empty-box>
|
||||
<div *ngIf="gradesTable" class="core-grades-container">
|
||||
<table cellspacing="0" cellpadding="0" class="core-grades-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
*ngFor="let column of gradesTable.columns"
|
||||
id="{{column.name}}"
|
||||
class="ion-text-start"
|
||||
[class.ion-hide-md-down]="column.hiddenPhone"
|
||||
[attr.colspan]="column.colspan"
|
||||
>
|
||||
{{ 'core.grades.' + column.name | translate }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
*ngFor="let row of gradesTable.rows"
|
||||
(click)="row.itemtype != 'category' && gotoGrade(row.id)"
|
||||
[class]="row.rowclass"
|
||||
[ngClass]='{"core-grades-grade-clickable": row.itemtype != "category"}'
|
||||
>
|
||||
<ng-container *ngIf="row.itemtype">
|
||||
<td
|
||||
*ngIf="row.itemtype == 'category'"
|
||||
class="core-grades-table-category"
|
||||
[attr.rowspan]="row.rowspan"
|
||||
></td>
|
||||
<th
|
||||
class="core-grades-table-gradeitem ion-text-start"
|
||||
[class.column-itemname]="row.itemtype == 'category'"
|
||||
[class.core-selected-item]="activeGradeId == row.id"
|
||||
[attr.colspan]="row.colspan"
|
||||
>
|
||||
<ion-icon *ngIf="row.icon" name="{{row.icon}}" slot="start"></ion-icon>
|
||||
<img *ngIf="row.image" [src]="row.image" slot="start" />
|
||||
<span [innerHTML]="row.gradeitem"></span>
|
||||
</th>
|
||||
<ng-container *ngFor="let column of gradesTable.columns">
|
||||
<td
|
||||
*ngIf="column.name != 'gradeitem' && row[column.name] != undefined"
|
||||
[class]="'ion-text-start core-grades-table-' + column.name"
|
||||
[class.ion-hide-md-down]="column.hiddenPhone"
|
||||
[innerHTML]="row[column.name]"
|
||||
></td>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</core-loading>
|
||||
</core-split-view>
|
||||
</ion-content>
|
|
@ -0,0 +1,133 @@
|
|||
@import "~theme/breakpoints";
|
||||
|
||||
// @todo darkmode
|
||||
// @todo RTL layout
|
||||
|
||||
:host-context(ion-app.md) {
|
||||
--border-color: var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, .13))));
|
||||
}
|
||||
|
||||
:host-context(ion-app.ios) {
|
||||
--border-color: var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-250, #c8c7cc)));
|
||||
}
|
||||
|
||||
.core-grades-table {
|
||||
border-collapse: collapse;
|
||||
line-height: 20px;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
color: var(--ion-text-color);
|
||||
|
||||
// @include darkmode() {
|
||||
// color: $core-dark-text-color;
|
||||
// }
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 10px 0 10px 10px;
|
||||
vertical-align: top;
|
||||
white-space: normal;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
thead th {
|
||||
vertical-align: bottom;
|
||||
font-weight: bold;
|
||||
background-color: var(--white);
|
||||
|
||||
// @include darkmode() {
|
||||
// background-color: $black;
|
||||
// }
|
||||
}
|
||||
|
||||
tbody th {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#gradeitem {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.core-grades-table-gradeitem {
|
||||
padding-left: 5px;
|
||||
font-weight: bold;
|
||||
|
||||
&.column-itemname {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.core-grades-table-feedback {
|
||||
padding-left: 5px;
|
||||
|
||||
.no-overflow {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.dimmed_text,
|
||||
.hidden {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.odd {
|
||||
|
||||
td, th, th.core-selected-item {
|
||||
background-color: var(--gray-lighter);
|
||||
|
||||
// @include darkmode() {
|
||||
// background-color: $gray-darker;
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.even {
|
||||
|
||||
td, th, th.core-selected-item {
|
||||
background-color: var(--white);
|
||||
|
||||
// @include darkmode() {
|
||||
// background-color: $black;
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.core-grades-grade-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
core-split-view.nested .core-grades-table .ion-hide-md-down,
|
||||
core-split-view.menu-and-content .core-grades-table .ion-hide-md-down {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-md) {
|
||||
|
||||
.core-grades-table td {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
// (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 { ActivatedRoute } from '@angular/router';
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreGrades } from '@features/grades/services/grades';
|
||||
import { CoreGradesFormattedTable, CoreGradesHelper } from '@features/grades/services/grades-helper';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreScreen } from '@services/screen';
|
||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||
import { CoreObject } from '@singletons/object';
|
||||
|
||||
/**
|
||||
* Page that displays a course grades.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-grades-course',
|
||||
templateUrl: 'course.html',
|
||||
styleUrls: ['course.scss'],
|
||||
})
|
||||
export class CoreGradesCoursePage implements OnInit, OnDestroy {
|
||||
|
||||
courseId: number;
|
||||
userId: number;
|
||||
gradesTable?: CoreGradesFormattedTable;
|
||||
gradesTableLoaded = false;
|
||||
activeGradeId?: number;
|
||||
layoutSubscription?: Subscription;
|
||||
|
||||
@ViewChild(CoreSplitViewComponent) splitView?: CoreSplitViewComponent;
|
||||
|
||||
constructor(private route: ActivatedRoute) {
|
||||
this.courseId = route.snapshot.params.courseId;
|
||||
this.userId = route.snapshot.queryParams.userId ?? CoreSites.instance.getCurrentSiteUserId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.layoutSubscription = CoreScreen.instance.layoutObservable.subscribe(() => this.updateActiveGrade());
|
||||
|
||||
await this.fetchGradesTable();
|
||||
|
||||
// Add log in Moodle.
|
||||
await CoreUtils.instance.ignoreErrors(CoreGrades.instance.logCourseGradesView(this.courseId, this.userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ionViewWillEnter(): void {
|
||||
this.updateActiveGrade();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.layoutSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the data required for the view.
|
||||
*/
|
||||
async fetchGradesTable(): Promise<void> {
|
||||
try {
|
||||
const table = await CoreGrades.instance.getCourseGradesTable(this.courseId, this.userId);
|
||||
|
||||
this.gradesTable = CoreGradesHelper.instance.formatGradesTable(table);
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading grades');
|
||||
|
||||
this.gradesTable = { rows: [], columns: [] };
|
||||
} finally {
|
||||
this.gradesTableLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh data.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
*/
|
||||
async refreshGradesTable(refresher: IonRefresher): Promise<void> {
|
||||
await CoreUtils.instance.ignoreErrors(CoreGrades.instance.invalidateCourseGradesData(this.courseId, this.userId));
|
||||
await CoreUtils.instance.ignoreErrors(this.fetchGradesTable());
|
||||
|
||||
refresher.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the grade of the selected item.
|
||||
*
|
||||
* @param gradeId Grade item ID where to navigate.
|
||||
*/
|
||||
async gotoGrade(gradeId: number): Promise<void> {
|
||||
const path = this.activeGradeId ? `../${gradeId}` : gradeId.toString();
|
||||
|
||||
await CoreNavigator.instance.navigate(path, {
|
||||
params: CoreObject.withoutEmpty({ userId: this.userId }),
|
||||
});
|
||||
|
||||
this.updateActiveGrade(gradeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update active grade.
|
||||
*
|
||||
* @param activeGradeId Active grade id.
|
||||
*/
|
||||
private updateActiveGrade(activeGradeId?: number): void {
|
||||
if (CoreScreen.instance.isMobile || this.splitView?.isNested) {
|
||||
delete this.activeGradeId;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeGradeId = activeGradeId ?? this.guessActiveGrade();
|
||||
}
|
||||
|
||||
/**
|
||||
* Guess active grade looking at the current route.
|
||||
*
|
||||
* @return Active grade id.
|
||||
*/
|
||||
private guessActiveGrade(): number | undefined {
|
||||
const gradeId = parseInt(this.route.snapshot?.firstChild?.params.gradeId);
|
||||
|
||||
return isNaN(gradeId) ? undefined : gradeId;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'core.grades.grades' | translate }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<core-split-view>
|
||||
<ion-refresher slot="fixed" [disabled]="!gradesLoaded" (ionRefresh)="refreshGrades($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="gradesLoaded">
|
||||
<core-empty-box
|
||||
*ngIf="grades && grades.length == 0"
|
||||
icon="stats"
|
||||
[message]="'core.grades.nogradesreturned' | translate"
|
||||
></core-empty-box>
|
||||
|
||||
<ion-list *ngIf="grades && grades.length > 0">
|
||||
<ion-item
|
||||
*ngFor="let grade of grades"
|
||||
[title]="grade.courseFullName"
|
||||
[class.core-selected-item]="grade.courseid === this.activeCourseId"
|
||||
class="ion-text-wrap"
|
||||
button
|
||||
detail
|
||||
(click)="openCourse(grade.courseid)"
|
||||
>
|
||||
<ion-label>
|
||||
<core-format-text
|
||||
[text]="grade.courseFullName"
|
||||
[contextInstanceId]="grade.courseid"
|
||||
contextLevel="course"
|
||||
></core-format-text>
|
||||
</ion-label>
|
||||
<ion-badge slot="end" color="light">{{grade.grade}}</ion-badge>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
</core-split-view>
|
||||
</ion-content>
|
|
@ -0,0 +1,143 @@
|
|||
// (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 { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreGrades } from '@features/grades/services/grades';
|
||||
import { CoreGradesHelper, CoreGradesGradeOverviewWithCourseData } from '@features/grades/services/grades-helper';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreScreen } from '@services/screen';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Page that displays courses grades (main menu option).
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-grades-courses',
|
||||
templateUrl: 'courses.html',
|
||||
})
|
||||
export class CoreGradesCoursesPage implements OnInit, OnDestroy {
|
||||
|
||||
grades?: CoreGradesGradeOverviewWithCourseData[];
|
||||
gradesLoaded = false;
|
||||
activeCourseId?: number;
|
||||
layoutSubscription?: Subscription;
|
||||
|
||||
constructor(private route: ActivatedRoute) {}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.layoutSubscription = CoreScreen.instance.layoutObservable.subscribe(() => this.updateActiveCourse());
|
||||
this.updateActiveCourse();
|
||||
|
||||
await this.fetchGrades();
|
||||
|
||||
if (!CoreScreen.instance.isMobile && !this.activeCourseId && this.grades && this.grades.length > 0) {
|
||||
this.openCourse(this.grades[0].courseid);
|
||||
}
|
||||
|
||||
// Add log in Moodle.
|
||||
await CoreUtils.instance.ignoreErrors(CoreGrades.instance.logCoursesGradesView());
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ionViewWillEnter(): void {
|
||||
this.updateActiveCourse();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.layoutSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the data required for the view.
|
||||
*/
|
||||
async fetchGrades(): Promise<void> {
|
||||
try {
|
||||
const grades = await CoreGrades.instance.getCoursesGrades();
|
||||
const gradesWithCourseData = await CoreGradesHelper.instance.getGradesCourseData(grades);
|
||||
|
||||
this.grades = gradesWithCourseData;
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading grades');
|
||||
|
||||
this.grades = [];
|
||||
} finally {
|
||||
this.gradesLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh data.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
*/
|
||||
async refreshGrades(refresher: IonRefresher): Promise<void> {
|
||||
await CoreUtils.instance.ignoreErrors(CoreGrades.instance.invalidateCoursesGradesData());
|
||||
await CoreUtils.instance.ignoreErrors(this.fetchGrades());
|
||||
|
||||
refresher.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the grades of the selected course.
|
||||
*
|
||||
* @param courseId Course Id where to navigate.
|
||||
*/
|
||||
async openCourse(courseId: number): Promise<void> {
|
||||
const path = this.activeCourseId ? `../${courseId}` : courseId.toString();
|
||||
|
||||
await CoreNavigator.instance.navigate(path);
|
||||
|
||||
this.updateActiveCourse(courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update active course.
|
||||
*
|
||||
* @param activeCourseId Active course id.
|
||||
*/
|
||||
private updateActiveCourse(activeCourseId?: number): void {
|
||||
if (CoreScreen.instance.isMobile) {
|
||||
delete this.activeCourseId;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeCourseId = activeCourseId ?? this.guessActiveCourse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Guess active course looking at the current route.
|
||||
*
|
||||
* @return Active course id.
|
||||
*/
|
||||
private guessActiveCourse(): number | undefined {
|
||||
const courseId = parseInt(this.route.snapshot?.firstChild?.params.courseId);
|
||||
|
||||
return isNaN(courseId) ? undefined : courseId;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'core.grades.grade' | translate }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!gradeLoaded" (ionRefresh)="refreshGrade($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="gradeLoaded">
|
||||
<core-empty-box *ngIf="!grade" icon="stats" [message]="'core.grades.nogradesreturned' | translate"></core-empty-box>
|
||||
|
||||
<ion-list *ngIf="grade">
|
||||
<ion-item *ngIf="grade.itemname && grade.link" class="ion-text-wrap" detail="true" [href]="grade.link" core-link capture="true">
|
||||
<ion-icon *ngIf="grade.icon" name="{{grade.icon}}" slot="start"></ion-icon>
|
||||
<img *ngIf="grade.image" [src]="grade.image" slot="start" class="core-module-icon" />
|
||||
<ion-label>
|
||||
<h2><core-format-text [text]="grade.itemname" contextLevel="course" [contextInstanceId]="courseId"></core-format-text></h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item *ngIf="grade.itemname && !grade.link" class="ion-text-wrap" >
|
||||
<ion-icon *ngIf="grade.icon" name="{{grade.icon}}" slot="start"></ion-icon>
|
||||
<img *ngIf="grade.image" [src]="grade.image" slot="start" class="core-module-icon" />
|
||||
<ion-label>
|
||||
<h2><core-format-text [text]="grade.itemname" contextLevel="course" [contextInstanceId]="courseId"></core-format-text></h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="grade.weight">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.grades.weight' | translate}}</h2>
|
||||
<p [innerHTML]="grade.weight"></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="grade.grade">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.grades.grade' | translate}}</h2>
|
||||
<p [innerHTML]="grade.grade"></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="grade.range">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.grades.range' | translate}}</h2>
|
||||
<p [innerHTML]="grade.range"></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="grade.percentage">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.grades.percentage' | translate}}</h2>
|
||||
<p [innerHTML]="grade.percentage"></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="grade.lettergrade">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.grades.lettergrade' | translate}}</h2>
|
||||
<p [innerHTML]="grade.lettergrade"></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="grade.rank">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.grades.rank' | translate}}</h2>
|
||||
<p [innerHTML]="grade.rank"></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="grade.average">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.grades.average' | translate}}</h2>
|
||||
<p [innerHTML]="grade.average"></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="grade.feedback">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.grades.feedback' | translate}}</h2>
|
||||
<p><core-format-text [fullTitle]="'core.grades.feedback' | translate" maxHeight="60" fullOnClick="true" [text]="grade.feedback" contextLevel="course" [contextInstanceId]="courseId"></core-format-text></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="grade.contributiontocoursetotal">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.grades.contributiontocoursetotal' | translate}}</h2>
|
||||
<p [innerHTML]="grade.contributiontocoursetotal"></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,77 @@
|
|||
// (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 { ActivatedRoute } from '@angular/router';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreGrades } from '@features/grades/services/grades';
|
||||
import { CoreGradesFormattedRow, CoreGradesHelper } from '@features/grades/services/grades-helper';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
|
||||
/**
|
||||
* Page that displays activity grade.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-grades-grade',
|
||||
templateUrl: 'grade.html',
|
||||
})
|
||||
export class CoreGradesGradePage implements OnInit {
|
||||
|
||||
courseId: number;
|
||||
userId: number;
|
||||
gradeId: number;
|
||||
grade?: CoreGradesFormattedRow | null;
|
||||
gradeLoaded = false;
|
||||
|
||||
constructor(route: ActivatedRoute) {
|
||||
this.courseId = route.snapshot.params.courseId ?? route.snapshot.parent?.params.courseId;
|
||||
this.gradeId = route.snapshot.params.gradeId;
|
||||
this.userId = route.snapshot.queryParams.userId ?? CoreSites.instance.getCurrentSiteUserId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.fetchGrade();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the data required for the view.
|
||||
*/
|
||||
async fetchGrade(): Promise<void> {
|
||||
try {
|
||||
this.grade = await CoreGradesHelper.instance.getGradeItem(this.courseId, this.gradeId, this.userId);
|
||||
this.gradeLoaded = true;
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading grade item');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh data.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
*/
|
||||
async refreshGrade(refresher: IonRefresher): Promise<void> {
|
||||
await CoreUtils.instance.ignoreErrors(CoreGrades.instance.invalidateCourseGradesData(this.courseId, this.userId));
|
||||
await CoreUtils.instance.ignoreErrors(this.fetchGrade());
|
||||
|
||||
refresher.complete();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,697 @@
|
|||
// (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 { NavController } from '@ionic/angular';
|
||||
|
||||
import { CoreLogger } from '@singletons/logger';
|
||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreCourses, CoreEnrolledCourseData, CoreCourseSearchedData } from '@features/courses/services/courses';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import {
|
||||
CoreGrades,
|
||||
CoreGradesGradeItem,
|
||||
CoreGradesGradeOverview,
|
||||
CoreGradesTable,
|
||||
CoreGradesTableRow,
|
||||
} from '@features/grades/services/grades';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreUrlUtils } from '@services/utils/url';
|
||||
import { CoreMenuItem, CoreUtils } from '@services/utils/utils';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { makeSingleton } from '@singletons';
|
||||
|
||||
/**
|
||||
* Service that provides some features regarding grades information.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CoreGradesHelperProvider {
|
||||
|
||||
protected logger: CoreLogger;
|
||||
|
||||
constructor() {
|
||||
this.logger = CoreLogger.getInstance('CoreGradesHelperProvider');
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a row from the grades table te be rendered in a page.
|
||||
*
|
||||
* @param tableRow JSON object representing row of grades table data.
|
||||
* @return Formatted row object.
|
||||
*/
|
||||
protected formatGradeRow(tableRow: CoreGradesTableRow): CoreGradesFormattedRow {
|
||||
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 = CoreTextUtils.instance.cleanTags(content);
|
||||
} else {
|
||||
content = CoreTextUtils.instance.replaceNewLines(content, '<br>');
|
||||
}
|
||||
|
||||
if (content == ' ') {
|
||||
content = '';
|
||||
}
|
||||
|
||||
row[name] = content.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a row from the grades table to be rendered in one table.
|
||||
*
|
||||
* @param tableRow JSON object representing row of grades table data.
|
||||
* @return Formatted row object.
|
||||
*/
|
||||
protected formatGradeRowForTable(tableRow: CoreGradesTableRow): CoreGradesFormattedRowForTable {
|
||||
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 = CoreTextUtils.instance.cleanTags(content);
|
||||
name = 'gradeitem';
|
||||
} else {
|
||||
content = CoreTextUtils.instance.replaceNewLines(content, '<br>');
|
||||
}
|
||||
|
||||
if (content == ' ') {
|
||||
content = '';
|
||||
}
|
||||
|
||||
row[name] = content.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes suffix formatted to compatibilize data from table and items.
|
||||
*
|
||||
* @param item Grade item to format.
|
||||
* @return Grade item formatted.
|
||||
*/
|
||||
protected formatGradeItem(item: CoreGradesGradeItem): CoreGradesFormattedItem {
|
||||
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 table JSON object representing a table with data.
|
||||
* @return Formatted HTML table.
|
||||
*/
|
||||
formatGradesTable(table: CoreGradesTable): CoreGradesFormattedTable {
|
||||
const maxDepth = table.maxdepth;
|
||||
const formatted: CoreGradesFormattedTable = {
|
||||
columns: [],
|
||||
rows: [],
|
||||
};
|
||||
|
||||
// Columns, in order.
|
||||
const 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 => this.formatGradeRowForTable(row));
|
||||
|
||||
// Get a row with some info.
|
||||
let normalRow = formatted.rows.find(
|
||||
row =>
|
||||
row.itemtype != 'leader' &&
|
||||
(typeof row.grade != 'undefined' || typeof row.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) => 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 grades Grades to get the data for.
|
||||
* @return Promise always resolved. Resolve param is the formatted grades.
|
||||
*/
|
||||
async getGradesCourseData(grades: CoreGradesGradeOverview[]): Promise<CoreGradesGradeOverviewWithCourseData[]> {
|
||||
// Obtain courses from cache to prevent network requests.
|
||||
let coursesWereMissing;
|
||||
|
||||
try {
|
||||
const courses = await CoreCourses.instance.getUserCourses(undefined, undefined, CoreSitesReadingStrategy.OnlyCache);
|
||||
const coursesMap = CoreUtils.instance.arrayToObject(courses, 'id');
|
||||
|
||||
coursesWereMissing = this.addCourseData(grades, coursesMap);
|
||||
} catch (error) {
|
||||
coursesWereMissing = true;
|
||||
}
|
||||
|
||||
// If any course wasn't found, make a network request.
|
||||
if (coursesWereMissing) {
|
||||
const coursesPromise = CoreCourses.instance.isGetCoursesByFieldAvailable()
|
||||
? CoreCourses.instance.getCoursesByField('ids', grades.map((grade) => grade.courseid).join(','))
|
||||
: CoreCourses.instance.getUserCourses(undefined, undefined, CoreSitesReadingStrategy.PreferNetwork);
|
||||
|
||||
const courses = await coursesPromise;
|
||||
const coursesMap =
|
||||
CoreUtils.instance.arrayToObject(courses as Record<string, unknown>[], 'id') as
|
||||
Record<string, CoreEnrolledCourseData> |
|
||||
Record<string, CoreCourseSearchedData>;
|
||||
|
||||
this.addCourseData(grades, coursesMap);
|
||||
}
|
||||
|
||||
return (grades as Record<string, unknown>[])
|
||||
.filter(grade => 'courseFullName' in grade) as CoreGradesGradeOverviewWithCourseData[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds course data to grades.
|
||||
*
|
||||
* @param grades Array of grades to populate.
|
||||
* @param courses HashMap of courses to read data from.
|
||||
* @return Boolean indicating if some courses were not found.
|
||||
*/
|
||||
protected addCourseData(
|
||||
grades: CoreGradesGradeOverview[],
|
||||
courses: Record<string, CoreEnrolledCourseData> | Record<string, CoreCourseSearchedData>,
|
||||
): boolean {
|
||||
let someCoursesAreMissing = false;
|
||||
|
||||
for (const grade of grades) {
|
||||
if (!(grade.courseid in courses)) {
|
||||
someCoursesAreMissing = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
(grade as CoreGradesGradeOverviewWithCourseData).courseFullName = courses[grade.courseid].fullname;
|
||||
}
|
||||
|
||||
return someCoursesAreMissing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an specific grade item.
|
||||
*
|
||||
* @param courseId ID of the course to get the grades from.
|
||||
* @param gradeId Grade ID.
|
||||
* @param userId ID of the user to get the grades from. If not defined use site's 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 to be resolved when the grades are retrieved.
|
||||
*/
|
||||
async getGradeItem(
|
||||
courseId: number,
|
||||
gradeId: number,
|
||||
userId?: number,
|
||||
siteId?: string,
|
||||
ignoreCache: boolean = false,
|
||||
): Promise<CoreGradesFormattedRow | null> {
|
||||
const grades = await CoreGrades.instance.getCourseGradesTable(courseId, userId, siteId, ignoreCache);
|
||||
|
||||
if (!grades) {
|
||||
throw new Error('Couldn\'t get grade item');
|
||||
}
|
||||
|
||||
return this.getGradesTableRow(grades, gradeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the label of the selected grade.
|
||||
*
|
||||
* @param grades Array with objects with value and label.
|
||||
* @param selectedGrade Selected grade value.
|
||||
* @return Selected grade label.
|
||||
*/
|
||||
getGradeLabelFromValue(grades: CoreGradesMenuItem[], 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 courseId ID of the course to get the grades from.
|
||||
* @param moduleId Module ID.
|
||||
* @param userId ID of the user to get the grades from. If not defined use site's current user.
|
||||
* @param groupId ID of the group to get the grades from. Not used for old gradebook table.
|
||||
* @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 to be resolved when the grades are retrieved.
|
||||
*/
|
||||
async getGradeModuleItems(
|
||||
courseId: number,
|
||||
moduleId: number,
|
||||
userId?: number,
|
||||
groupId?: number,
|
||||
siteId?: string,
|
||||
ignoreCache: boolean = false,
|
||||
): Promise<CoreGradesFormattedItem> {
|
||||
const grades = await CoreGrades.instance.getGradeItems(courseId, userId, groupId, siteId, ignoreCache);
|
||||
|
||||
if (!grades) {
|
||||
throw new Error('Couldn\'t get grade module items');
|
||||
}
|
||||
|
||||
if ('tabledata' in grades) {
|
||||
// Table format.
|
||||
return this.getModuleGradesTableRows(grades, moduleId);
|
||||
}
|
||||
|
||||
return grades.filter((item) => item.cmid == moduleId).map((item) => this.formatGradeItem(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the selected grade.
|
||||
*
|
||||
* @param grades Array with objects with value and label.
|
||||
* @param selectedGrade Selected grade label.
|
||||
* @return Selected grade value.
|
||||
*/
|
||||
getGradeValueFromLabel(grades: CoreMenuItem[], 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 text HTML where the link is present.
|
||||
* @return URL linking to the module.
|
||||
*/
|
||||
protected getModuleLink(text: string): string | false {
|
||||
const el = CoreDomUtils.instance.toDom(text)[0];
|
||||
const 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 table JSON object representing a table with data.
|
||||
* @param gradeId Grade Object identifier.
|
||||
* @return Formatted HTML table.
|
||||
*/
|
||||
getGradesTableRow(table: CoreGradesTable, gradeId: number): CoreGradesFormattedRow | null {
|
||||
if (table.tabledata) {
|
||||
const selectedRow = table.tabledata.find(
|
||||
(row) =>
|
||||
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 null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the rows related to a module from the grades table.
|
||||
*
|
||||
* @param table JSON object representing a table with data.
|
||||
* @param moduleId Grade Object identifier.
|
||||
* @return Formatted HTML table.
|
||||
*/
|
||||
getModuleGradesTableRows(table: CoreGradesTable, moduleId: number): CoreGradesFormattedRow[] {
|
||||
if (!table.tabledata) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 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 = CoreUrlUtils.instance.extractUrlParams(matches[1]);
|
||||
|
||||
return hrefParams && parseInt(hrefParams.id) === moduleId;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}).map((row) => this.formatGradeRow(row));
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to view grades.
|
||||
*
|
||||
* @param courseId Course ID to view.
|
||||
* @param userId User to view. If not defined, current user.
|
||||
* @param moduleId Module to view. If not defined, view all course grades.
|
||||
* @param navCtrl NavController to use.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async goToGrades(
|
||||
courseId: number,
|
||||
userId?: number,
|
||||
moduleId?: number,
|
||||
navCtrl?: NavController,
|
||||
siteId?: string,
|
||||
): Promise<void> {
|
||||
const modal = await CoreDomUtils.instance.showModalLoading();
|
||||
let currentUserId;
|
||||
|
||||
try {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
siteId = site.id;
|
||||
currentUserId = site.getUserId();
|
||||
|
||||
if (moduleId) {
|
||||
// Try to open the module grade directly. Check if it's possible.
|
||||
const grades = await CoreGrades.instance.isGradeItemsAvalaible(siteId);
|
||||
|
||||
if (!grades) {
|
||||
throw new Error();
|
||||
}
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
try {
|
||||
// Can get grades. Do it.
|
||||
const items = await CoreGrades.instance.getGradeItems(courseId, userId, undefined, siteId);
|
||||
|
||||
// Find the item of the module.
|
||||
const item = Array.isArray(items) && items.find((item) => moduleId == item.cmid);
|
||||
|
||||
if (!item) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
// Open the item directly.
|
||||
const gradeId = item.id;
|
||||
|
||||
await CoreUtils.instance.ignoreErrors(
|
||||
CoreNavigator.instance.navigateToSitePath(`/grades/${courseId}/${gradeId}`, {
|
||||
siteId,
|
||||
params: { userId },
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
// Cannot get grade items or there's no need to.
|
||||
if (userId && userId != currentUserId) {
|
||||
// View another user grades. Open the grades page directly.
|
||||
await CoreUtils.instance.ignoreErrors(
|
||||
CoreNavigator.instance.navigateToSitePath(`/grades/${courseId}`, {
|
||||
siteId,
|
||||
params: { userId },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// View own grades. Check if we already are in the course index page.
|
||||
if (CoreCourse.instance.currentViewIsCourse(navCtrl, courseId)) {
|
||||
// Current view is this course, just select the grades tab.
|
||||
CoreCourse.instance.selectCourseTab('CoreGrades');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// @todo
|
||||
// Open the course with the grades tab selected.
|
||||
// await CoreCourseHelper.instance.getCourse(courseId, siteId).then(async (result) => {
|
||||
// const pageParams = {
|
||||
// course: result.course,
|
||||
// selectedTab: 'CoreGrades',
|
||||
// };
|
||||
|
||||
// // CoreContentLinksHelper.instance.goInSite(navCtrl, 'CoreCourseSectionPage', pageParams, siteId)
|
||||
// return await CoreUtils.instance.ignoreErrors(CoreNavigator.instance.navigateToSitePath('/course', {
|
||||
// siteId,
|
||||
// params: pageParams,
|
||||
// }));
|
||||
// });
|
||||
}
|
||||
} catch (error) {
|
||||
// Cannot get course for some reason, just open the grades page.
|
||||
await CoreNavigator.instance.navigateToSitePath(`/grades/${courseId}`, { siteId });
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the grade items for a certain module.
|
||||
*
|
||||
* @param courseId ID of the course to invalidate the grades.
|
||||
* @param userId ID of the user to invalidate. If not defined use site's current user.
|
||||
* @param groupId ID of the group to invalidate. Not used for old gradebook table.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise to be resolved when the grades are invalidated.
|
||||
*/
|
||||
async invalidateGradeModuleItems(courseId: number, userId?: number, groupId?: number, siteId?: string): Promise<void> {
|
||||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
userId = userId || site.getUserId();
|
||||
|
||||
const enabled = await CoreGrades.instance.isGradeItemsAvalaible(siteId);
|
||||
|
||||
return enabled
|
||||
? CoreGrades.instance.invalidateCourseGradesItemsData(courseId, userId, groupId, siteId)
|
||||
: CoreGrades.instance.invalidateCourseGradesData(courseId, userId, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the image and sets it to the row.
|
||||
*
|
||||
* @param row Formatted grade row object.
|
||||
* @param text HTML where the image will be rendered.
|
||||
* @return Row object with the image.
|
||||
*/
|
||||
protected setRowIcon(row: CoreGradesFormattedRowForTable, text: string): CoreGradesFormattedRowForTable {
|
||||
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'] = CoreCourse.instance.getModuleIconSrc(
|
||||
module[1],
|
||||
CoreDomUtils.instance.convertToElement(text).querySelector('img')?.getAttribute('src') ?? undefined,
|
||||
);
|
||||
}
|
||||
} 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 gradingType If positive, max grade you can provide. If negative, scale Id.
|
||||
* @param 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 defaultLabel Element that will become default option, if not defined, it won't be added.
|
||||
* @param defaultValue Element that will become default option value. Default ''.
|
||||
* @param scale Scale csv list String. If not provided, it will take it from the module grade info.
|
||||
* @return Array with objects with value and label to create a propper HTML select.
|
||||
*/
|
||||
makeGradesMenu(
|
||||
gradingType: number,
|
||||
moduleId?: number,
|
||||
defaultLabel: string = '',
|
||||
defaultValue: string | number = '',
|
||||
scale?: string,
|
||||
): Promise<CoreGradesMenuItem[]> {
|
||||
if (gradingType < 0) {
|
||||
if (scale) {
|
||||
return Promise.resolve(CoreUtils.instance.makeMenuFromList(scale, defaultLabel, undefined, defaultValue));
|
||||
} else if (moduleId) {
|
||||
return CoreCourse.instance.getModuleBasicGradeInfo(moduleId).then((gradeInfo) => {
|
||||
if (gradeInfo && gradeInfo.scale) {
|
||||
return CoreUtils.instance.makeMenuFromList(gradeInfo.scale, defaultLabel, undefined, defaultValue);
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
||||
|
||||
if (gradingType > 0) {
|
||||
const grades: CoreGradesMenuItem[] = [];
|
||||
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([]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class CoreGradesHelper extends makeSingleton(CoreGradesHelperProvider) {}
|
||||
|
||||
// @todo formatted data types.
|
||||
export type CoreGradesFormattedRow = any;
|
||||
export type CoreGradesFormattedRowForTable = any;
|
||||
export type CoreGradesFormattedItem = any;
|
||||
export type CoreGradesFormattedTable = {
|
||||
columns: any[];
|
||||
rows: any[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Grade overview with course data added by CoreGradesHelperProvider#addCourseData method.
|
||||
*/
|
||||
export type CoreGradesGradeOverviewWithCourseData = CoreGradesGradeOverview & {
|
||||
courseFullName: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Grade menu item created by CoreGradesHelperProvider#makeGradesMenu method.
|
||||
*/
|
||||
export type CoreGradesMenuItem = {
|
||||
label: string;
|
||||
value: string | number;
|
||||
};
|
|
@ -0,0 +1,582 @@
|
|||
// (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 { CoreCourses } from '@features/courses/services/courses';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreLogger } from '@singletons/logger';
|
||||
import { CoreWSExternalWarning } from '@services/ws';
|
||||
|
||||
/**
|
||||
* Service to provide grade functionalities.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CoreGradesProvider {
|
||||
|
||||
static readonly TYPE_NONE = 0; // Moodle's GRADE_TYPE_NONE.
|
||||
static readonly TYPE_VALUE = 1; // Moodle's GRADE_TYPE_VALUE.
|
||||
static readonly TYPE_SCALE = 2; // Moodle's GRADE_TYPE_SCALE.
|
||||
static readonly TYPE_TEXT = 3; // Moodle's GRADE_TYPE_TEXT.
|
||||
|
||||
protected readonly ROOT_CACHE_KEY = 'mmGrades:';
|
||||
|
||||
protected logger: CoreLogger;
|
||||
|
||||
constructor() {
|
||||
this.logger = CoreLogger.getInstance('CoreGradesProvider');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for grade table data WS calls.
|
||||
*
|
||||
* @param courseId ID of the course to get the grades from.
|
||||
* @param userId ID of the user to get the grades from.
|
||||
* @return Cache key.
|
||||
*/
|
||||
protected getCourseGradesCacheKey(courseId: number, userId: number): string {
|
||||
return this.getCourseGradesPrefixCacheKey(courseId) + userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for grade items data WS calls.
|
||||
*
|
||||
* @param courseId ID of the course to get the grades from.
|
||||
* @param userId ID of the user to get the grades from.
|
||||
* @param groupId ID of the group to get the grades from. Default: 0.
|
||||
* @return Cache key.
|
||||
*/
|
||||
protected getCourseGradesItemsCacheKey(courseId: number, userId: number, groupId?: number): string {
|
||||
groupId = groupId ?? 0;
|
||||
|
||||
return this.getCourseGradesPrefixCacheKey(courseId) + userId + ':' + groupId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get prefix cache key for grade table data WS calls.
|
||||
*
|
||||
* @param courseId ID of the course to get the grades from.
|
||||
* @return Cache key.
|
||||
*/
|
||||
protected getCourseGradesPrefixCacheKey(courseId: number): string {
|
||||
return this.ROOT_CACHE_KEY + 'items:' + courseId + ':';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for courses grade WS calls.
|
||||
*
|
||||
* @return Cache key.
|
||||
*/
|
||||
protected getCoursesGradesCacheKey(): string {
|
||||
return this.ROOT_CACHE_KEY + 'coursesgrades';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the grade items for a certain module. Keep in mind that may have more than one item to include outcomes and scales.
|
||||
* Fallback function only used if 'gradereport_user_get_grade_items' WS is not avalaible Moodle < 3.2.
|
||||
*
|
||||
* @param courseId ID of the course to get the grades from.
|
||||
* @param userId ID of the user to get the grades from. If not defined use site's current user.
|
||||
* @param groupId ID of the group to get the grades from. Not used for old gradebook table.
|
||||
* @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 to be resolved when the grades are retrieved.
|
||||
*/
|
||||
async getGradeItems(
|
||||
courseId: number,
|
||||
userId?: number,
|
||||
groupId?: number,
|
||||
siteId?: string,
|
||||
ignoreCache: boolean = false,
|
||||
): Promise<CoreGradesGradeItem[] | CoreGradesTable> {
|
||||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
userId = userId || site.getUserId();
|
||||
|
||||
const enabled = await this.isGradeItemsAvalaible(siteId);
|
||||
|
||||
if (enabled) {
|
||||
try {
|
||||
const items = await this.getCourseGradesItems(courseId, userId, groupId, siteId, ignoreCache);
|
||||
|
||||
return items;
|
||||
} catch (error) {
|
||||
// Ignore while solving MDL-57255
|
||||
}
|
||||
}
|
||||
|
||||
return this.getCourseGradesTable(courseId, userId, siteId, ignoreCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the grade items for a certain course.
|
||||
*
|
||||
* @param courseId ID of the course to get the grades from.
|
||||
* @param userId ID of the user to get the grades from.
|
||||
* @param groupId ID of the group to get the grades from. Default 0.
|
||||
* @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 to be resolved when the grades table is retrieved.
|
||||
*/
|
||||
async getCourseGradesItems(
|
||||
courseId: number,
|
||||
userId?: number,
|
||||
groupId?: number,
|
||||
siteId?: string,
|
||||
ignoreCache: boolean = false,
|
||||
): Promise<CoreGradesGradeItem[]> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
userId = userId || site.getUserId();
|
||||
groupId = groupId || 0;
|
||||
|
||||
this.logger.debug(`Get grades for course '${courseId}' and user '${userId}'`);
|
||||
|
||||
const params: CoreGradesGetUserGradeItemsWSParams = {
|
||||
courseid: courseId,
|
||||
userid: userId,
|
||||
groupid: groupId,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getCourseGradesItemsCacheKey(courseId, userId, groupId),
|
||||
};
|
||||
|
||||
if (ignoreCache) {
|
||||
preSets['getFromCache'] = 0;
|
||||
preSets['emergencyCache'] = 0;
|
||||
}
|
||||
|
||||
const grades = await site.read<CoreGradesGetUserGradeItemsWSResponse>(
|
||||
'gradereport_user_get_grade_items',
|
||||
params,
|
||||
preSets,
|
||||
);
|
||||
|
||||
if (!grades?.usergrades?.[0]) {
|
||||
throw new Error('Couldn\'t get course grades items');
|
||||
}
|
||||
|
||||
return grades.usergrades[0].gradeitems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the grades for a certain course.
|
||||
*
|
||||
* @param courseId ID of the course to get the grades from.
|
||||
* @param userId ID of the user to get the grades from.
|
||||
* @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 to be resolved when the grades table is retrieved.
|
||||
*/
|
||||
async getCourseGradesTable(
|
||||
courseId: number,
|
||||
userId?: number,
|
||||
siteId?: string,
|
||||
ignoreCache: boolean = false,
|
||||
): Promise<CoreGradesTable> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
userId = userId || site.getUserId();
|
||||
|
||||
this.logger.debug(`Get grades for course '${courseId}' and user '${userId}'`);
|
||||
|
||||
const params: CoreGradesGetUserGradesTableWSParams = {
|
||||
courseid: courseId,
|
||||
userid: userId,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getCourseGradesCacheKey(courseId, userId),
|
||||
};
|
||||
|
||||
if (ignoreCache) {
|
||||
preSets['getFromCache'] = 0;
|
||||
preSets['emergencyCache'] = 0;
|
||||
}
|
||||
|
||||
const table = await site.read<CoreGradesGetUserGradesTableWSResponse>('gradereport_user_get_grades_table', params, preSets);
|
||||
|
||||
if (!table?.tables?.[0]) {
|
||||
throw new Error('Coudln\'t get course grades table');
|
||||
}
|
||||
|
||||
return table.tables[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the grades for a certain course.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise to be resolved when the grades are retrieved.
|
||||
*/
|
||||
async getCoursesGrades(siteId?: string): Promise<CoreGradesGradeOverview[]> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
this.logger.debug('Get course grades');
|
||||
|
||||
const params: CoreGradesGetOverviewCourseGradesWSParams = {};
|
||||
const preSets = {
|
||||
cacheKey: this.getCoursesGradesCacheKey(),
|
||||
};
|
||||
|
||||
const data = await site.read<CoreGradesGetOverviewCourseGradesWSResponse>(
|
||||
'gradereport_overview_get_course_grades',
|
||||
params,
|
||||
preSets,
|
||||
);
|
||||
|
||||
if (!data?.grades) {
|
||||
throw new Error('Couldn\'t get course grades');
|
||||
}
|
||||
|
||||
return data.grades;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates courses grade table and items WS calls for all users.
|
||||
*
|
||||
* @param courseId ID of the course to get the grades from.
|
||||
* @param siteId Site ID (empty for current site).
|
||||
* @return Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateAllCourseGradesData(courseId: number, siteId?: string): Promise<void> {
|
||||
return CoreSites.instance.getSite(siteId)
|
||||
.then((site) => site.invalidateWsCacheForKeyStartingWith(this.getCourseGradesPrefixCacheKey(courseId)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates grade table data WS calls.
|
||||
*
|
||||
* @param courseId Course ID.
|
||||
* @param userId User ID.
|
||||
* @param siteId Site id (empty for current site).
|
||||
* @return Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateCourseGradesData(courseId: number, userId?: number, siteId?: string): Promise<void> {
|
||||
return CoreSites.instance.getSite(siteId).then((site) => {
|
||||
userId = userId || site.getUserId();
|
||||
|
||||
return site.invalidateWsCacheForKey(this.getCourseGradesCacheKey(courseId, userId));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates courses grade data WS calls.
|
||||
*
|
||||
* @param siteId Site id (empty for current site).
|
||||
* @return Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateCoursesGradesData(siteId?: string): Promise<void> {
|
||||
return CoreSites.instance.getSite(siteId).then((site) => site.invalidateWsCacheForKey(this.getCoursesGradesCacheKey()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates courses grade items data WS calls.
|
||||
*
|
||||
* @param courseId ID of the course to get the grades from.
|
||||
* @param userId ID of the user to get the grades from.
|
||||
* @param groupId ID of the group to get the grades from. Default: 0.
|
||||
* @param siteId Site id (empty for current site).
|
||||
* @return Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateCourseGradesItemsData(courseId: number, userId: number, groupId?: number, siteId?: string): Promise<void> {
|
||||
return CoreSites.instance.getSite(siteId)
|
||||
.then((site) => site.invalidateWsCacheForKey(this.getCourseGradesItemsCacheKey(courseId, userId, groupId)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the plugin is enabled for a certain site.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Resolve with true if plugin is enabled, false otherwise.
|
||||
* @since Moodle 3.2
|
||||
*/
|
||||
isCourseGradesEnabled(siteId?: string): Promise<boolean> {
|
||||
return CoreSites.instance.getSite(siteId).then((site) => {
|
||||
if (!site.wsAvailable('gradereport_overview_get_course_grades')) {
|
||||
return false;
|
||||
}
|
||||
// Now check that the configurable mygradesurl is pointing to the gradereport_overview plugin.
|
||||
const url = site.getStoredConfig('mygradesurl') || '';
|
||||
|
||||
return url.indexOf('/grade/report/overview/') !== -1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the grade addon is enabled for a certain course.
|
||||
*
|
||||
* @param courseId Course ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise.
|
||||
*/
|
||||
isPluginEnabledForCourse(courseId: number, siteId?: string): Promise<boolean> {
|
||||
if (!courseId) {
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
return CoreCourses.instance.getUserCourse(courseId, true, siteId)
|
||||
.then((course) => !(course && typeof course.showgrades != 'undefined' && !course.showgrades));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not WS Grade Items is avalaible.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return True if ws is avalaible, false otherwise.
|
||||
* @since Moodle 3.2
|
||||
*/
|
||||
isGradeItemsAvalaible(siteId?: string): Promise<boolean> {
|
||||
return CoreSites.instance.getSite(siteId).then((site) => site.wsAvailable('gradereport_user_get_grade_items'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Log Course grades view in Moodle.
|
||||
*
|
||||
* @param courseId Course ID.
|
||||
* @param userId User ID.
|
||||
* @param name Course name. If not set, it will be calculated.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async logCourseGradesView(courseId: number, userId: number, name?: string): Promise<void> {
|
||||
userId = userId || CoreSites.instance.getCurrentSiteUserId();
|
||||
|
||||
const wsName = 'gradereport_user_view_grade_report';
|
||||
|
||||
if (!name) {
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
CoreCourses.instance.getUserCourse(courseId, true)
|
||||
.catch(() => ({}))
|
||||
.then(course => CorePushNotifications.instance.logViewEvent(
|
||||
courseId,
|
||||
'fullname' in course ? course.fullname : '',
|
||||
'grades',
|
||||
wsName,
|
||||
{ userid: userId },
|
||||
));
|
||||
} else {
|
||||
CorePushNotifications.instance.logViewEvent(courseId, name, 'grades', wsName, { userid: userId });
|
||||
}
|
||||
|
||||
const site = await CoreSites.instance.getCurrentSite();
|
||||
|
||||
await site?.write(wsName, { courseid: courseId, userid: userId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Log Courses grades view in Moodle.
|
||||
*
|
||||
* @param courseId Course ID. If not defined, site Home ID.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async logCoursesGradesView(courseId?: number): Promise<void> {
|
||||
if (!courseId) {
|
||||
courseId = CoreSites.instance.getCurrentSiteHomeId();
|
||||
}
|
||||
|
||||
const params = {
|
||||
courseid: courseId,
|
||||
};
|
||||
|
||||
CorePushNotifications.instance.logViewListEvent('grades', 'gradereport_overview_view_grade_report', params);
|
||||
|
||||
const site = await CoreSites.instance.getCurrentSite();
|
||||
|
||||
await site?.write('gradereport_overview_view_grade_report', params);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class CoreGrades extends makeSingleton(CoreGradesProvider) {}
|
||||
|
||||
/**
|
||||
* Params of gradereport_user_get_grade_items WS.
|
||||
*/
|
||||
type CoreGradesGetUserGradeItemsWSParams = {
|
||||
courseid: number; // Course Id.
|
||||
userid?: number; // Return grades only for this user (optional).
|
||||
groupid?: number; // Get users from this group only.
|
||||
};
|
||||
|
||||
/**
|
||||
* Params of gradereport_user_get_grades_table WS.
|
||||
*/
|
||||
type CoreGradesGetUserGradesTableWSParams = {
|
||||
courseid: number; // Course Id.
|
||||
userid?: number; // Return grades only for this user (optional).
|
||||
groupid?: number; // Get users from this group only.
|
||||
};
|
||||
|
||||
/**
|
||||
* Params of gradereport_overview_get_course_grades WS.
|
||||
*/
|
||||
type CoreGradesGetOverviewCourseGradesWSParams = {
|
||||
userid?: number; // Get grades for this user (optional, default current).
|
||||
};
|
||||
|
||||
/**
|
||||
* Data returned by gradereport_user_get_grade_items WS.
|
||||
*/
|
||||
export type CoreGradesGetUserGradeItemsWSResponse = {
|
||||
usergrades: {
|
||||
courseid: number; // Course id.
|
||||
userid: number; // User id.
|
||||
userfullname: string; // User fullname.
|
||||
useridnumber: string; // User idnumber.
|
||||
maxdepth: number; // Table max depth (needed for printing it).
|
||||
gradeitems: CoreGradesGradeItem[];
|
||||
}[];
|
||||
warnings?: CoreWSExternalWarning[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Data returned by gradereport_user_get_grades_table WS.
|
||||
*/
|
||||
export type CoreGradesGetUserGradesTableWSResponse = {
|
||||
tables: CoreGradesTable[];
|
||||
warnings?: CoreWSExternalWarning[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Data returned by gradereport_overview_get_course_grades WS.
|
||||
*/
|
||||
export type CoreGradesGetOverviewCourseGradesWSResponse = {
|
||||
grades: CoreGradesGradeOverview[];
|
||||
warnings?: CoreWSExternalWarning[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Grade item data.
|
||||
*/
|
||||
export type CoreGradesGradeItem = {
|
||||
id: number; // Grade item id.
|
||||
itemname: string; // Grade item name.
|
||||
itemtype: string; // Grade item type.
|
||||
itemmodule: string; // Grade item module.
|
||||
iteminstance: number; // Grade item instance.
|
||||
itemnumber: number; // Grade item item number.
|
||||
idnumber: string; // Grade item idnumber.
|
||||
categoryid: number; // Grade item category id.
|
||||
outcomeid: number; // Outcome id.
|
||||
scaleid: number; // Scale id.
|
||||
locked?: boolean; // Grade item for user locked?.
|
||||
cmid?: number; // Course module id (if type mod).
|
||||
weightraw?: number; // Weight raw.
|
||||
weightformatted?: string; // Weight.
|
||||
status?: string; // Status.
|
||||
graderaw?: number; // Grade raw.
|
||||
gradedatesubmitted?: number; // Grade submit date.
|
||||
gradedategraded?: number; // Grade graded date.
|
||||
gradehiddenbydate?: boolean; // Grade hidden by date?.
|
||||
gradeneedsupdate?: boolean; // Grade needs update?.
|
||||
gradeishidden?: boolean; // Grade is hidden?.
|
||||
gradeislocked?: boolean; // Grade is locked?.
|
||||
gradeisoverridden?: boolean; // Grade overridden?.
|
||||
gradeformatted?: string; // The grade formatted.
|
||||
grademin?: number; // Grade min.
|
||||
grademax?: number; // Grade max.
|
||||
rangeformatted?: string; // Range formatted.
|
||||
percentageformatted?: string; // Percentage.
|
||||
lettergradeformatted?: string; // Letter grade.
|
||||
rank?: number; // Rank in the course.
|
||||
numusers?: number; // Num users in course.
|
||||
averageformatted?: string; // Grade average.
|
||||
feedback?: string; // Grade feedback.
|
||||
feedbackformat?: number; // Feedback format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
||||
};
|
||||
|
||||
/**
|
||||
* Grade table data.
|
||||
*/
|
||||
export type CoreGradesTable = {
|
||||
courseid: number; // Course id.
|
||||
userid: number; // User id.
|
||||
userfullname: string; // User fullname.
|
||||
maxdepth: number; // Table max depth (needed for printing it).
|
||||
tabledata: CoreGradesTableRow[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Grade table data item.
|
||||
*/
|
||||
export type CoreGradesTableRow = {
|
||||
itemname?: {
|
||||
class: string; // Class.
|
||||
colspan: number; // Col span.
|
||||
content: string; // Cell content.
|
||||
celltype: string; // Cell type.
|
||||
id: string; // Id.
|
||||
}; // The item returned data.
|
||||
leader?: {
|
||||
class: string; // Class.
|
||||
rowspan: number; // Row span.
|
||||
}; // The item returned data.
|
||||
weight?: {
|
||||
class: string; // Class.
|
||||
content: string; // Cell content.
|
||||
headers: string; // Headers.
|
||||
}; // Weight column.
|
||||
grade?: {
|
||||
class: string; // Class.
|
||||
content: string; // Cell content.
|
||||
headers: string; // Headers.
|
||||
}; // Grade column.
|
||||
range?: {
|
||||
class: string; // Class.
|
||||
content: string; // Cell content.
|
||||
headers: string; // Headers.
|
||||
}; // Range column.
|
||||
percentage?: {
|
||||
class: string; // Class.
|
||||
content: string; // Cell content.
|
||||
headers: string; // Headers.
|
||||
}; // Percentage column.
|
||||
lettergrade?: {
|
||||
class: string; // Class.
|
||||
content: string; // Cell content.
|
||||
headers: string; // Headers.
|
||||
}; // Lettergrade column.
|
||||
rank?: {
|
||||
class: string; // Class.
|
||||
content: string; // Cell content.
|
||||
headers: string; // Headers.
|
||||
}; // Rank column.
|
||||
average?: {
|
||||
class: string; // Class.
|
||||
content: string; // Cell content.
|
||||
headers: string; // Headers.
|
||||
}; // Average column.
|
||||
feedback?: {
|
||||
class: string; // Class.
|
||||
content: string; // Cell content.
|
||||
headers: string; // Headers.
|
||||
}; // Feedback column.
|
||||
contributiontocoursetotal?: {
|
||||
class: string; // Class.
|
||||
content: string; // Cell content.
|
||||
headers: string; // Headers.
|
||||
}; // Contributiontocoursetotal column.
|
||||
};
|
||||
|
||||
/**
|
||||
* Grade overview data.
|
||||
*/
|
||||
export type CoreGradesGradeOverview = {
|
||||
courseid: number; // Course id.
|
||||
grade: string; // Grade formatted.
|
||||
rawgrade: string; // Raw grade, not formatted.
|
||||
rank?: number; // Your rank in the course.
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
// (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 { CoreGrades } from '@features/grades/services/grades';
|
||||
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@features/mainmenu/services/mainmenu-delegate';
|
||||
import { makeSingleton } from '@singletons';
|
||||
|
||||
/**
|
||||
* Handler to inject an option into main menu.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CoreGradesMainMenuHandlerService implements CoreMainMenuHandler {
|
||||
|
||||
static readonly PAGE_NAME = 'grades';
|
||||
|
||||
name = 'CoreGrades';
|
||||
priority = 600;
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled on a site level.
|
||||
*
|
||||
* @return Whether or not the handler is enabled on a site level.
|
||||
*/
|
||||
isEnabled(): Promise<boolean> {
|
||||
return CoreGrades.instance.isCourseGradesEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data needed to render the handler.
|
||||
*
|
||||
* @return Data needed to render the handler.
|
||||
*/
|
||||
getDisplayData(): CoreMainMenuHandlerData {
|
||||
return {
|
||||
icon: 'stats-chart',
|
||||
title: 'core.grades.grades',
|
||||
page: CoreGradesMainMenuHandlerService.PAGE_NAME,
|
||||
class: 'core-grades-coursesgrades-handler',
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default class CoreGradesMainMenuHandler extends makeSingleton(CoreGradesMainMenuHandlerService) {}
|
|
@ -846,7 +846,7 @@ export class CoreUtilsProvider {
|
|||
defaultLabel?: string,
|
||||
separator: string = ',',
|
||||
defaultValue?: T,
|
||||
): { label: string; value: T | number }[] {
|
||||
): CoreMenuItem<T>[] {
|
||||
// Split and format the list.
|
||||
const split = list.split(separator).map((label, index) => ({
|
||||
label: label.trim(),
|
||||
|
@ -1673,3 +1673,11 @@ export type CoreCountry = {
|
|||
code: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Menu item.
|
||||
*/
|
||||
export type CoreMenuItem<T = number> = {
|
||||
label: string;
|
||||
value: T | number;
|
||||
};
|
||||
|
|
|
@ -123,10 +123,13 @@ ion-toolbar {
|
|||
font-size: 14px;
|
||||
}
|
||||
|
||||
.core-selected-item {
|
||||
border-inline-start: var(--selected-item-border-width) solid var(--selected-item-color);
|
||||
}
|
||||
|
||||
// Item styles
|
||||
.item.core-selected-item {
|
||||
// TODO: Add safe are to border and RTL
|
||||
border-inline-start: var(--selected-item-border-width) solid var(--selected-item-color);
|
||||
// TODO: Add safe area to border and RTL
|
||||
--ion-safe-area-left: calc(-1 * var(--selected-item-border-width));
|
||||
}
|
||||
|
||||
|
@ -257,12 +260,18 @@ ion-avatar ion-img, ion-avatar img {
|
|||
}
|
||||
|
||||
// Activity modules
|
||||
.core-module-icon {
|
||||
width: auto;
|
||||
max-width: 24px;
|
||||
max-height: 24px;
|
||||
}
|
||||
|
||||
ion-item img.core-module-icon[slot="start"] {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
margin-right: 32px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
[dir=rtl] ion-item img.core-module-icon[slot="start"] {
|
||||
margin-right: unset;
|
||||
margin-left: 32px;
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Layout Breakpoints
|
||||
*
|
||||
* https://ionicframework.com/docs/layout/grid#default-breakpoints
|
||||
*/
|
||||
|
||||
$breakpoint-xs: 0px;
|
||||
$breakpoint-sm: 576px;
|
||||
$breakpoint-md: 768px;
|
||||
$breakpoint-lg: 992px;
|
||||
$breakpoint-xl: 1200px;
|
||||
|
||||
$breakpoint-tablet: $breakpoint-lg;
|
Loading…
Reference in New Issue