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.module.ts b/src/app/app.module.ts index 5da362ffc..919648b2d 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -61,6 +61,7 @@ import { CoreCourseModule } from '../core/course/course.module'; import { CoreSiteHomeModule } from '../core/sitehome/sitehome.module'; import { CoreContentLinksModule } from '../core/contentlinks/contentlinks.module'; import { CoreUserModule } from '../core/user/user.module'; +import { CoreGradesModule } from '../core/grades/grades.module'; // Addon modules. import { AddonCalendarModule } from '../addon/calendar/calendar.module'; @@ -102,6 +103,7 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { CoreSiteHomeModule, CoreContentLinksModule, CoreUserModule, + CoreGradesModule, AddonCalendarModule, AddonUserProfileFieldModule, AddonFilesModule, diff --git a/src/app/app.scss b/src/app/app.scss index 540d8a4bf..e21adbc2e 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,15 +51,17 @@ } } -@media only screen and (max-width: 768px) { +@include media-breakpoint-down(md) { .hidden-phone { display: none !important; + opacity: 0 !important; } } -@media only screen and (min-width: 769px) { +@include media-breakpoint-up(md) { .hidden-tablet { display: none !important; + opacity: 0 !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/split-view/split-view.ts b/src/components/split-view/split-view.ts index 9ddf22670..0ab53cca2 100644 --- a/src/components/split-view/split-view.ts +++ b/src/components/split-view/split-view.ts @@ -143,7 +143,7 @@ export class CoreSplitViewComponent implements OnInit { activateSplitView(): void { const currentView = this.masterNav.getActive(), currentPageName = currentView.component.name; - if (this.masterNav.getPrevious().component.name == this.masterPageName) { + if (this.masterNav.getPrevious() && this.masterNav.getPrevious().component.name == this.masterPageName) { if (currentPageName != this.masterPageName) { // CurrentView is a 'Detail' page remove it from the 'master' nav stack. this.masterNav.pop(); 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..25798e609 --- /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..a04232fb8 --- /dev/null +++ b/src/core/grades/components/course/course.scss @@ -0,0 +1,79 @@ +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, th.core-split-item-selected { + background-color: $gray-lighter; + } + } + .even { + td, th, th.core-split-item-selected { + background-color: $white; + } + } + + @include media-breakpoint-up(md) { + td { + font-size: 0.85em; + } + } + } +} + +.split-pane-side, .split-pane-main { + core-grades-course .core-grades-table .hidden-phone { + display: none; + opacity: 0; + } +} diff --git a/src/core/grades/components/course/course.ts b/src/core/grades/components/course/course.ts new file mode 100644 index 000000000..bdbe66489 --- /dev/null +++ b/src/core/grades/components/course/course.ts @@ -0,0 +1,124 @@ +// (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, Optional } 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'; +import { CoreSplitViewComponent } from '../../../../components/split-view/split-view'; +import { CoreAppProvider } from '../../../../providers/app'; + +/** + * 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; + @Input() gradeId?: number; + + gradesLoaded = false; + gradesTable: any; + + constructor(private gradesProvider: CoreGradesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams, + private gradesHelper: CoreGradesHelperProvider, private sitesProvider: CoreSitesProvider, private navCtrl: NavController, + private appProvider: CoreAppProvider, @Optional() private svComponent: CoreSplitViewComponent) { + } + + /** + * View loaded. + */ + ngOnInit(): void { + this.fetchData().then(() => { + if (this.gradeId) { + // There is the grade to load. + this.gotoGrade(this.gradeId); + } + + // 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'); + }); + } + + /** + * 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 grade of the selected item. + * @param {number} gradeId Grade item ID where to navigate. + */ + gotoGrade(gradeId: number): void { + if (gradeId) { + this.gradeId = gradeId; + let whereToPush, pageName; + + if (this.svComponent) { + if (this.svComponent.getMasterNav().getActive().component.name == 'CoreGradesCourseSplitPage') { + // Table is on left side. Push on right. + whereToPush = this.svComponent; + pageName = 'CoreGradesGradePage'; + } else { + // Table is on right side. Load new split view. + whereToPush = this.svComponent.getMasterNav(); + pageName = 'CoreGradesCourseSplitPage'; + } + } else { + if (this.appProvider.isWide()) { + // Table is full screen and large. Load here. + whereToPush = this.navCtrl; + pageName = 'CoreGradesCourseSplitPage'; + } else { + // Table is full screen but on mobile. Load here. + whereToPush = this.navCtrl; + pageName = 'CoreGradesGradePage'; + } + + } + whereToPush.push(pageName, {courseId: this.courseId, userId: this.userId, gradeId: gradeId}); + } + } +} diff --git a/src/core/grades/grades.module.ts b/src/core/grades/grades.module.ts new file mode 100644 index 000000000..44bd085fb --- /dev/null +++ b/src/core/grades/grades.module.ts @@ -0,0 +1,71 @@ +// (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 { 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'; +import { CoreGradesUserLinkHandler } from './providers/user-link-handler'; +import { CoreGradesOverviewLinkHandler } from './providers/overview-link-handler'; +import { CoreContentLinksDelegate } from '../contentlinks/providers/delegate'; +import { CoreGradesUserHandler } from './providers/user-handler'; +import { CoreUserDelegate } from '../user/providers/user-delegate'; +import { CoreEventsProvider } from '../../providers/events'; +import { CoreSitesProvider } from '../../providers/sites'; +import { CoreUserProvider } from '../user/providers/user'; + +@NgModule({ + declarations: [ + ], + imports: [ + CoreGradesComponentsModule + ], + providers: [ + CoreGradesProvider, + CoreGradesHelperProvider, + CoreGradesMainMenuHandler, + CoreGradesCourseOptionHandler, + CoreGradesUserLinkHandler, + CoreGradesOverviewLinkHandler, + CoreGradesUserHandler + ] +}) +export class CoreGradesModule { + constructor(mainMenuDelegate: CoreMainMenuDelegate, gradesMenuHandler: CoreGradesMainMenuHandler, + courseOptionHandler: CoreGradesCourseOptionHandler, courseOptionsDelegate: CoreCourseOptionsDelegate, + contentLinksDelegate: CoreContentLinksDelegate, userLinkHandler: CoreGradesUserLinkHandler, + overviewLinkHandler: CoreGradesOverviewLinkHandler, userHandler: CoreGradesUserHandler, + userDelegate: CoreUserDelegate, eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider) { + + // Register handlers. + mainMenuDelegate.registerHandler(gradesMenuHandler); + courseOptionsDelegate.registerHandler(courseOptionHandler); + contentLinksDelegate.registerHandler(userLinkHandler); + contentLinksDelegate.registerHandler(overviewLinkHandler); + userDelegate.registerHandler(userHandler); + + // Clear user profile handler cache. + eventsProvider.on(CoreUserProvider.PROFILE_REFRESHED, (data) => { + userHandler.clearViewGradesCache(data.courseId, data.userId); + }, sitesProvider.getCurrentSiteId()); + + eventsProvider.on(CoreEventsProvider.LOGOUT, () => { + userHandler.clearViewGradesCache(); + }, sitesProvider.getCurrentSiteId()); + } +} diff --git a/src/core/grades/lang/en.json b/src/core/grades/lang/en.json new file mode 100644 index 000000000..720a51127 --- /dev/null +++ b/src/core/grades/lang/en.json @@ -0,0 +1,14 @@ +{ + "average": "Average", + "contributiontocoursetotal": "Contribution to course total", + "feedback": "Feedback", + "grade": "Grade", + "gradeitem": "Grade item", + "grades": "Grades", + "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..903241e3b --- /dev/null +++ b/src/core/grades/pages/course/course.module.ts @@ -0,0 +1,31 @@ +// (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 { CoreGradesComponentsModule } from '../../components/components.module'; + +@NgModule({ + declarations: [ + CoreGradesCoursePage + ], + imports: [ + CoreGradesComponentsModule, + 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.html b/src/core/grades/pages/courses/courses.html new file mode 100644 index 000000000..f3de49188 --- /dev/null +++ b/src/core/grades/pages/courses/courses.html @@ -0,0 +1,23 @@ + + + {{ 'core.grades.grades' | translate }} + + + + + + + + + + + + + +

