MOBILE-2326 grades: Course grades page

main
Pau Ferrer Ocaña 2018-01-31 09:12:59 +01:00
parent abd25cf6ac
commit 4903b31cb0
33 changed files with 883 additions and 73 deletions

View File

@ -28,6 +28,11 @@
}
}
.ios .core-avoid-header ion-content {
top: $navbar-ios-height;
height: calc(100% - #{($navbar-ios-height)});
}
// Highlights inside the input element.
@if ($core-text-input-ios-show-highlight) {
.card-ios, .list-ios {

View File

@ -28,6 +28,11 @@
}
}
.md .core-avoid-header ion-content {
top: $navbar-md-height;
height: calc(100% - #{($navbar-md-height)});
}
// Highlights inside the input element.
@if ($core-text-input-md-show-highlight) {
.card-md, .list-md {

View File

@ -30,11 +30,16 @@
clear: both;
}
}
.img-responsive {
display: block;
max-width: 100%;
height: auto;
}
.opacity-hide { opacity: 0; }
.core-big { font-size: 115%; }
@media only screen and (min-width: 430px) {
@include media-breakpoint-up(sm) {
.core-center-view .scroll-content {
display: flex!important;
align-content: center !important;
@ -46,13 +51,13 @@
}
}
@media only screen and (max-width: 768px) {
@include media-breakpoint-down(md) {
.hidden-phone {
display: none !important;
}
}
@media only screen and (min-width: 769px) {
@include media-breakpoint-up(md) {
.hidden-tablet {
display: none !important;
}

View File

@ -27,3 +27,8 @@
@extend .card-content-wp;
}
}
.wp .core-avoid-header ion-content {
top: $navbar-wp-height;
height: calc(100% - #{($navbar-wp-height)});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

View File

@ -42,7 +42,7 @@ core-empty-box {
}
}
@media only screen and (max-height: 420px) {
@include media-breakpoint-down(sm) {
.core-empty-box {
position: relative;

View File

@ -1,6 +1,6 @@
<ion-split-pane (ionChange)="onSplitPaneChanged($event._visible);" [when]="when">
<ion-menu [content]="detailNav" type="push">
<ion-menu [content]="detailNav" type="push" class="core-avoid-header">
<ng-content></ng-content>
</ion-menu>
<ion-nav [root]="detailPage" #detailNav main></ion-nav>
<ion-nav [root]="detailPage" #detailNav main class="core-avoid-header"></ion-nav>
</ion-split-pane>

View File

@ -36,16 +36,7 @@ core-split-view {
}
}
}
ion-header {
display: none;
}
}
.ios ion-header + core-split-view ion-menu.split-pane-side ion-content{
top: $navbar-ios-height;
}
.md ion-header + core-split-view ion-menu.split-pane-side ion-content{
top: $navbar-md-height;
}
.wp ion-header + core-split-view ion-menu.split-pane-side ion-content{
top: $navbar-wp-height;
}

View File

@ -1,11 +0,0 @@
core-tabs {
.core-tabs-bar {
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
> a {
font-size: 1.6rem;
}
}
}

View File

@ -1,6 +1,7 @@
core-tabs {
.core-tabs-bar {
@include position(null, null, 0, 0);
left: 0;
position: relative;
z-index: $z-index-toolbar;
display: flex;
width: 100%;
@ -11,15 +12,17 @@ core-tabs {
background: $core-top-tabs-background;
color: $core-top-tabs-color !important;
border-bottom: 1px solid $core-top-tabs-border;
font-size: 1.6rem;
border: 0;
&[aria-selected=true] {
color: $core-top-tabs-color-active !important;
border-bottom: 2px solid $core-top-tabs-color-active;
border: 0 !important;
border-bottom: 2px solid $core-top-tabs-color-active !important;
}
}
}
.core-tabs-content-container {
height: 100%;
}
@ -56,3 +59,21 @@ core-tabs {
.scroll-content.no-scroll {
overflow: hidden !important;
}
.ios core-tabs {
.core-tabs-bar {
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
> a {
font-size: 1.6rem;
}
}
}
.md core-tabs {
.core-tabs-bar::after {
@extend .header-md::after;
}
}

View File

@ -1,24 +1,3 @@
page-core-course-section {
.core-tabs-bar {
@include position(null, null, 0, 0);
z-index: $z-index-toolbar;
display: flex;
width: 100%;
background: $core-top-tabs-background;
> a {
@extend .tab-button;
background: $core-top-tabs-background;
color: $core-top-tabs-color !important;
border-bottom: 1px solid $core-top-tabs-border;
font-size: 1.6rem;
&[aria-selected=true] {
color: $core-top-tabs-color-active !important;
border-bottom: 2px solid $core-top-tabs-color-active;
}
}
}
}

View File

@ -1,6 +1,6 @@
core-courses-course-progress {
&.core-courseoverview {
@media (max-width: 576px) {
@include media-breakpoint-down(sm) {
ion-card.card {
margin: 0;
border-radius: 0;

View File

@ -0,0 +1,45 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreGradesCourseComponent } from './course/course';
import { CoreComponentsModule } from '../../../components/components.module';
import { CoreDirectivesModule } from '../../../directives/directives.module';
import { CorePipesModule } from '../../../pipes/pipes.module';
@NgModule({
declarations: [
CoreGradesCourseComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule
],
providers: [
],
exports: [
CoreGradesCourseComponent
],
entryComponents: [
CoreGradesCourseComponent
]
})
export class CoreGradesComponentsModule {}

View File

@ -0,0 +1,36 @@
<ion-content>
<ion-refresher [enabled]="gradesLoaded" (ionRefresh)="refreshGrades($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="gradesLoaded">
<core-empty-box *ngIf="!gradesTable" icon="stats" [message]="errormessage">
</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.hidden-phone]="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">
<td *ngIf="row.itemtype == 'category'" class="core-grades-table-category" [attr.rowspan]="row.rowspan">
</td>
<th class="core-grades-table-gradeitem" [attr.colspan]="row.colspan">
<ion-icon *ngIf="row.icon" name="{{row.icon}}" item-start></ion-icon>
<img *ngIf="row.image" [src]="row.image" item-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]="'core-grades-table-' + column.name" [innerHTML]="row[column.name]" [class.hidden-phone]="column.hiddenPhone">
</td>
</ng-container>
</tr>
</tbody>
</table>
</div>
</core-loading>
</ion-content>

View File

@ -0,0 +1,72 @@
core-grades-course {
.core-grades-table {
border-collapse: collapse;
line-height: 20px;
width: 100%;
font-size: 16px;
color: $text-color;
tr {
border-bottom: 1px solid $list-border-color;
}
th, td {
padding-top: 10px;
padding-bottom: 10px;
padding-right: 10px;
vertical-align: top;
white-space: normal;
text-align: left;
}
thead th {
vertical-align: bottom;
font-weight: bold;
background-color: $white;
}
tbody th {
font-weight: normal;
}
#gradeitem {
padding-left: 5px;
}
.core-grades-table-gradeitem {
padding-left: 5px;
font-weight: bold;
img {
width: 16px;
height: 16px;
}
ion-icon {
color: #999999;
}
}
.core-grades-table-feedback {
padding-left: 5px;
.no-overflow {
overflow: auto;
}
}
.dimmed_text,
.hidden {
opacity: .7;
}
.odd {
td, th {
background-color: $gray-lighter;
}
}
.even {
td, th {
background-color: $white;
}
}
@include media-breakpoint-up(md) {
td {
font-size: 0.85em;
}
}
}
}

View File

@ -0,0 +1,93 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, ViewChild, Input } from '@angular/core';
import { Content, NavParams, NavController } from 'ionic-angular';
import { CoreGradesProvider } from '../../providers/grades';
import { CoreSitesProvider } from '../../../../providers/sites';
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
import { CoreGradesHelperProvider } from '../../providers/helper';
/**
* Component that displays a course grades.
*/
@Component({
selector: 'core-grades-course',
templateUrl: 'course.html',
})
export class CoreGradesCourseComponent {
@ViewChild(Content) content: Content;
@Input() courseId: number;
@Input() userId: number;
errorMessage: string;
gradesLoaded = false;
gradesTable: any;
constructor(private gradesProvider: CoreGradesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams,
private gradesHelper: CoreGradesHelperProvider, private sitesProvider: CoreSitesProvider, private navCtrl: NavController) {
}
/**
* View loaded.
*/
ngOnInit(): void {
// Get first participants.
this.fetchData().then(() => {
// Add log in Moodle.
return this.gradesProvider.logCourseGradesView(this.courseId, this.userId);
}).finally(() => {
this.gradesLoaded = true;
});
}
/**
* Fetch all the data required for the view.
*
* @param {boolean} [refresh] Empty events array first.
* @return {Promise<any>} Resolved when done.
*/
fetchData(refresh: boolean = false): Promise<any> {
return this.gradesProvider.getCourseGradesTable(this.courseId, this.userId).then((table) => {
this.gradesTable = this.gradesHelper.formatGradesTable(table);
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'Error loading grades');
this.errorMessage = error;
});
}
/**
* Refresh data.
*
* @param {any} refresher Refresher.
*/
refreshGrades(refresher: any): void {
this.gradesProvider.invalidateCourseGradesData(this.courseId, this.userId).finally(() => {
this.fetchData().finally(() => {
refresher.complete();
});
});
}
/**
* Navigate to the grades of the selected item.
* @param {number} gradeId Grade item ID where to navigate.
*/
gotoGrade(gradeId: number): void {
if (gradeId) {
this.navCtrl.push('CoreGradesGradePage', {courseId: this.courseId, userId: this.userId, gradeId: gradeId});
}
}
}

View File

@ -17,20 +17,27 @@ import { CoreGradesProvider } from './providers/grades';
import { CoreGradesHelperProvider } from './providers/helper';
import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate';
import { CoreGradesMainMenuHandler } from './providers/mainmenu-handler';
import { CoreGradesCourseOptionHandler } from './providers/course-option-handler';
import { CoreGradesComponentsModule } from './components/components.module';
import { CoreCourseOptionsDelegate } from '../course/providers/options-delegate';
@NgModule({
declarations: [
],
imports: [
CoreGradesComponentsModule
],
providers: [
CoreGradesProvider,
CoreGradesHelperProvider,
CoreGradesMainMenuHandler
CoreGradesMainMenuHandler,
CoreGradesCourseOptionHandler
]
})
export class CoreGradesModule {
constructor(mainMenuDelegate: CoreMainMenuDelegate, gradesMenuHandler: CoreGradesMainMenuHandler) {
constructor(mainMenuDelegate: CoreMainMenuDelegate, gradesMenuHandler: CoreGradesMainMenuHandler,
courseOptionHandler: CoreGradesCourseOptionHandler, courseOptionsDelegate: CoreCourseOptionsDelegate) {
mainMenuDelegate.registerHandler(gradesMenuHandler);
courseOptionsDelegate.registerHandler(courseOptionHandler);
}
}

View File

@ -1,4 +1,14 @@
{
"average": "Average",
"contributiontocoursetotal": "Contribution to course total",
"feedback": "Feedback",
"grade": "Grade",
"gradeitem": "Grade item",
"grades": "Grades",
"nogradesreturned": "No grades returned"
"lettergrade": "Letter grade",
"nogradesreturned": "No grades returned",
"percentage": "Percentage",
"range": "Range",
"rank": "Rank",
"weight": "Weight"
}

View File

@ -0,0 +1,6 @@
<ion-header>
<ion-navbar>
<ion-title>{{ 'core.grades.grades' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<core-grades-course class="core-avoid-header" [courseId]="courseId" [userId]="userId"></core-grades-course>

View File

@ -0,0 +1,35 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreGradesCoursePage } from './course';
import { CoreComponentsModule } from '../../../../components/components.module';
import { CoreDirectivesModule } from '../../../../directives/directives.module';
import { CoreGradesComponentsModule } from '../../components/components.module';
@NgModule({
declarations: [
CoreGradesCoursePage
],
imports: [
CoreGradesComponentsModule,
CoreComponentsModule,
CoreDirectivesModule,
IonicPageModule.forChild(CoreGradesCoursePage),
TranslateModule.forChild()
],
})
export class CoreGradesCoursePageModule {}

View File

@ -0,0 +1,35 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { IonicPage, NavParams } from 'ionic-angular';
import { CoreSitesProvider } from '../../../../providers/sites';
/**
* Page that displays a course grades.
*/
@IonicPage({ segment: 'core-grades-course' })
@Component({
selector: 'page-core-grades-course',
templateUrl: 'course.html',
})
export class CoreGradesCoursePage {
courseId: number;
userId: number;
constructor(navParams: NavParams, sitesProvider: CoreSitesProvider) {
this.courseId = navParams.get('courseId');
this.userId = navParams.get('userId') || sitesProvider.getCurrentSiteUserId();
}
}

View File

@ -44,8 +44,17 @@ export class CoreGradesCoursesPage {
* View loaded.
*/
ionViewDidLoad(): void {
if (this.courseId) {
// There is an event to load, open the event in a new state.
this.gotoCourseGrades(this.courseId);
}
// Get first participants.
this.fetchData().then(() => {
if (!this.courseId && this.splitviewCtrl.isOn() && this.grades.length > 0) {
this.gotoCourseGrades(this.grades[0].courseid);
}
// Add log in Moodle.
return this.gradesProvider.logCoursesGradesView();
}).finally(() => {

View File

@ -0,0 +1,92 @@
// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { NavController } from 'ionic-angular';
import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '../../course/providers/options-delegate';
import { CoreCourseProvider } from '../../course/providers/course';
import { CoreGradesProvider } from './grades';
import { CoreCoursesProvider } from '../../courses/providers/courses';
import { CoreGradesCourseComponent } from '../components/course/course';
/**
* Course nav handler.
*/
@Injectable()
export class CoreGradesCourseOptionHandler implements CoreCourseOptionsHandler {
name = 'CoreGrades';
priority = 400;
constructor(private gradesProvider: CoreGradesProvider, private coursesProvider: CoreCoursesProvider) {}
/**
* Should invalidate the data to determine if the handler is enabled for a certain course.
*
* @param {number} courseId The course ID.
* @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
* @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
* @return {Promise<any>} Promise resolved when done.
*/
invalidateEnabledForCourse(courseId: number, navOptions?: any, admOptions?: any): Promise<any> {
if (navOptions && typeof navOptions.grades != 'undefined') {
// No need to invalidate anything.
return Promise.resolve();
}
return this.coursesProvider.invalidateUserCourses();
}
/**
* Check if the handler is enabled on a site level.
*
* @return {boolean} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return true;
}
/**
* Whether or not the handler is enabled for a certain course.
*
* @param {number} courseId The course ID.
* @param {any} accessData Access type and data. Default, guest, ...
* @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
* @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
* @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
*/
isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise<boolean> {
if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) {
return false; // Not enabled for guests.
}
if (navOptions && typeof navOptions.grades != 'undefined') {
return navOptions.grades;
}
return this.gradesProvider.isPluginEnabledForCourse(courseId);
}
/**
* Returns the data needed to render the handler.
*
* @return {CoreMainMenuHandlerData} Data needed to render the handler.
*/
getDisplayData(): CoreCourseOptionsHandlerData {
return {
title: 'core.grades.grades',
class: 'core-grades-course-handler',
component: CoreGradesCourseComponent
};
}
}

View File

@ -17,6 +17,7 @@ import { CoreLoggerProvider } from '../../../providers/logger';
import { CoreSite } from '../../../classes/site';
import { CoreSitesProvider } from '../../../providers/sites';
import { CoreUtilsProvider } from '../../../providers/utils/utils';
import { CoreCoursesProvider } from '../../courses/providers/courses';
/**
* Service to provide grade functionalities.
@ -27,10 +28,46 @@ export class CoreGradesProvider {
protected logger;
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider) {
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider,
private coursesProvider: CoreCoursesProvider) {
this.logger = logger.getInstance('CoreGradesProvider');
}
/**
* Get cache key for grade table data WS calls.
*
* @param {number} courseId ID of the course to get the grades from.
* @param {number} userId ID of the user to get the grades from.
* @return {string} Cache key.
*/
protected getCourseGradesCacheKey(courseId: number, userId: number): string {
return this.getCourseGradesPrefixCacheKey(courseId) + userId;
}
/**
* Get cache key for grade table data WS calls.
*
* @param {number} courseId ID of the course to get the grades from.
* @param {number} userId ID of the user to get the grades from.
* @param {number} [groupId] ID of the group to get the grades from. Default: 0.
* @return {string} 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 {number} courseId ID of the course to get the grades from.
* @return {string} Cache key.
*/
protected getCourseGradesPrefixCacheKey(courseId: number): string {
return this.ROOT_CACHE_KEY + 'items:' + courseId + ':';
}
/**
* Get cache key for courses grade WS calls.
*
@ -40,6 +77,87 @@ export class CoreGradesProvider {
return this.ROOT_CACHE_KEY + 'coursesgrades';
}
/**
* Get the grade items for a certain course.
*
* @param {number} courseId ID of the course to get the grades from.
* @param {number} [userId] ID of the user to get the grades from.
* @param {number} [groupId] ID of the group to get the grades from. Default 0.
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down).
* @return {Promise<any>} Promise to be resolved when the grades table is retrieved.
*/
getCourseGradesItems(courseId: number, userId?: number, groupId?: number, siteId?: string,
ignoreCache: boolean = false): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
groupId = groupId || 0;
this.logger.debug(`Get grades for course '${courseId}' and user '${userId}'`);
const data = {
courseid : courseId,
userid : userId,
groupid : groupId
},
preSets = {
cacheKey: this.getCourseGradesItemsCacheKey(courseId, userId, groupId)
};
if (ignoreCache) {
preSets['getFromCache'] = 0;
preSets['emergencyCache'] = 0;
}
return site.read('gradereport_user_get_grade_items', data, preSets).then((grades) => {
if (grades && grades.usergrades && grades.usergrades[0]) {
return grades.usergrades[0].gradeitems;
}
return Promise.reject(null);
});
});
}
/**
* Get the grades for a certain course.
* Using gradereport_user_get_grades_table in case is not avalaible.
*
* @param {number} courseId ID of the course to get the grades from.
* @param {number} [userId] ID of the user to get the grades from.
* @param {string} [siteId] Site ID. If not defined, current site.
* @param {boolean} [ignoreCache=false] True if it should ignore cached data (it will always fail in offline or server down).
* @return {Promise<any>} Promise to be resolved when the grades table is retrieved.
*/
getCourseGradesTable(courseId: number, userId?: number, siteId?: string, ignoreCache: boolean = false): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
this.logger.debug(`Get grades for course '${courseId}' and user '${userId}'`);
const data = {
courseid : courseId,
userid : userId
},
preSets = {
cacheKey: this.getCourseGradesCacheKey(courseId, userId)
};
if (ignoreCache) {
preSets['getFromCache'] = 0;
preSets['emergencyCache'] = 0;
}
return site.read('gradereport_user_get_grades_table', data, preSets).then((table) => {
if (table && table.tables && table.tables[0]) {
return table.tables[0];
}
return Promise.reject(null);
});
});
}
/**
* Get the grades for a certain course.
*
@ -64,6 +182,22 @@ export class CoreGradesProvider {
});
}
/**
* Invalidates grade table data WS calls.
*
* @param {number} courseId Course ID.
* @param {number} [userId] User ID.
* @param {string} [siteId] Site id (empty for current site).
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateCourseGradesData(courseId: number, userId?: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
return site.invalidateWsCacheForKey(this.getCourseGradesCacheKey(courseId, userId));
});
}
/**
* Invalidates courses grade data WS calls.
*
@ -96,9 +230,55 @@ export class CoreGradesProvider {
}
/**
* Log Courses grades view in Moodle.
* Returns whether or not the grade addon is enabled for a certain course.
*
* @param {number} courseId Course ID.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promisee<boolean>} 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 this.coursesProvider.getUserCourse(courseId, true, siteId).then((course) => {
return !(course && typeof course.showgrades != 'undefined' && course.showgrades == 0);
});
}
/**
* Returns whether or not WS Grade Items is avalaible.
*
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<boolean>} True if ws is avalaible, false otherwise.
* @since Moodle 3.2
*/
isGradeItemsAvalaible(siteId?: string): Promise<boolean> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.wsAvailable('gradereport_user_get_grade_items');
});
}
/**
* Log Course grades view in Moodle.
*
* @param {number} courseId Course ID.
* @param {number} userId User ID.
* @return {Promise<any>} Promise resolved when done.
*/
logCourseGradesView(courseId: number, userId: number): Promise<any> {
userId = userId || this.sitesProvider.getCurrentSiteUserId();
return this.sitesProvider.getCurrentSite().write('gradereport_user_view_grade_report', {
courseid: courseId,
userid: userId
});
}
/**
* Log Courses grades view in Moodle.
*
* @param {number} [courseId] Course ID. If not defined, site Home ID.
* @return {Promise<any>} Promise resolved when done.
*/
logCoursesGradesView(courseId?: number): Promise<any> {

View File

@ -14,20 +14,130 @@
import { Injectable } from '@angular/core';
import { CoreLoggerProvider } from '../../../providers/logger';
import { CoreSitesProvider } from '../../../providers/sites';
import { TranslateService } from '@ngx-translate/core';
import { CoreCoursesProvider } from '../../courses/providers/courses';
import { CoreCourseProvider } from '../../course/providers/course';
import { CoreGradesProvider } from './grades';
import { CoreTextUtilsProvider } from '../../../providers/utils/text';
import { CoreDomUtilsProvider } from '../../../providers/utils/dom';
/**
* Service that provides some features regarding users information.
* Service that provides some features regarding grades information.
*/
@Injectable()
export class CoreGradesHelperProvider {
protected logger;
constructor(logger: CoreLoggerProvider, private coursesProvider: CoreCoursesProvider) {
constructor(logger: CoreLoggerProvider, private coursesProvider: CoreCoursesProvider,
private gradesProvider: CoreGradesProvider, private sitesProvider: CoreSitesProvider,
private textUtils: CoreTextUtilsProvider, private courseProvider: CoreCourseProvider,
private domUtils: CoreDomUtilsProvider, private translate: TranslateService) {
this.logger = logger.getInstance('CoreGradesHelperProvider');
}
/**
* Formats the response of gradereport_user_get_grades_table to be rendered.
*
* @param {any} table JSON object representing a table with data.
* @return {any} Formatted HTML table.
*/
formatGradesTable(table: any): any {
const maxDepth = table.maxdepth,
formatted = {
columns: [],
rows: []
},
// Columns, in order.
columns = {
gradeitem: true,
weight: false,
grade: false,
range: false,
percentage: false,
lettergrade: false,
rank: false,
average: false,
feedback: false,
contributiontocoursetotal: false
};
formatted.rows = table.tabledata.map((row: any) => {
return this.getGradeRow(row);
}).filter((row: any) => {
return typeof row.gradeitem !== 'undefined';
});
// Get a row with some info.
let normalRow = formatted.rows.find((e) => {
return e.itemtype != 'leader' && (typeof e.grade != 'undefined' || typeof e.percentage != 'undefined');
});
// Decide if grades or percentage is being shown on phones.
if (normalRow && typeof normalRow.grade != 'undefined') {
columns.grade = true;
} else if (normalRow && typeof normalRow.percentage != 'undefined') {
columns.percentage = true;
} else {
normalRow = formatted.rows.find((e) => {
return e.itemtype != 'leader';
});
columns.grade = true;
}
for (const colName in columns) {
if (typeof normalRow[colName] != 'undefined') {
formatted.columns.push({
name: colName,
colspan: colName == 'gradeitem' ? maxDepth : 1,
hiddenPhone: !columns[colName]
});
}
}
return formatted;
}
/**
* Get a row from the grades table.
*
* @param {any} tableRow JSON object representing row of grades table data.
* @return {any} Formatted row object.
*/
getGradeRow(tableRow: any): any {
const row = {};
for (let name in tableRow) {
if (typeof(tableRow[name].content) != 'undefined') {
let content = tableRow[name].content;
if (name == 'itemname') {
this.setRowIcon(row, content);
row['link'] = this.getModuleLink(content);
row['rowclass'] = tableRow[name].class.indexOf('leveleven') < 0 ? 'odd' : 'even';
row['rowclass'] += tableRow[name].class.indexOf('hidden') >= 0 ? ' hidden' : '';
row['rowclass'] += tableRow[name].class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
content = content.replace(/<\/span>/gi, '\n');
content = this.textUtils.cleanTags(content);
row['id'] = parseInt(tableRow[name].id.split('_')[1], 10);
row['colspan'] = tableRow[name].colspan;
row['rowspan'] = (tableRow['leader'] && tableRow['leader'].rowspan) || 1;
name = 'gradeitem';
} else {
content = this.textUtils.replaceNewLines(content, '<br>');
}
if (content == '&nbsp;') {
content = '';
}
row[name] = content.trim();
}
}
return row;
}
/**
* Get course data for grades since they only have courseid.
*
@ -35,7 +145,7 @@ export class CoreGradesHelperProvider {
* @return {Promise<any>} Promise always resolved. Resolve param is the formatted grades.
*/
getGradesCourseData(grades: any): Promise<any> {
// We ommit to use $mmCourses.getUserCourse for performance reasons.
// Using cache for performance reasons.
return this.coursesProvider.getUserCourses(true).then((courses) => {
const indexedCourses = {};
courses.forEach((course) => {
@ -52,4 +162,60 @@ export class CoreGradesHelperProvider {
});
}
/**
* Parses the image and sets it to the row.
*
* @param {any} row Formatted grade row object.
* @param {string} text HTML where the image will be rendered.
* @return {any} Row object with the image.
*/
protected setRowIcon(row: any, text: string): any {
text = text.replace('%2F', '/').replace('%2f', '/');
if (text.indexOf('/agg_mean') > -1) {
row['itemtype'] = 'agg_mean';
row['image'] = 'assets/img/grades/agg_mean.png';
} else if (text.indexOf('/agg_sum') > -1) {
row['itemtype'] = 'agg_sum';
row['image'] = 'assets/img/grades/agg_sum.png';
} else if (text.indexOf('/outcomes') > -1 || text.indexOf('fa-tasks') > -1) {
row['itemtype'] = 'outcome';
row['image'] = 'assets/img/grades/outcomes.png';
} else if (text.indexOf('i/folder') > -1 || text.indexOf('fa-folder') > -1) {
row['itemtype'] = 'category';
row['icon'] = 'folder';
} else if (text.indexOf('/manual_item') > -1 || text.indexOf('fa-square-o') > -1) {
row['itemtype'] = 'manual';
row['icon'] = 'square-outline';
} else if (text.indexOf('/mod/') > -1) {
const module = text.match(/mod\/([^\/]*)\//);
if (typeof module[1] != 'undefined') {
row['itemtype'] = 'mod';
row['itemmodule'] = module[1];
row['image'] = this.courseProvider.getModuleIconSrc(module[1]);
}
} else if (text.indexOf('src=') > -1) {
const src = text.match(/src="([^"]*)"/);
row['image'] = src[1];
}
return row;
}
/**
* Gets the link to the module for the selected grade.
*
* @param {string} text HTML where the link is present.
* @return {string | false} URL linking to the module.
*/
protected getModuleLink(text: string): string | false {
const el = this.domUtils.toDom(text)[0],
link = el.attributes['href'] ? el.attributes['href'].value : false;
if (!link || link.indexOf('/mod/') < 0) {
return false;
}
return link;
}
}

View File

@ -1,4 +1,4 @@
<ion-tabs *ngIf="loaded" #mainTabs [selectedIndex]="initialTab">
<ion-tabs *ngIf="loaded" #mainTabs [selectedIndex]="initialTab" tabsPlacement="bottom" tabsLayout="title-hide">
<ion-tab [enabled]="false" [show]="false" [root]="redirectPage" [rootParams]="redirectParams"></ion-tab>
<ion-tab *ngFor="let tab of tabs" [root]="tab.page" [tabTitle]="tab.title | translate" [tabIcon]="tab.icon" class="{{tab.class}}"></ion-tab>
</ion-tabs>

View File

@ -17,7 +17,6 @@ import { NavController } from 'ionic-angular';
import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '../../course/providers/options-delegate';
import { CoreCourseProvider } from '../../course/providers/course';
import { CoreUserProvider } from './user';
import { CoreLoginHelperProvider } from '../../login/providers/helper';
import { CoreUserParticipantsComponent } from '../components/participants/participants';
/**
@ -25,10 +24,10 @@ import { CoreUserParticipantsComponent } from '../components/participants/partic
*/
@Injectable()
export class CoreUserParticipantsCourseOptionHandler implements CoreCourseOptionsHandler {
name = 'AddonParticipants';
name = 'CoreUserParticipants';
priority = 600;
constructor(private userProvider: CoreUserProvider, private loginHelper: CoreLoginHelperProvider) {}
constructor(private userProvider: CoreUserProvider) {}
/**
* Should invalidate the data to determine if the handler is enabled for a certain course.

View File

@ -23,7 +23,7 @@ import { CoreUserProvider } from './user';
*/
@Injectable()
export class CoreUserParticipantsLinkHandler extends CoreContentLinksHandlerBase {
name = 'AddonParticipants';
name = 'CoreUserParticipants';
featureName = '$mmCoursesDelegate_mmaParticipants';
pattern = /\/user\/index\.php/;

View File

@ -61,7 +61,7 @@
"download": "Download",
"downloading": "Downloading",
"edit": "Edit",
"emptysplit": "This page will appear blank if the left panel is empty or is loading.",
"emptysplit": "This page will appear blank if the side panel is empty or is loading.",
"error": "Error",
"errorchangecompletion": "An error occurred while changing the completion status. Please try again.",
"errordeletefile": "Error deleting the file. Please try again.",

View File

@ -664,6 +664,19 @@ export class CoreSitesProvider {
return this.currentSite;
}
/**
* Get the site home ID of the current site.
*
* @return {number} Current site home ID.
*/
getCurrentSiteHomeId(): number {
if (this.currentSite) {
return this.currentSite.getSiteHomeId();
} else {
return 1;
}
}
/**
* Get current site ID.
*
@ -678,15 +691,15 @@ export class CoreSitesProvider {
}
/**
* Get the site home ID of the current site.
* Get current site User ID.
*
* @return {number} Current site home ID.
* @return {number} Current site User ID.
*/
getCurrentSiteHomeId(): number {
getCurrentSiteUserId(): number {
if (this.currentSite) {
return this.currentSite.getSiteHomeId();
return this.currentSite.getUserId();
} else {
return 1;
return 0;
}
}

View File

@ -896,6 +896,18 @@ export class CoreDomUtilsProvider {
(el.tagName.toLowerCase() == 'input' && this.INPUT_SUPPORT_KEYBOARD.indexOf(el.type) != -1));
}
/**
* Converts HTML formatted text to DOM element.
* @param {string} text HTML text.
* @return {HTMLCollection} Same text converted to HTMLCollection.
*/
toDom(text: string): HTMLCollection {
const element = document.createElement('div');
element.innerHTML = text;
return element.children;
}
/**
* View an image in a new page or modal.
*