diff --git a/src/app/app.ios.scss b/src/app/app.ios.scss index 408be06f3..93f5fcfbf 100644 --- a/src/app/app.ios.scss +++ b/src/app/app.ios.scss @@ -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 { diff --git a/src/app/app.md.scss b/src/app/app.md.scss index 2f599babb..e9029a1dc 100644 --- a/src/app/app.md.scss +++ b/src/app/app.md.scss @@ -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 { diff --git a/src/app/app.scss b/src/app/app.scss index 540d8a4bf..30520d667 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -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; } diff --git a/src/app/app.wp.scss b/src/app/app.wp.scss index 8233ee3e4..4d22d076f 100644 --- a/src/app/app.wp.scss +++ b/src/app/app.wp.scss @@ -27,3 +27,8 @@ @extend .card-content-wp; } } + +.wp .core-avoid-header ion-content { + top: $navbar-wp-height; + height: calc(100% - #{($navbar-wp-height)}); +} diff --git a/src/assets/img/grades/agg_mean.png b/src/assets/img/grades/agg_mean.png new file mode 100644 index 000000000..78c8046df Binary files /dev/null and b/src/assets/img/grades/agg_mean.png differ diff --git a/src/assets/img/grades/agg_sum.png b/src/assets/img/grades/agg_sum.png new file mode 100644 index 000000000..7bcc0e43a Binary files /dev/null and b/src/assets/img/grades/agg_sum.png differ diff --git a/src/assets/img/grades/outcomes.png b/src/assets/img/grades/outcomes.png new file mode 100644 index 000000000..f52d9b682 Binary files /dev/null and b/src/assets/img/grades/outcomes.png differ diff --git a/src/components/empty-box/empty-box.scss b/src/components/empty-box/empty-box.scss index 4147e73e7..fe8163b29 100644 --- a/src/components/empty-box/empty-box.scss +++ b/src/components/empty-box/empty-box.scss @@ -42,7 +42,7 @@ core-empty-box { } } - @media only screen and (max-height: 420px) { + @include media-breakpoint-down(sm) { .core-empty-box { position: relative; diff --git a/src/components/split-view/split-view.html b/src/components/split-view/split-view.html index 6f8042dd3..b71eba4f8 100644 --- a/src/components/split-view/split-view.html +++ b/src/components/split-view/split-view.html @@ -1,6 +1,6 @@ - + - + \ No newline at end of file diff --git a/src/components/split-view/split-view.scss b/src/components/split-view/split-view.scss index 580884451..a88d5cd97 100644 --- a/src/components/split-view/split-view.scss +++ b/src/components/split-view/split-view.scss @@ -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; -} \ No newline at end of file diff --git a/src/components/tabs/tabs.ios.scss b/src/components/tabs/tabs.ios.scss deleted file mode 100644 index b9134b986..000000000 --- a/src/components/tabs/tabs.ios.scss +++ /dev/null @@ -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; - } - } -} diff --git a/src/components/tabs/tabs.scss b/src/components/tabs/tabs.scss index c68461533..b7e3916fa 100644 --- a/src/components/tabs/tabs.scss +++ b/src/components/tabs/tabs.scss @@ -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; + } +} \ No newline at end of file diff --git a/src/core/course/pages/section/section.scss b/src/core/course/pages/section/section.scss index cbcbff326..a99f04bdb 100644 --- a/src/core/course/pages/section/section.scss +++ b/src/core/course/pages/section/section.scss @@ -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; - } - } - } } diff --git a/src/core/courses/components/course-progress/course-progress.scss b/src/core/courses/components/course-progress/course-progress.scss index d115cdd3b..f320622ba 100644 --- a/src/core/courses/components/course-progress/course-progress.scss +++ b/src/core/courses/components/course-progress/course-progress.scss @@ -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; diff --git a/src/core/grades/components/components.module.ts b/src/core/grades/components/components.module.ts new file mode 100644 index 000000000..5e50f4d7a --- /dev/null +++ b/src/core/grades/components/components.module.ts @@ -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 {} diff --git a/src/core/grades/components/course/course.html b/src/core/grades/components/course/course.html new file mode 100644 index 000000000..033744660 --- /dev/null +++ b/src/core/grades/components/course/course.html @@ -0,0 +1,36 @@ + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ {{ 'core.grades.' + column.name | translate }} +
+ + + + + +
+
+
+
\ No newline at end of file diff --git a/src/core/grades/components/course/course.scss b/src/core/grades/components/course/course.scss new file mode 100644 index 000000000..b02ce767b --- /dev/null +++ b/src/core/grades/components/course/course.scss @@ -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; + } + } + } +} \ No newline at end of file diff --git a/src/core/grades/components/course/course.ts b/src/core/grades/components/course/course.ts new file mode 100644 index 000000000..0985d25e6 --- /dev/null +++ b/src/core/grades/components/course/course.ts @@ -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} Resolved when done. + */ + fetchData(refresh: boolean = false): Promise { + 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}); + } + } +} diff --git a/src/core/grades/grades.module.ts b/src/core/grades/grades.module.ts index 5f3b4d752..8eb8b7442 100644 --- a/src/core/grades/grades.module.ts +++ b/src/core/grades/grades.module.ts @@ -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); } } diff --git a/src/core/grades/lang/en.json b/src/core/grades/lang/en.json index 256d5b636..720a51127 100644 --- a/src/core/grades/lang/en.json +++ b/src/core/grades/lang/en.json @@ -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" } \ No newline at end of file diff --git a/src/core/grades/pages/course/course.html b/src/core/grades/pages/course/course.html new file mode 100644 index 000000000..73a7ecdc2 --- /dev/null +++ b/src/core/grades/pages/course/course.html @@ -0,0 +1,6 @@ + + + {{ 'core.grades.grades' | translate }} + + + diff --git a/src/core/grades/pages/course/course.module.ts b/src/core/grades/pages/course/course.module.ts new file mode 100644 index 000000000..25b0e2f34 --- /dev/null +++ b/src/core/grades/pages/course/course.module.ts @@ -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 {} diff --git a/src/core/grades/pages/course/course.ts b/src/core/grades/pages/course/course.ts new file mode 100644 index 000000000..ce40b75e5 --- /dev/null +++ b/src/core/grades/pages/course/course.ts @@ -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(); + } +} diff --git a/src/core/grades/pages/courses/courses.ts b/src/core/grades/pages/courses/courses.ts index 9c9081b4b..4dd345cff 100644 --- a/src/core/grades/pages/courses/courses.ts +++ b/src/core/grades/pages/courses/courses.ts @@ -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(() => { diff --git a/src/core/grades/providers/course-option-handler.ts b/src/core/grades/providers/course-option-handler.ts new file mode 100644 index 000000000..d2cd4028e --- /dev/null +++ b/src/core/grades/providers/course-option-handler.ts @@ -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} Promise resolved when done. + */ + invalidateEnabledForCourse(courseId: number, navOptions?: any, admOptions?: any): Promise { + 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 { + 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} True or promise resolved with true if enabled. + */ + isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise { + 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 + }; + } +} diff --git a/src/core/grades/providers/grades.ts b/src/core/grades/providers/grades.ts index 66b5a79f6..6b9f833aa 100644 --- a/src/core/grades/providers/grades.ts +++ b/src/core/grades/providers/grades.ts @@ -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} Promise to be resolved when the grades table is retrieved. + */ + getCourseGradesItems(courseId: number, userId?: number, groupId?: number, siteId?: string, + ignoreCache: boolean = false): Promise { + 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} Promise to be resolved when the grades table is retrieved. + */ + getCourseGradesTable(courseId: number, userId?: number, siteId?: string, ignoreCache: boolean = false): Promise { + 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} Promise resolved when the data is invalidated. + */ + invalidateCourseGradesData(courseId: number, userId?: number, siteId?: string): Promise { + 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} Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise. + */ + isPluginEnabledForCourse(courseId: number, siteId?: string): Promise { + 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} True if ws is avalaible, false otherwise. + * @since Moodle 3.2 + */ + isGradeItemsAvalaible(siteId?: string): Promise { + 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} Promise resolved when done. + */ + logCourseGradesView(courseId: number, userId: number): Promise { + 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} Promise resolved when done. */ logCoursesGradesView(courseId?: number): Promise { diff --git a/src/core/grades/providers/helper.ts b/src/core/grades/providers/helper.ts index b5cd905c0..a5e62a4d8 100644 --- a/src/core/grades/providers/helper.ts +++ b/src/core/grades/providers/helper.ts @@ -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, '
'); + } + + if (content == ' ') { + 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} Promise always resolved. Resolve param is the formatted grades. */ getGradesCourseData(grades: any): Promise { - // 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; + } } diff --git a/src/core/mainmenu/pages/menu/menu.html b/src/core/mainmenu/pages/menu/menu.html index 241edcbf3..be5610957 100644 --- a/src/core/mainmenu/pages/menu/menu.html +++ b/src/core/mainmenu/pages/menu/menu.html @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/src/core/user/providers/course-option-handler.ts b/src/core/user/providers/course-option-handler.ts index 633b5fde2..95ed8173d 100644 --- a/src/core/user/providers/course-option-handler.ts +++ b/src/core/user/providers/course-option-handler.ts @@ -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. diff --git a/src/core/user/providers/participants-link-handler.ts b/src/core/user/providers/participants-link-handler.ts index d2d46c695..f085458f8 100644 --- a/src/core/user/providers/participants-link-handler.ts +++ b/src/core/user/providers/participants-link-handler.ts @@ -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/; diff --git a/src/lang/en.json b/src/lang/en.json index 6ca3ca4cf..cf901b5b4 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -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.", diff --git a/src/providers/sites.ts b/src/providers/sites.ts index ef7441bd2..bcefaba14 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -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; } } diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts index e1a8a5c41..3578d8b9a 100644 --- a/src/providers/utils/dom.ts +++ b/src/providers/utils/dom.ts @@ -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. *