+ {{grade.grade}} +
+
+
+
+
\ No newline at end of file diff --git a/src/core/grades/pages/courses/courses.module.ts b/src/core/grades/pages/courses/courses.module.ts new file mode 100644 index 000000000..b4c8b834d --- /dev/null +++ b/src/core/grades/pages/courses/courses.module.ts @@ -0,0 +1,33 @@ +// (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 { CoreGradesCoursesPage } from './courses'; +import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreDirectivesModule } from '../../../../directives/directives.module'; + +@NgModule({ + declarations: [ + CoreGradesCoursesPage + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(CoreGradesCoursesPage), + TranslateModule.forChild() + ], +}) +export class CoreGradesCoursesPageModule {} diff --git a/src/core/grades/pages/courses/courses.scss b/src/core/grades/pages/courses/courses.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/core/grades/pages/courses/courses.ts b/src/core/grades/pages/courses/courses.ts new file mode 100644 index 000000000..4610fdce7 --- /dev/null +++ b/src/core/grades/pages/courses/courses.ts @@ -0,0 +1,100 @@ +// (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 } from '@angular/core'; +import { IonicPage, Content } from 'ionic-angular'; +import { CoreGradesProvider } from '../../providers/grades'; +import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreSplitViewComponent } from '../../../../components/split-view/split-view'; +import { CoreGradesHelperProvider } from '../../providers/helper'; + +/** + * Page that displays courses grades (main menu option). + */ +@IonicPage({ segment: 'core-grades-courses' }) +@Component({ + selector: 'page-core-grades-courses', + templateUrl: 'courses.html', +}) +export class CoreGradesCoursesPage { + @ViewChild(Content) content: Content; + @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; + + grades = []; + courseId: number; + userId: number; + gradesLoaded = false; + + constructor(private gradesProvider: CoreGradesProvider, private domUtils: CoreDomUtilsProvider, + private gradesHelper: CoreGradesHelperProvider) { + } + + /** + * View loaded. + */ + ionViewDidLoad(): void { + if (this.courseId) { + // There is the course to load, open the course in a new state. + this.gotoCourseGrades(this.courseId); + } + + 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(() => { + this.gradesLoaded = true; + }); + } + + /** + * Fetch all the data required for the view. + * + * @return {Promise} Resolved when done. + */ + fetchData(): Promise { + return this.gradesProvider.getCoursesGrades().then((grades) => { + return this.gradesHelper.getGradesCourseData(grades).then((grades) => { + this.grades = grades; + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error loading grades'); + }); + } + + /** + * Refresh data. + * + * @param {any} refresher Refresher. + */ + refreshGrades(refresher: any): void { + this.gradesProvider.invalidateCoursesGradesData().finally(() => { + this.fetchData().finally(() => { + refresher.complete(); + }); + }); + } + + /** + * Navigate to the grades of the selected course. + * @param {number} courseId Course Id where to navigate. + */ + gotoCourseGrades(courseId: number): void { + this.courseId = courseId; + this.splitviewCtrl.push('CoreGradesCoursePage', {courseId: courseId, userId: this.userId}); + } +} diff --git a/src/core/grades/pages/coursesplit/coursesplit.html b/src/core/grades/pages/coursesplit/coursesplit.html new file mode 100644 index 000000000..de279c1f7 --- /dev/null +++ b/src/core/grades/pages/coursesplit/coursesplit.html @@ -0,0 +1,10 @@ + + + {{ 'core.grades.grades' | translate }} + + + + + + + diff --git a/src/core/grades/pages/coursesplit/coursesplit.module.ts b/src/core/grades/pages/coursesplit/coursesplit.module.ts new file mode 100644 index 000000000..fb9bab05a --- /dev/null +++ b/src/core/grades/pages/coursesplit/coursesplit.module.ts @@ -0,0 +1,33 @@ +// (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 { CoreGradesCourseSplitPage } from './coursesplit'; +import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreGradesComponentsModule } from '../../components/components.module'; + +@NgModule({ + declarations: [ + CoreGradesCourseSplitPage + ], + imports: [ + CoreGradesComponentsModule, + CoreComponentsModule, + IonicPageModule.forChild(CoreGradesCourseSplitPage), + TranslateModule.forChild() + ], +}) +export class CoreGradesCourseSplitPageModule {} diff --git a/src/core/grades/pages/coursesplit/coursesplit.ts b/src/core/grades/pages/coursesplit/coursesplit.ts new file mode 100644 index 000000000..f759b72ca --- /dev/null +++ b/src/core/grades/pages/coursesplit/coursesplit.ts @@ -0,0 +1,38 @@ +// (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-split' }) +@Component({ + selector: 'page-core-grades-course-split', + templateUrl: 'coursesplit.html', +}) +export class CoreGradesCourseSplitPage { + + courseId: number; + userId: number; + gradeId: number; + + constructor(navParams: NavParams, sitesProvider: CoreSitesProvider) { + this.courseId = navParams.get('courseId'); + this.userId = navParams.get('userId') || sitesProvider.getCurrentSiteUserId(); + this.gradeId = navParams.get('gradeId'); + } +} diff --git a/src/core/grades/pages/grade/grade.html b/src/core/grades/pages/grade/grade.html new file mode 100644 index 000000000..dd9d671a1 --- /dev/null +++ b/src/core/grades/pages/grade/grade.html @@ -0,0 +1,72 @@ + + + {{ 'core.grades.grade' | translate }} + + + + + + + + + + + + + +

