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 }}
+
+
+
+
+
+
+
+
+
+
+
+ 0">
+
+
+ {{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.
*