+
+ + + + +

+
+ + +

{{ 'core.grades.weight' | translate}}

+

+
+ + +

{{ 'core.grades.grade' | translate}}

+

+
+ + +

{{ 'core.grades.range' | translate}}

+

+
+ + +

{{ 'core.grades.percentage' | translate}}

+

+
+ + +

{{ 'core.grades.lettergrade' | translate}}

+

+
+ + +

{{ 'core.grades.rank' | translate}}

+

+
+ + +

{{ 'core.grades.average' | translate}}

+

+
+ + +

{{ 'core.grades.feedback' | translate}}

+

+
+ + +

{{ 'core.grades.contributiontocoursetotal' | translate}}

+

+
+
+
+
diff --git a/src/core/grades/pages/grade/grade.module.ts b/src/core/grades/pages/grade/grade.module.ts new file mode 100644 index 000000000..5b591fddd --- /dev/null +++ b/src/core/grades/pages/grade/grade.module.ts @@ -0,0 +1,33 @@ +// (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 { CoreGradesGradePage } from './grade'; +import { CoreComponentsModule } from '../../../../components/components.module'; +import { CoreDirectivesModule } from '../../../../directives/directives.module'; + +@NgModule({ + declarations: [ + CoreGradesGradePage + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(CoreGradesGradePage), + TranslateModule.forChild() + ], +}) +export class CoreGradesGradePageModule {} diff --git a/src/core/grades/pages/grade/grade.ts b/src/core/grades/pages/grade/grade.ts new file mode 100644 index 000000000..181b1d6f1 --- /dev/null +++ b/src/core/grades/pages/grade/grade.ts @@ -0,0 +1,81 @@ +// (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 } from '@angular/core'; +import { IonicPage, Content, NavParams } from 'ionic-angular'; +import { CoreGradesProvider } from '../../providers/grades'; +import { CoreDomUtilsProvider } from '../../../../providers/utils/dom'; +import { CoreGradesHelperProvider } from '../../providers/helper'; +import { CoreSitesProvider } from '../../../../providers/sites'; + +/** + * Page that displays activity grade. + */ +@IonicPage({ segment: 'core-grades-grade' }) +@Component({ + selector: 'page-core-grades-grade', + templateUrl: 'grade.html', +}) +export class CoreGradesGradePage { + @ViewChild(Content) content: Content; + + grade: any; + courseId: number; + userId: number; + gradeId: number; + gradeLoaded = false; + + constructor(private gradesProvider: CoreGradesProvider, private domUtils: CoreDomUtilsProvider, + private gradesHelper: CoreGradesHelperProvider, navParams: NavParams, sitesProvider: CoreSitesProvider) { + + this.courseId = navParams.get('courseId'); + this.userId = navParams.get('userId') || sitesProvider.getCurrentSiteUserId(); + this.gradeId = navParams.get('gradeId'); + } + + /** + * View loaded. + */ + ionViewDidLoad(): void { + this.fetchData().finally(() => { + this.gradeLoaded = true; + }); + } + + /** + * Fetch all the data required for the view. + * + * @return {Promise} Resolved when done. + */ + fetchData(): Promise { + return this.gradesHelper.getGradeItem(this.courseId, this.gradeId, this.userId).then((grade) => { + this.grade = grade; + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error loading grade item'); + }); + } + + /** + * Refresh data. + * + * @param {any} refresher Refresher. + */ + refreshGrade(refresher: any): void { + this.gradesProvider.invalidateCourseGradesData(this.courseId, this.userId).finally(() => { + this.fetchData().finally(() => { + refresher.complete(); + }); + }); + } +} 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 new file mode 100644 index 000000000..21cf6afe3 --- /dev/null +++ b/src/core/grades/providers/grades.ts @@ -0,0 +1,337 @@ +// (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 { CoreLoggerProvider } from '../../../providers/logger'; +import { CoreSite } from '../../../classes/site'; +import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreCoursesProvider } from '../../courses/providers/courses'; + +/** + * Service to provide grade functionalities. + */ +@Injectable() +export class CoreGradesProvider { + protected ROOT_CACHE_KEY = 'mmGrades:'; + + protected logger; + + 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 items 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. + * + * @return {string} 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 {number} courseId ID of the course to get the grades from. + * @param {number} [userId] ID of the user to get the grades from. If not defined use site's current user. + * @param {number} [groupId] ID of the group to get the grades from. Not used for old gradebook table. + * @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 are retrieved. + */ + getGradeItems(courseId: number, userId?: number, groupId?: number, siteId?: string, ignoreCache: boolean = false): + Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + return this.isGradeItemsAvalaible(siteId).then((enabled) => { + if (enabled) { + return this.getCourseGradesItems(courseId, userId, groupId, siteId, ignoreCache).catch(() => { + // FallBack while solving MDL-57255. + return this.getCourseGradesTable(courseId, userId, siteId, ignoreCache); + }); + } else { + return this.getCourseGradesTable(courseId, userId, siteId, ignoreCache); + } + }); + }); + } + + /** + * 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. + * + * @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. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise to be resolved when the grades are retrieved. + */ + getCoursesGrades(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + this.logger.debug('Get course grades'); + + const preSets = { + cacheKey: this.getCoursesGradesCacheKey() + }; + + return site.read('gradereport_overview_get_course_grades', undefined, preSets).then((data) => { + if (data && data.grades) { + return data.grades; + } + + return Promise.reject(null); + }); + }); + } + + /** + * 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. + * + * @param {string} [siteId] Site id (empty for current site). + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateCoursesGradesData(siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getCoursesGradesCacheKey()); + }); + } + + /** + * Invalidates courses grade items 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. + * @param {string} [siteId] Site id (empty for current site). + * @return {Promise} Promise resolved when the data is invalidated. + */ + invalidateCourseGradesItemsData(courseId: number, userId: number, groupId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getCourseGradesItemsCacheKey(courseId, userId, groupId)); + }); + } + + /** + * Returns whether or not the plugin is enabled for a certain site. + * + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Resolve with true if plugin is enabled, false otherwise. + * @since Moodle 3.2 + */ + isCourseGradesEnabled(siteId?: string): Promise { + return this.sitesProvider.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 {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 { + if (!courseId) { + courseId = this.sitesProvider.getCurrentSiteHomeId(); + } + + return this.sitesProvider.getCurrentSite().write('gradereport_overview_view_grade_report', { + courseid: courseId + }); + } +} diff --git a/src/core/grades/providers/helper.ts b/src/core/grades/providers/helper.ts new file mode 100644 index 000000000..8c7a42dfc --- /dev/null +++ b/src/core/grades/providers/helper.ts @@ -0,0 +1,407 @@ +// (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 { 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 { CoreUrlUtilsProvider } from '../../../providers/utils/url'; +import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; + +/** + * Service that provides some features regarding grades information. + */ +@Injectable() +export class CoreGradesHelperProvider { + protected logger; + + constructor(logger: CoreLoggerProvider, private coursesProvider: CoreCoursesProvider, + private gradesProvider: CoreGradesProvider, private sitesProvider: CoreSitesProvider, + private textUtils: CoreTextUtilsProvider, private courseProvider: CoreCourseProvider, + private domUtils: CoreDomUtilsProvider, private translate: TranslateService, + private urlUtils: CoreUrlUtilsProvider) { + this.logger = logger.getInstance('CoreGradesHelperProvider'); + } + + /** + * Formats a row from the grades table te be rendered in a page. + * + * @param {any} tableRow JSON object representing row of grades table data. + * @return {any} Formatted row object. + */ + protected formatGradeRow(tableRow: any): any { + const row = {}; + for (const 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('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); + } else { + content = this.textUtils.replaceNewLines(content, '
'); + } + + if (content == ' ') { + content = ''; + } + + row[name] = content.trim(); + } + } + + return row; + } + + /** + * Formats a row from the grades table to be rendered in one table. + * + * @param {any} tableRow JSON object representing row of grades table data. + * @return {any} Formatted row object. + */ + protected formatGradeRowForTable(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['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; + } + + /** + * Removes suffix formatted to compatibilize data from table and items. + * + * @param {any} item Grade item to format. + * @return {any} Grade item formatted. + */ + protected formatGradeItem(item: any): any { + 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 {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.formatGradeRowForTable(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 course data for grades since they only have courseid. + * + * @param {any} grades Grades to get the data for. + * @return {Promise} Promise always resolved. Resolve param is the formatted grades. + */ + getGradesCourseData(grades: any): Promise { + // Using cache for performance reasons. + return this.coursesProvider.getUserCourses(true).then((courses) => { + const indexedCourses = {}; + courses.forEach((course) => { + indexedCourses[course.id] = course; + }); + + grades.forEach((grade) => { + if (typeof indexedCourses[grade.courseid] != 'undefined') { + grade.courseFullName = indexedCourses[grade.courseid].fullname; + } + }); + + return grades; + }); + } + + /** + * Get an specific grade item. + * + * @param {number} courseId ID of the course to get the grades from. + * @param {number} gradeId Grade ID. + * @param {number} [userId] ID of the user to get the grades from. If not defined use site's current user. + * @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 are retrieved. + */ + getGradeItem(courseId: number, gradeId: number, userId?: number, siteId?: string, ignoreCache: boolean = false): Promise { + + return this.gradesProvider.getCourseGradesTable(courseId, userId, siteId, ignoreCache).then((grades) => { + if (grades) { + return this.getGradesTableRow(grades, gradeId); + } + + return Promise.reject(null); + }); + } + + /** + * Get the grade items for a certain module. Keep in mind that may have more than one item to include outcomes and scales. + * + * @param {number} courseId ID of the course to get the grades from. + * @param {number} moduleId Module ID. + * @param {number} [userId] ID of the user to get the grades from. If not defined use site's current user. + * @param {number} [groupId] ID of the group to get the grades from. Not used for old gradebook table. + * @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 are retrieved. + */ + getGradeModuleItems(courseId: number, moduleId: number, userId?: number, groupId?: number, siteId?: string, + ignoreCache: boolean = false): Promise { + + return this.gradesProvider.getGradeItems(courseId, userId, groupId, siteId, ignoreCache).then((grades) => { + if (grades) { + if (typeof grades.tabledata != 'undefined') { + // Table format. + return this.getModuleGradesTableRows(grades, moduleId); + } else { + return grades.filter((item) => { + return item.cmid == moduleId; + }).map((item) => { + return this.formatGradeItem(item); + }); + } + } + + return Promise.reject(null); + }); + } + + /** + * 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; + } + + /** + * Get a row from the grades table. + * + * @param {any} table JSON object representing a table with data. + * @param {number} gradeId Grade Object identifier. + * @return {any} Formatted HTML table. + */ + getGradesTableRow(table: any, gradeId: number): any { + if (table.tabledata) { + const selectedRow = table.tabledata.find((row) => { + return 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 ''; + } + + /** + * Get the rows related to a module from the grades table. + * + * @param {any} table JSON object representing a table with data. + * @param {number} moduleId Grade Object identifier. + * @return {any} Formatted HTML table. + */ + getModuleGradesTableRows(table: any, moduleId: number): any { + + if (table.tabledata) { + // 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 = this.urlUtils.extractUrlParams(matches[1]); + + return hrefParams && hrefParams.id == moduleId; + } + } + + return false; + }).map((row) => { + return this.formatGradeRow(row); + }); + } + + return []; + } + + /** + * Invalidate the grade items for a certain module. + * + * @param {number} courseId ID of the course to invalidate the grades. + * @param {number} [userId] ID of the user to invalidate. If not defined use site's current user. + * @param {number} [groupId] ID of the group to invalidate. Not used for old gradebook table. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise to be resolved when the grades are invalidated. + */ + invalidateGradeModuleItems(courseId: number, userId?: number, groupId?: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + return this.gradesProvider.isGradeItemsAvalaible(siteId).then((enabled) => { + if (enabled) { + return this.gradesProvider.invalidateCourseGradesItemsData(courseId, userId, groupId, siteId); + } else { + return this.gradesProvider.invalidateCourseGradesData(courseId, userId, siteId); + } + }); + }); + } + + /** + * 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; + } +} diff --git a/src/core/grades/providers/mainmenu-handler.ts b/src/core/grades/providers/mainmenu-handler.ts new file mode 100644 index 000000000..ff9ee2ad7 --- /dev/null +++ b/src/core/grades/providers/mainmenu-handler.ts @@ -0,0 +1,51 @@ +// (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 { CoreGradesProvider } from './grades'; +import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '../../mainmenu/providers/delegate'; + +/** + * Handler to inject an option into main menu. + */ +@Injectable() +export class CoreGradesMainMenuHandler implements CoreMainMenuHandler { + name = 'CoreGrades'; + priority = 950; + + constructor(private gradesProvider: CoreGradesProvider) { } + + /** + * Check if the handler is enabled on a site level. + * + * @return {boolean | Promise} Whether or not the handler is enabled on a site level. + */ + isEnabled(): boolean | Promise { + return this.gradesProvider.isCourseGradesEnabled(); + } + + /** + * Returns the data needed to render the handler. + * + * @return {CoreMainMenuHandlerData} Data needed to render the handler. + */ + getDisplayData(): CoreMainMenuHandlerData { + return { + icon: 'stats', + title: 'core.grades.grades', + page: 'CoreGradesCoursesPage', + class: 'core-grades-coursesgrades-handler' + }; + } +} diff --git a/src/core/grades/providers/overview-link-handler.ts b/src/core/grades/providers/overview-link-handler.ts new file mode 100644 index 000000000..915b9e2c2 --- /dev/null +++ b/src/core/grades/providers/overview-link-handler.ts @@ -0,0 +1,65 @@ +// (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 { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '../../contentlinks/providers/delegate'; +import { CoreContentLinksHelperProvider } from '../../contentlinks/providers/helper'; +import { CoreGradesProvider } from './grades'; + +/** + * Handler to treat links to overview courses grades. + */ +@Injectable() +export class CoreGradesOverviewLinkHandler extends CoreContentLinksHandlerBase { + name = 'CoreGradesOverviewLinkHandler'; + pattern = /\/grade\/report\/overview\/index.php/; + + constructor(private linkHelper: CoreContentLinksHelperProvider, private gradesProvider: CoreGradesProvider) { + super(); + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + return [{ + action: (siteId, navCtrl?): void => { + // Always use redirect to make it the new history root (to avoid "loops" in history). + this.linkHelper.goInSite(navCtrl, 'CoreGradesCoursesPage', undefined, siteId); + } + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param {string} siteId The site ID. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {boolean|Promise} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { + return this.gradesProvider.isCourseGradesEnabled(siteId); + } +} diff --git a/src/core/grades/providers/user-handler.ts b/src/core/grades/providers/user-handler.ts new file mode 100644 index 000000000..593b7cdab --- /dev/null +++ b/src/core/grades/providers/user-handler.ts @@ -0,0 +1,120 @@ +// (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 { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '../../user/providers/user-delegate'; +import { CoreSitesProvider } from '../../../providers/sites'; +import { CoreContentLinksHelperProvider } from '../../contentlinks/providers/helper'; +import { CoreGradesProvider } from './grades'; + +/** + * Profile grades handler. + */ +@Injectable() +export class CoreGradesUserHandler implements CoreUserProfileHandler { + name = 'mmGrades'; + priority = 400; + type = CoreUserDelegate.TYPE_NEW_PAGE; + viewGradesEnabledCache = {}; + + constructor(private linkHelper: CoreContentLinksHelperProvider, protected sitesProvider: CoreSitesProvider, + private gradesProvider: CoreGradesProvider) { } + + /** + * Clear view grades cache. + * If a courseId and userId are specified, it will only delete the entry for that user and course. + * + * @param {number} [courseId] Course ID. + * @param {number} [userId] User ID. + */ + clearViewGradesCache(courseId?: number, userId?: number): void { + if (courseId && userId) { + delete this.viewGradesEnabledCache[this.getCacheKey(courseId, userId)]; + } else { + this.viewGradesEnabledCache = {}; + } + } + + /** + * Get a cache key to identify a course and a user. + * + * @param {number} courseId Course ID. + * @param {number} userId User ID. + * @return {string} Cache key. + */ + protected getCacheKey(courseId: number, userId: number): string { + return courseId + '#' + userId; + } + + /** + * Check if handler is enabled. + * + * @return {boolean} Always enabled. + */ + isEnabled(): boolean { + return true; + } + + /** + * Check if handler is enabled for this user in this context. + * + * @param {any} user User to check. + * @param {number} courseId Course ID. + * @param {any} [navOptions] Course navigation options for current user. See $mmCourses#getUserNavigationOptions. + * @param {any} [admOptions] Course admin options for current user. See $mmCourses#getUserAdministrationOptions. + * @return {boolean|Promise} Promise resolved with true if enabled, resolved with false otherwise. + */ + isEnabledForUser(user: any, courseId: number, navOptions?: any, admOptions?: any): boolean | Promise { + const cacheKey = this.getCacheKey(courseId, user.id), + cache = this.viewGradesEnabledCache[cacheKey]; + if (typeof cache != 'undefined') { + return cache; + } + + return this.gradesProvider.isPluginEnabledForCourse(courseId).then(() => { + return this.gradesProvider.getCourseGradesTable(courseId, user.id).then(() => { + this.viewGradesEnabledCache[cacheKey] = true; + + return true; + }).catch(() => { + this.viewGradesEnabledCache[cacheKey] = false; + + return false; + }); + }); + } + + /** + * Returns the data needed to render the handler. + * + * @return {CoreUserProfileHandlerData} Data needed to render the handler. + */ + getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData { + return { + icon: 'stats', + title: 'core.grades.grades', + class: 'core-grades-user-handler', + action: (event, navCtrl, user, courseId): void => { + event.preventDefault(); + event.stopPropagation(); + const pageParams = { + courseId: courseId, + userId: user.id + }; + // Always use redirect to make it the new history root (to avoid "loops" in history). + this.linkHelper.goInSite(navCtrl, 'CoreGradesCoursePage', pageParams); + } + }; + } +} diff --git a/src/core/grades/providers/user-link-handler.ts b/src/core/grades/providers/user-link-handler.ts new file mode 100644 index 000000000..589f258aa --- /dev/null +++ b/src/core/grades/providers/user-link-handler.ts @@ -0,0 +1,73 @@ +// (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 { CoreContentLinksHandlerBase } from '../../contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '../../contentlinks/providers/delegate'; +import { CoreContentLinksHelperProvider } from '../../contentlinks/providers/helper'; +import { CoreGradesProvider } from './grades'; + +/** + * Handler to treat links to user grades. + */ +@Injectable() +export class CoreGradesUserLinkHandler extends CoreContentLinksHandlerBase { + name = 'CoreGradesUserLinkHandler'; + pattern = /\/grade\/report\/user\/index.php/; + + constructor(private linkHelper: CoreContentLinksHelperProvider, private gradesProvider: CoreGradesProvider) { + super(); + } + + /** + * Get the list of actions for a link (url). + * + * @param {string[]} siteIds List of sites the URL belongs to. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + return [{ + action: (siteId, navCtrl?): void => { + const pageParams = { + course: {id: courseId}, + userId: params.userid ? parseInt(params.userid, 10) : false, + }; + // Always use redirect to make it the new history root (to avoid "loops" in history). + this.linkHelper.goInSite(navCtrl, 'CoreGradesCoursePage', pageParams, siteId); + } + }]; + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param {string} siteId The site ID. + * @param {string} url The URL to treat. + * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param {number} [courseId] Course ID related to the URL. Optional but recommended. + * @return {boolean|Promise} Whether the handler is enabled for the URL and site. + */ + isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise { + if (!courseId) { + return false; + } + + return this.gradesProvider.isPluginEnabledForCourse(courseId, siteId); + } +} 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/app.ts b/src/providers/app.ts index 86df6ce5d..054ef2467 100644 --- a/src/providers/app.ts +++ b/src/providers/app.ts @@ -191,6 +191,15 @@ export class CoreAppProvider { return this.platform.is('cordova'); } + /** + * Checks if the current window is wider than a mobile. + * + * @return {boolean} Whether the app the current window is wider than a mobile. + */ + isWide(): boolean { + return this.platform.width() > 768; + } + /** * Returns whether we are online. * 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. *