From abd25cf6ac4946ec44637b756f06052286588300 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= <crazyserver@gmail.com>
Date: Tue, 30 Jan 2018 10:02:04 +0100
Subject: [PATCH 1/4] MOBILE-2326 grades: Courses grades page

---
 src/app/app.module.ts                         |   2 +
 src/components/split-view/split-view.ts       |   2 +-
 src/core/grades/grades.module.ts              |  36 ++++++
 src/core/grades/lang/en.json                  |   4 +
 src/core/grades/pages/courses/courses.html    |  23 ++++
 .../grades/pages/courses/courses.module.ts    |  33 +++++
 src/core/grades/pages/courses/courses.scss    |   0
 src/core/grades/pages/courses/courses.ts      |  92 ++++++++++++++
 src/core/grades/providers/grades.ts           | 113 ++++++++++++++++++
 src/core/grades/providers/helper.ts           |  55 +++++++++
 src/core/grades/providers/mainmenu-handler.ts |  51 ++++++++
 11 files changed, 410 insertions(+), 1 deletion(-)
 create mode 100644 src/core/grades/grades.module.ts
 create mode 100644 src/core/grades/lang/en.json
 create mode 100644 src/core/grades/pages/courses/courses.html
 create mode 100644 src/core/grades/pages/courses/courses.module.ts
 create mode 100644 src/core/grades/pages/courses/courses.scss
 create mode 100644 src/core/grades/pages/courses/courses.ts
 create mode 100644 src/core/grades/providers/grades.ts
 create mode 100644 src/core/grades/providers/helper.ts
 create mode 100644 src/core/grades/providers/mainmenu-handler.ts

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/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/core/grades/grades.module.ts b/src/core/grades/grades.module.ts
new file mode 100644
index 000000000..5f3b4d752
--- /dev/null
+++ b/src/core/grades/grades.module.ts
@@ -0,0 +1,36 @@
+// (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';
+
+@NgModule({
+    declarations: [
+    ],
+    imports: [
+    ],
+    providers: [
+        CoreGradesProvider,
+        CoreGradesHelperProvider,
+        CoreGradesMainMenuHandler
+    ]
+})
+export class CoreGradesModule {
+    constructor(mainMenuDelegate: CoreMainMenuDelegate, gradesMenuHandler: CoreGradesMainMenuHandler) {
+        mainMenuDelegate.registerHandler(gradesMenuHandler);
+    }
+}
diff --git a/src/core/grades/lang/en.json b/src/core/grades/lang/en.json
new file mode 100644
index 000000000..256d5b636
--- /dev/null
+++ b/src/core/grades/lang/en.json
@@ -0,0 +1,4 @@
+{
+    "grades": "Grades",
+    "nogradesreturned": "No grades returned"
+}
\ No newline at end of file
diff --git a/src/core/grades/pages/courses/courses.html b/src/core/grades/pages/courses/courses.html
new file mode 100644
index 000000000..9541c0f75
--- /dev/null
+++ b/src/core/grades/pages/courses/courses.html
@@ -0,0 +1,23 @@
+<ion-header>
+    <ion-navbar>
+        <ion-title>{{ 'core.grades.grades' | translate }}</ion-title>
+    </ion-navbar>
+</ion-header>
+<core-split-view>
+    <ion-content>
+        <ion-refresher [enabled]="gradesLoaded" (ionRefresh)="refreshGrades($event)">
+            <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
+        </ion-refresher>
+        <core-loading [hideUntil]="gradesLoaded">
+            <core-empty-box *ngIf="grades && grades.length == 0" icon="stats" [message]="'core.grades.nogradesreturned' | translate">
+            </core-empty-box>
+
+            <ion-list *ngIf="grades && grades.length > 0">
+                <a ion-item text-wrap *ngFor="let grade of grades" [title]="grade.coursefullname" (click)="gotoCourseGrades(grade.courseid)" [class.core-split-item-selected]="grade.courseid == courseId">
+                    <h2><core-format-text [text]="grade.coursefullname"></core-format-text></h2>
+                    <ion-badge item-end color="light">{{grade.grade}}</ion-badge>
+                </a>
+            </ion-list>
+        </core-loading>
+    </ion-content>
+</core-split-view>
\ 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..9c9081b4b
--- /dev/null
+++ b/src/core/grades/pages/courses/courses.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 { 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 courseHelper: CoreGradesHelperProvider) {
+    }
+
+    /**
+     * View loaded.
+     */
+    ionViewDidLoad(): void {
+        // Get first participants.
+        this.fetchData().then(() => {
+            // Add log in Moodle.
+            return this.gradesProvider.logCoursesGradesView();
+        }).finally(() => {
+            this.gradesLoaded = true;
+        });
+    }
+
+    /**
+     * Fetch all the data required for the view.
+     *
+     * @return {Promise<any>}     Resolved when done.
+     */
+    fetchData(): Promise<any> {
+        return this.gradesProvider.getCoursesGrades().then((grades) => {
+            return this.courseHelper.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, forcephoneview: 1});
+    }
+}
diff --git a/src/core/grades/providers/grades.ts b/src/core/grades/providers/grades.ts
new file mode 100644
index 000000000..66b5a79f6
--- /dev/null
+++ b/src/core/grades/providers/grades.ts
@@ -0,0 +1,113 @@
+// (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 { CoreUtilsProvider } from '../../../providers/utils/utils';
+
+/**
+ * Service to provide grade functionalities.
+ */
+@Injectable()
+export class CoreGradesProvider {
+    protected ROOT_CACHE_KEY = 'mmGrades:';
+
+    protected logger;
+
+    constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider) {
+        this.logger = logger.getInstance('CoreGradesProvider');
+    }
+
+    /**
+     * Get cache key for courses grade WS calls.
+     *
+     * @return {string}   Cache key.
+     */
+    protected getCoursesGradesCacheKey(): string {
+        return this.ROOT_CACHE_KEY + 'coursesgrades';
+    }
+
+    /**
+     * Get the grades for a certain course.
+     *
+     * @param {string} [siteId] Site ID. If not defined, current site.
+     * @return {Promise<any>}   Promise to be resolved when the grades are retrieved.
+     */
+    getCoursesGrades(siteId?: string): Promise<any> {
+        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 courses grade data WS calls.
+     *
+     * @param {string} [siteId]   Site id (empty for current site).
+     * @return {Promise<any>}     Promise resolved when the data is invalidated.
+     */
+    invalidateCoursesGradesData(siteId?: string): Promise<any> {
+        return this.sitesProvider.getSite(siteId).then((site) => {
+            return site.invalidateWsCacheForKey(this.getCoursesGradesCacheKey());
+        });
+    }
+
+    /**
+     * Returns whether or not the plugin is enabled for a certain site.
+     *
+     * @param  {string} [siteId] Site ID. If not defined, current site.
+     * @return {Promise<boolean>}  Resolve with true if plugin is enabled, false otherwise.
+     * @since  Moodle 3.2
+     */
+    isCourseGradesEnabled(siteId?: string): Promise<boolean> {
+        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;
+        });
+    }
+
+    /**
+     * Log Courses grades view in Moodle.
+     *
+     * @param  {number}  courseId Course ID.
+     * @return {Promise<any>}     Promise resolved when done.
+     */
+    logCoursesGradesView(courseId?: number): Promise<any> {
+        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..b5cd905c0
--- /dev/null
+++ b/src/core/grades/providers/helper.ts
@@ -0,0 +1,55 @@
+// (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 { TranslateService } from '@ngx-translate/core';
+import { CoreCoursesProvider } from '../../courses/providers/courses';
+
+/**
+ * Service that provides some features regarding users information.
+ */
+@Injectable()
+export class CoreGradesHelperProvider {
+    protected logger;
+
+    constructor(logger: CoreLoggerProvider, private coursesProvider: CoreCoursesProvider) {
+        this.logger = logger.getInstance('CoreGradesHelperProvider');
+    }
+
+    /**
+     * Get course data for grades since they only have courseid.
+     *
+     * @param  {Object[]} grades  Grades to get the data for.
+     * @return {Promise<any>}         Promise always resolved. Resolve param is the formatted grades.
+     */
+    getGradesCourseData(grades: any): Promise<any> {
+        // We ommit to use $mmCourses.getUserCourse for performance reasons.
+        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;
+        });
+    }
+
+}
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<boolean>} Whether or not the handler is enabled on a site level.
+     */
+    isEnabled(): boolean | Promise<boolean> {
+        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'
+        };
+    }
+}

From 4903b31cb0209afaf65af3ec111ad42e972c387b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= <crazyserver@gmail.com>
Date: Wed, 31 Jan 2018 09:12:59 +0100
Subject: [PATCH 2/4] MOBILE-2326 grades: Course grades page

---
 src/app/app.ios.scss                          |   5 +
 src/app/app.md.scss                           |   5 +
 src/app/app.scss                              |  11 +-
 src/app/app.wp.scss                           |   5 +
 src/assets/img/grades/agg_mean.png            | Bin 0 -> 341 bytes
 src/assets/img/grades/agg_sum.png             | Bin 0 -> 318 bytes
 src/assets/img/grades/outcomes.png            | Bin 0 -> 349 bytes
 src/components/empty-box/empty-box.scss       |   2 +-
 src/components/split-view/split-view.html     |   4 +-
 src/components/split-view/split-view.scss     |  15 +-
 src/components/tabs/tabs.ios.scss             |  11 --
 src/components/tabs/tabs.scss                 |  27 ++-
 src/core/course/pages/section/section.scss    |  21 --
 .../course-progress/course-progress.scss      |   2 +-
 .../grades/components/components.module.ts    |  45 +++++
 src/core/grades/components/course/course.html |  36 ++++
 src/core/grades/components/course/course.scss |  72 +++++++
 src/core/grades/components/course/course.ts   |  93 +++++++++
 src/core/grades/grades.module.ts              |  11 +-
 src/core/grades/lang/en.json                  |  12 +-
 src/core/grades/pages/course/course.html      |   6 +
 src/core/grades/pages/course/course.module.ts |  35 ++++
 src/core/grades/pages/course/course.ts        |  35 ++++
 src/core/grades/pages/courses/courses.ts      |   9 +
 .../grades/providers/course-option-handler.ts |  92 +++++++++
 src/core/grades/providers/grades.ts           | 184 +++++++++++++++++-
 src/core/grades/providers/helper.ts           | 172 +++++++++++++++-
 src/core/mainmenu/pages/menu/menu.html        |   2 +-
 .../user/providers/course-option-handler.ts   |   5 +-
 .../providers/participants-link-handler.ts    |   2 +-
 src/lang/en.json                              |   2 +-
 src/providers/sites.ts                        |  23 ++-
 src/providers/utils/dom.ts                    |  12 ++
 33 files changed, 883 insertions(+), 73 deletions(-)
 create mode 100644 src/assets/img/grades/agg_mean.png
 create mode 100644 src/assets/img/grades/agg_sum.png
 create mode 100644 src/assets/img/grades/outcomes.png
 delete mode 100644 src/components/tabs/tabs.ios.scss
 create mode 100644 src/core/grades/components/components.module.ts
 create mode 100644 src/core/grades/components/course/course.html
 create mode 100644 src/core/grades/components/course/course.scss
 create mode 100644 src/core/grades/components/course/course.ts
 create mode 100644 src/core/grades/pages/course/course.html
 create mode 100644 src/core/grades/pages/course/course.module.ts
 create mode 100644 src/core/grades/pages/course/course.ts
 create mode 100644 src/core/grades/providers/course-option-handler.ts

diff --git a/src/app/app.ios.scss b/src/app/app.ios.scss
index 408be06f3..93f5fcfbf 100644
--- a/src/app/app.ios.scss
+++ b/src/app/app.ios.scss
@@ -28,6 +28,11 @@
   }
 }
 
+.ios .core-avoid-header ion-content {
+    top: $navbar-ios-height;
+    height: calc(100% - #{($navbar-ios-height)});
+}
+
 // Highlights inside the input element.
 @if ($core-text-input-ios-show-highlight) {
   .card-ios, .list-ios {
diff --git a/src/app/app.md.scss b/src/app/app.md.scss
index 2f599babb..e9029a1dc 100644
--- a/src/app/app.md.scss
+++ b/src/app/app.md.scss
@@ -28,6 +28,11 @@
   }
 }
 
+.md .core-avoid-header ion-content {
+    top: $navbar-md-height;
+    height: calc(100% - #{($navbar-md-height)});
+}
+
 // Highlights inside the input element.
 @if ($core-text-input-md-show-highlight) {
   .card-md, .list-md {
diff --git a/src/app/app.scss b/src/app/app.scss
index 540d8a4bf..30520d667 100644
--- a/src/app/app.scss
+++ b/src/app/app.scss
@@ -30,11 +30,16 @@
     clear: both;
   }
 }
+.img-responsive {
+  display: block;
+  max-width: 100%;
+  height: auto;
+}
 
 .opacity-hide { opacity: 0; }
 .core-big   { font-size: 115%; }
 
-@media only screen and (min-width: 430px) {
+@include media-breakpoint-up(sm) {
   .core-center-view .scroll-content {
     display: flex!important;
     align-content: center !important;
@@ -46,13 +51,13 @@
   }
 }
 
-@media only screen and (max-width: 768px) {
+@include media-breakpoint-down(md) {
   .hidden-phone {
     display: none !important;
   }
 }
 
-@media only screen and (min-width: 769px) {
+@include media-breakpoint-up(md) {
   .hidden-tablet {
     display: none !important;
   }
diff --git a/src/app/app.wp.scss b/src/app/app.wp.scss
index 8233ee3e4..4d22d076f 100644
--- a/src/app/app.wp.scss
+++ b/src/app/app.wp.scss
@@ -27,3 +27,8 @@
     @extend .card-content-wp;
   }
 }
+
+.wp .core-avoid-header ion-content {
+    top: $navbar-wp-height;
+    height: calc(100% - #{($navbar-wp-height)});
+}
diff --git a/src/assets/img/grades/agg_mean.png b/src/assets/img/grades/agg_mean.png
new file mode 100644
index 0000000000000000000000000000000000000000..78c8046dfe8fa156481cf5a070e7d5e5ca2a93b8
GIT binary patch
literal 341
zcmV-b0jmCqP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUy_en%SRCwByQ^65}KnxUy0w}=_lnR6n
zpaXsJbf`c(aD4L84uA@<1Uirov;?!d=foR0Au}P|?IpQQa^AurWm!JNgd=dB=XvHk
zoMVSGq`@a}99W9p9JmXcE`fFJEi-RuK_L>ZJ#p+^L_rUG(O3;)72rT;{I|pm#r01B
z$#fMxTKrPl%HrNZ$BKAXnL^24DLPMm(3>#>aPI7L{=_!*L(cyKG<qi3fms1o);gUW
zfb)>D(nj`r%-PmY5m{way%t545g_5-Nd+lb8=OxtU>+Y$Yh5A}ux6I~z@vKOhC<J*
n3)(Npo)^jy>z%<NF98MsiN97Y!B!K800000NkvXXu0mjfw&;jC

literal 0
HcmV?d00001

diff --git a/src/assets/img/grades/agg_sum.png b/src/assets/img/grades/agg_sum.png
new file mode 100644
index 0000000000000000000000000000000000000000..7bcc0e43aadddb5c7b3d776b73379bdfca45aa66
GIT binary patch
literal 318
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk?
zp1FzXsX?iUDV2pMQ*D5X9(cMqhE&{2N^xK^PCr+|5cFHJgPH%*<;$WoB~GxjJFx6x
z6mR&!JWI(@p{(T*r$-UHBa8B`jSl%H3@fWt6kQht%PX7|XINP`!@^A8$Dx>YQzDOp
zukeC@^$k~9kL+NQe8A<hXPrU@!v(Vm&v=ixunP0(t1zr&Su#WONGij&2m^+o=Mzn=
zonl>Xum+snFooliNQbYnhE}7*nm$&~0EShO46AMm+CEvB^^CcY;Y7g>@d+l<4p*B@
z<Q+~c|GQ8i*zlA&MO;E5ldbWugYOTfdc}n$4jeiQjx?~INMsgBVA!-fICro3e_Nnm
O89ZJ6T-G@yGywn#ux#T1

literal 0
HcmV?d00001

diff --git a/src/assets/img/grades/outcomes.png b/src/assets/img/grades/outcomes.png
new file mode 100644
index 0000000000000000000000000000000000000000..f52d9b6825f1aa03cab654788121b2fba3b6f8d6
GIT binary patch
literal 349
zcmV-j0iyniP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUy|4BqaRCwByQ_&HEFbr)y24Do(05-r4
za02|}QyswxJU;n@31kD=pll!`uz|jCsTV84HFxhCn&u^a4O)3vUDqL&2XHCNvZuYB
z^0On55DCc0S9+mJgeBt736%yBdx{v@UW*_x-+;{F96=Hzp2NR8u|5#VbhmJ!?UC2Q
z*{gs?voOYKP1*sVYf$?g(4wQx21&&8O7h5uCm@Dz$&@50R#b0=pP^gBSN#}W;k81L
z0w*AM^r3EMZGw$=H-GjcE3WS#SsSDVgJ&`cJ4u4dli>O#A`g<`8Q*Lj)l>Hw=b5Il
v+(_>EocifN{0~Qdw!{&vLgi+O^)0{vGk8rTS3fwi00000NkvXXu0mjfbSsd{

literal 0
HcmV?d00001

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 @@
 <ion-split-pane (ionChange)="onSplitPaneChanged($event._visible);" [when]="when">
-    <ion-menu [content]="detailNav" type="push">
+    <ion-menu [content]="detailNav" type="push" class="core-avoid-header">
         <ng-content></ng-content>
     </ion-menu>
-    <ion-nav [root]="detailPage" #detailNav main></ion-nav>
+    <ion-nav [root]="detailPage" #detailNav main class="core-avoid-header"></ion-nav>
 </ion-split-pane>
\ No newline at end of file
diff --git a/src/components/split-view/split-view.scss b/src/components/split-view/split-view.scss
index 580884451..a88d5cd97 100644
--- a/src/components/split-view/split-view.scss
+++ b/src/components/split-view/split-view.scss
@@ -36,16 +36,7 @@ core-split-view {
             }
         }
     }
+    ion-header {
+        display: none;
+    }
 }
-
-.ios ion-header + core-split-view  ion-menu.split-pane-side ion-content{
-    top: $navbar-ios-height;
-}
-
-.md ion-header + core-split-view  ion-menu.split-pane-side ion-content{
-    top: $navbar-md-height;
-}
-
-.wp ion-header + core-split-view  ion-menu.split-pane-side ion-content{
-    top: $navbar-wp-height;
-}
\ No newline at end of file
diff --git a/src/components/tabs/tabs.ios.scss b/src/components/tabs/tabs.ios.scss
deleted file mode 100644
index b9134b986..000000000
--- a/src/components/tabs/tabs.ios.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-core-tabs {
-    .core-tabs-bar {
-        -webkit-box-pack: center;
-        -webkit-justify-content: center;
-        -ms-flex-pack: center;
-        justify-content: center;
-        > a {
-            font-size: 1.6rem;
-        }
-    }
-}
diff --git a/src/components/tabs/tabs.scss b/src/components/tabs/tabs.scss
index c68461533..b7e3916fa 100644
--- a/src/components/tabs/tabs.scss
+++ b/src/components/tabs/tabs.scss
@@ -1,6 +1,7 @@
 core-tabs {
     .core-tabs-bar {
-        @include position(null, null, 0, 0);
+        left: 0;
+        position: relative;
         z-index: $z-index-toolbar;
         display: flex;
         width: 100%;
@@ -11,15 +12,17 @@ core-tabs {
 
             background: $core-top-tabs-background;
             color: $core-top-tabs-color !important;
-            border-bottom: 1px solid $core-top-tabs-border;
             font-size: 1.6rem;
+            border: 0;
 
             &[aria-selected=true] {
                 color: $core-top-tabs-color-active !important;
-                border-bottom: 2px solid $core-top-tabs-color-active;
+                border: 0 !important;
+                border-bottom: 2px solid $core-top-tabs-color-active !important;
             }
         }
     }
+
     .core-tabs-content-container {
         height: 100%;
     }
@@ -56,3 +59,21 @@ core-tabs {
 .scroll-content.no-scroll {
     overflow: hidden !important;
 }
+
+.ios core-tabs {
+    .core-tabs-bar {
+        -webkit-box-pack: center;
+        -webkit-justify-content: center;
+        -ms-flex-pack: center;
+        justify-content: center;
+        > a {
+            font-size: 1.6rem;
+        }
+    }
+}
+
+.md core-tabs {
+    .core-tabs-bar::after {
+        @extend .header-md::after;
+    }
+}
\ No newline at end of file
diff --git a/src/core/course/pages/section/section.scss b/src/core/course/pages/section/section.scss
index cbcbff326..a99f04bdb 100644
--- a/src/core/course/pages/section/section.scss
+++ b/src/core/course/pages/section/section.scss
@@ -1,24 +1,3 @@
 page-core-course-section {
-    .core-tabs-bar {
-        @include position(null, null, 0, 0);
 
-        z-index: $z-index-toolbar;
-        display: flex;
-        width: 100%;
-        background: $core-top-tabs-background;
-
-        > a {
-            @extend .tab-button;
-
-            background: $core-top-tabs-background;
-            color: $core-top-tabs-color !important;
-            border-bottom: 1px solid $core-top-tabs-border;
-            font-size: 1.6rem;
-
-            &[aria-selected=true] {
-                color: $core-top-tabs-color-active !important;
-                border-bottom: 2px solid $core-top-tabs-color-active;
-            }
-        }
-    }
 }
diff --git a/src/core/courses/components/course-progress/course-progress.scss b/src/core/courses/components/course-progress/course-progress.scss
index d115cdd3b..f320622ba 100644
--- a/src/core/courses/components/course-progress/course-progress.scss
+++ b/src/core/courses/components/course-progress/course-progress.scss
@@ -1,6 +1,6 @@
 core-courses-course-progress {
     &.core-courseoverview {
-        @media (max-width: 576px) {
+        @include media-breakpoint-down(sm) {
             ion-card.card {
                 margin: 0;
                 border-radius: 0;
diff --git a/src/core/grades/components/components.module.ts b/src/core/grades/components/components.module.ts
new file mode 100644
index 000000000..5e50f4d7a
--- /dev/null
+++ b/src/core/grades/components/components.module.ts
@@ -0,0 +1,45 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreGradesCourseComponent } from './course/course';
+import { CoreComponentsModule } from '../../../components/components.module';
+import { CoreDirectivesModule } from '../../../directives/directives.module';
+import { CorePipesModule } from '../../../pipes/pipes.module';
+
+@NgModule({
+    declarations: [
+        CoreGradesCourseComponent
+    ],
+    imports: [
+        CommonModule,
+        IonicModule,
+        TranslateModule.forChild(),
+        CoreComponentsModule,
+        CoreDirectivesModule,
+        CorePipesModule
+    ],
+    providers: [
+    ],
+    exports: [
+        CoreGradesCourseComponent
+    ],
+    entryComponents: [
+        CoreGradesCourseComponent
+    ]
+})
+export class CoreGradesComponentsModule {}
diff --git a/src/core/grades/components/course/course.html b/src/core/grades/components/course/course.html
new file mode 100644
index 000000000..033744660
--- /dev/null
+++ b/src/core/grades/components/course/course.html
@@ -0,0 +1,36 @@
+<ion-content>
+    <ion-refresher [enabled]="gradesLoaded" (ionRefresh)="refreshGrades($event)">
+        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
+    </ion-refresher>
+    <core-loading [hideUntil]="gradesLoaded">
+        <core-empty-box *ngIf="!gradesTable" icon="stats" [message]="errormessage">
+        </core-empty-box>
+
+        <div *ngIf="gradesTable" class="core-grades-container">
+            <table cellspacing="0" cellpadding="0" class="core-grades-table">
+                <thead>
+                    <tr>
+                        <th *ngFor="let column of gradesTable.columns" id="{{column.name}}" [class.hidden-phone]="column.hiddenPhone" [attr.colspan]="column.colspan">
+                            {{ 'core.grades.' + column.name | translate }}
+                        </th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <tr *ngFor="let row of gradesTable.rows" (click)="row.itemtype != 'category' && gotoGrade(row.id)" [class]="row.rowclass">
+                        <td *ngIf="row.itemtype == 'category'" class="core-grades-table-category" [attr.rowspan]="row.rowspan">
+                        </td>
+                        <th class="core-grades-table-gradeitem" [attr.colspan]="row.colspan">
+                            <ion-icon *ngIf="row.icon" name="{{row.icon}}" item-start></ion-icon>
+                            <img *ngIf="row.image" [src]="row.image" item-start/>
+                            <span [innerHTML]="row.gradeitem"></span>
+                        </th>
+                        <ng-container *ngFor="let column of gradesTable.columns">
+                            <td *ngIf="column.name != 'gradeitem' && row[column.name] != undefined" [class]="'core-grades-table-' + column.name" [innerHTML]="row[column.name]" [class.hidden-phone]="column.hiddenPhone">
+                            </td>
+                        </ng-container>
+                    </tr>
+                </tbody>
+            </table>
+        </div>
+    </core-loading>
+</ion-content>
\ No newline at end of file
diff --git a/src/core/grades/components/course/course.scss b/src/core/grades/components/course/course.scss
new file mode 100644
index 000000000..b02ce767b
--- /dev/null
+++ b/src/core/grades/components/course/course.scss
@@ -0,0 +1,72 @@
+core-grades-course {
+  .core-grades-table {
+    border-collapse: collapse;
+    line-height: 20px;
+    width: 100%;
+    font-size: 16px;
+    color: $text-color;
+
+    tr {
+      border-bottom: 1px solid $list-border-color;
+    }
+    th, td {
+      padding-top: 10px;
+      padding-bottom: 10px;
+      padding-right: 10px;
+      vertical-align: top;
+      white-space: normal;
+      text-align: left;
+    }
+    thead th {
+      vertical-align: bottom;
+      font-weight: bold;
+      background-color: $white;
+    }
+    tbody th {
+      font-weight: normal;
+    }
+    #gradeitem {
+      padding-left: 5px;
+    }
+
+    .core-grades-table-gradeitem {
+      padding-left: 5px;
+      font-weight: bold;
+      img {
+        width: 16px;
+        height: 16px;
+      }
+      ion-icon {
+        color: #999999;
+      }
+    }
+    .core-grades-table-feedback {
+      padding-left: 5px;
+      .no-overflow {
+        overflow: auto;
+      }
+    }
+
+    .dimmed_text,
+    .hidden {
+      opacity: .7;
+    }
+
+    .odd {
+      td, th {
+        background-color: $gray-lighter;
+      }
+    }
+    .even {
+      td, th {
+        background-color: $white;
+      }
+    }
+
+    @include media-breakpoint-up(md) {
+      td {
+        font-size: 0.85em;
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/core/grades/components/course/course.ts b/src/core/grades/components/course/course.ts
new file mode 100644
index 000000000..0985d25e6
--- /dev/null
+++ b/src/core/grades/components/course/course.ts
@@ -0,0 +1,93 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, ViewChild, Input } from '@angular/core';
+import { Content, NavParams, NavController } from 'ionic-angular';
+import { CoreGradesProvider } from '../../providers/grades';
+import { CoreSitesProvider } from '../../../../providers/sites';
+import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
+import { CoreGradesHelperProvider } from '../../providers/helper';
+
+/**
+ * Component that displays a course grades.
+ */
+@Component({
+    selector: 'core-grades-course',
+    templateUrl: 'course.html',
+})
+export class CoreGradesCourseComponent {
+    @ViewChild(Content) content: Content;
+
+    @Input() courseId: number;
+    @Input() userId: number;
+
+    errorMessage: string;
+    gradesLoaded = false;
+    gradesTable: any;
+
+    constructor(private gradesProvider: CoreGradesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams,
+        private gradesHelper: CoreGradesHelperProvider, private sitesProvider: CoreSitesProvider, private navCtrl: NavController) {
+    }
+
+    /**
+     * View loaded.
+     */
+    ngOnInit(): void {
+        // Get first participants.
+        this.fetchData().then(() => {
+            // Add log in Moodle.
+            return this.gradesProvider.logCourseGradesView(this.courseId, this.userId);
+        }).finally(() => {
+            this.gradesLoaded = true;
+        });
+    }
+
+    /**
+     * Fetch all the data required for the view.
+     *
+     * @param {boolean} [refresh] Empty events array first.
+     * @return {Promise<any>}     Resolved when done.
+     */
+    fetchData(refresh: boolean = false): Promise<any> {
+        return this.gradesProvider.getCourseGradesTable(this.courseId, this.userId).then((table) => {
+            this.gradesTable = this.gradesHelper.formatGradesTable(table);
+        }).catch((error) => {
+            this.domUtils.showErrorModalDefault(error, 'Error loading grades');
+            this.errorMessage = error;
+        });
+    }
+
+    /**
+     * Refresh data.
+     *
+     * @param {any} refresher Refresher.
+     */
+    refreshGrades(refresher: any): void {
+        this.gradesProvider.invalidateCourseGradesData(this.courseId, this.userId).finally(() => {
+            this.fetchData().finally(() => {
+                refresher.complete();
+            });
+        });
+    }
+
+    /**
+     * Navigate to the grades of the selected item.
+     * @param {number} gradeId  Grade item ID where to navigate.
+     */
+    gotoGrade(gradeId: number): void {
+        if (gradeId) {
+            this.navCtrl.push('CoreGradesGradePage', {courseId: this.courseId, userId: this.userId, gradeId: gradeId});
+        }
+    }
+}
diff --git a/src/core/grades/grades.module.ts b/src/core/grades/grades.module.ts
index 5f3b4d752..8eb8b7442 100644
--- a/src/core/grades/grades.module.ts
+++ b/src/core/grades/grades.module.ts
@@ -17,20 +17,27 @@ import { CoreGradesProvider } from './providers/grades';
 import { CoreGradesHelperProvider } from './providers/helper';
 import { CoreMainMenuDelegate } from '../mainmenu/providers/delegate';
 import { CoreGradesMainMenuHandler } from './providers/mainmenu-handler';
+import { CoreGradesCourseOptionHandler } from './providers/course-option-handler';
+import { CoreGradesComponentsModule } from './components/components.module';
+import { CoreCourseOptionsDelegate } from '../course/providers/options-delegate';
 
 @NgModule({
     declarations: [
     ],
     imports: [
+        CoreGradesComponentsModule
     ],
     providers: [
         CoreGradesProvider,
         CoreGradesHelperProvider,
-        CoreGradesMainMenuHandler
+        CoreGradesMainMenuHandler,
+        CoreGradesCourseOptionHandler
     ]
 })
 export class CoreGradesModule {
-    constructor(mainMenuDelegate: CoreMainMenuDelegate, gradesMenuHandler: CoreGradesMainMenuHandler) {
+    constructor(mainMenuDelegate: CoreMainMenuDelegate, gradesMenuHandler: CoreGradesMainMenuHandler,
+            courseOptionHandler: CoreGradesCourseOptionHandler, courseOptionsDelegate: CoreCourseOptionsDelegate) {
         mainMenuDelegate.registerHandler(gradesMenuHandler);
+        courseOptionsDelegate.registerHandler(courseOptionHandler);
     }
 }
diff --git a/src/core/grades/lang/en.json b/src/core/grades/lang/en.json
index 256d5b636..720a51127 100644
--- a/src/core/grades/lang/en.json
+++ b/src/core/grades/lang/en.json
@@ -1,4 +1,14 @@
 {
+    "average": "Average",
+    "contributiontocoursetotal": "Contribution to course total",
+    "feedback": "Feedback",
+    "grade": "Grade",
+    "gradeitem": "Grade item",
     "grades": "Grades",
-    "nogradesreturned": "No grades returned"
+    "lettergrade": "Letter grade",
+    "nogradesreturned": "No grades returned",
+    "percentage": "Percentage",
+    "range": "Range",
+    "rank": "Rank",
+    "weight": "Weight"
 }
\ No newline at end of file
diff --git a/src/core/grades/pages/course/course.html b/src/core/grades/pages/course/course.html
new file mode 100644
index 000000000..73a7ecdc2
--- /dev/null
+++ b/src/core/grades/pages/course/course.html
@@ -0,0 +1,6 @@
+<ion-header>
+    <ion-navbar>
+        <ion-title>{{ 'core.grades.grades' | translate }}</ion-title>
+    </ion-navbar>
+</ion-header>
+<core-grades-course class="core-avoid-header" [courseId]="courseId" [userId]="userId"></core-grades-course>
diff --git a/src/core/grades/pages/course/course.module.ts b/src/core/grades/pages/course/course.module.ts
new file mode 100644
index 000000000..25b0e2f34
--- /dev/null
+++ b/src/core/grades/pages/course/course.module.ts
@@ -0,0 +1,35 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicPageModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreGradesCoursePage } from './course';
+import { CoreComponentsModule } from '../../../../components/components.module';
+import { CoreDirectivesModule } from '../../../../directives/directives.module';
+import { CoreGradesComponentsModule } from '../../components/components.module';
+
+@NgModule({
+    declarations: [
+        CoreGradesCoursePage
+    ],
+    imports: [
+        CoreGradesComponentsModule,
+        CoreComponentsModule,
+        CoreDirectivesModule,
+        IonicPageModule.forChild(CoreGradesCoursePage),
+        TranslateModule.forChild()
+    ],
+})
+export class CoreGradesCoursePageModule {}
diff --git a/src/core/grades/pages/course/course.ts b/src/core/grades/pages/course/course.ts
new file mode 100644
index 000000000..ce40b75e5
--- /dev/null
+++ b/src/core/grades/pages/course/course.ts
@@ -0,0 +1,35 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component } from '@angular/core';
+import { IonicPage, NavParams } from 'ionic-angular';
+import { CoreSitesProvider } from '../../../../providers/sites';
+
+/**
+ * Page that displays a course grades.
+ */
+@IonicPage({ segment: 'core-grades-course' })
+@Component({
+    selector: 'page-core-grades-course',
+    templateUrl: 'course.html',
+})
+export class CoreGradesCoursePage {
+    courseId: number;
+    userId: number;
+
+    constructor(navParams: NavParams, sitesProvider: CoreSitesProvider) {
+        this.courseId = navParams.get('courseId');
+        this.userId = navParams.get('userId') || sitesProvider.getCurrentSiteUserId();
+    }
+}
diff --git a/src/core/grades/pages/courses/courses.ts b/src/core/grades/pages/courses/courses.ts
index 9c9081b4b..4dd345cff 100644
--- a/src/core/grades/pages/courses/courses.ts
+++ b/src/core/grades/pages/courses/courses.ts
@@ -44,8 +44,17 @@ export class CoreGradesCoursesPage {
      * View loaded.
      */
     ionViewDidLoad(): void {
+        if (this.courseId) {
+            // There is an event to load, open the event in a new state.
+            this.gotoCourseGrades(this.courseId);
+        }
+
         // Get first participants.
         this.fetchData().then(() => {
+            if (!this.courseId && this.splitviewCtrl.isOn() && this.grades.length > 0) {
+                this.gotoCourseGrades(this.grades[0].courseid);
+            }
+
             // Add log in Moodle.
             return this.gradesProvider.logCoursesGradesView();
         }).finally(() => {
diff --git a/src/core/grades/providers/course-option-handler.ts b/src/core/grades/providers/course-option-handler.ts
new file mode 100644
index 000000000..d2cd4028e
--- /dev/null
+++ b/src/core/grades/providers/course-option-handler.ts
@@ -0,0 +1,92 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { NavController } from 'ionic-angular';
+import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '../../course/providers/options-delegate';
+import { CoreCourseProvider } from '../../course/providers/course';
+import { CoreGradesProvider } from './grades';
+import { CoreCoursesProvider } from '../../courses/providers/courses';
+import { CoreGradesCourseComponent } from '../components/course/course';
+
+/**
+ * Course nav handler.
+ */
+@Injectable()
+export class CoreGradesCourseOptionHandler implements CoreCourseOptionsHandler {
+    name = 'CoreGrades';
+    priority = 400;
+
+    constructor(private gradesProvider: CoreGradesProvider, private coursesProvider: CoreCoursesProvider) {}
+
+    /**
+     * Should invalidate the data to determine if the handler is enabled for a certain course.
+     *
+     * @param {number} courseId The course ID.
+     * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
+     * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
+     * @return {Promise<any>} Promise resolved when done.
+     */
+    invalidateEnabledForCourse(courseId: number, navOptions?: any, admOptions?: any): Promise<any> {
+        if (navOptions && typeof navOptions.grades != 'undefined') {
+            // No need to invalidate anything.
+            return Promise.resolve();
+        }
+
+        return this.coursesProvider.invalidateUserCourses();
+    }
+
+    /**
+     * Check if the handler is enabled on a site level.
+     *
+     * @return {boolean} Whether or not the handler is enabled on a site level.
+     */
+    isEnabled(): boolean | Promise<boolean> {
+        return true;
+    }
+
+    /**
+     * Whether or not the handler is enabled for a certain course.
+     *
+     * @param {number} courseId The course ID.
+     * @param {any} accessData Access type and data. Default, guest, ...
+     * @param {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
+     * @param {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
+     * @return {boolean|Promise<boolean>} True or promise resolved with true if enabled.
+     */
+    isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise<boolean> {
+        if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) {
+            return false; // Not enabled for guests.
+        }
+
+        if (navOptions && typeof navOptions.grades != 'undefined') {
+            return navOptions.grades;
+        }
+
+        return this.gradesProvider.isPluginEnabledForCourse(courseId);
+    }
+
+    /**
+     * Returns the data needed to render the handler.
+     *
+     * @return {CoreMainMenuHandlerData} Data needed to render the handler.
+     */
+    getDisplayData(): CoreCourseOptionsHandlerData {
+        return {
+            title: 'core.grades.grades',
+            class: 'core-grades-course-handler',
+            component: CoreGradesCourseComponent
+        };
+    }
+}
diff --git a/src/core/grades/providers/grades.ts b/src/core/grades/providers/grades.ts
index 66b5a79f6..6b9f833aa 100644
--- a/src/core/grades/providers/grades.ts
+++ b/src/core/grades/providers/grades.ts
@@ -17,6 +17,7 @@ import { CoreLoggerProvider } from '../../../providers/logger';
 import { CoreSite } from '../../../classes/site';
 import { CoreSitesProvider } from '../../../providers/sites';
 import { CoreUtilsProvider } from '../../../providers/utils/utils';
+import { CoreCoursesProvider } from '../../courses/providers/courses';
 
 /**
  * Service to provide grade functionalities.
@@ -27,10 +28,46 @@ export class CoreGradesProvider {
 
     protected logger;
 
-    constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider) {
+    constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider,
+            private coursesProvider: CoreCoursesProvider) {
         this.logger = logger.getInstance('CoreGradesProvider');
     }
 
+    /**
+     * Get cache key for grade table data WS calls.
+     *
+     * @param {number} courseId ID of the course to get the grades from.
+     * @param {number} userId   ID of the user to get the grades from.
+     * @return {string}         Cache key.
+     */
+    protected getCourseGradesCacheKey(courseId: number, userId: number): string {
+        return this.getCourseGradesPrefixCacheKey(courseId) + userId;
+    }
+
+    /**
+     * Get cache key for grade table data WS calls.
+     *
+     * @param {number} courseId     ID of the course to get the grades from.
+     * @param {number} userId       ID of the user to get the grades from.
+     * @param {number} [groupId]    ID of the group to get the grades from. Default: 0.
+     * @return {string}         Cache key.
+     */
+    protected getCourseGradesItemsCacheKey(courseId: number, userId: number, groupId: number): string {
+        groupId = groupId || 0;
+
+        return this.getCourseGradesPrefixCacheKey(courseId) + userId + ':' + groupId;
+    }
+
+    /**
+     * Get prefix cache key for grade table data WS calls.
+     *
+     * @param {number} courseId ID of the course to get the grades from.
+     * @return {string}         Cache key.
+     */
+    protected getCourseGradesPrefixCacheKey(courseId: number): string {
+        return this.ROOT_CACHE_KEY + 'items:' + courseId + ':';
+    }
+
     /**
      * Get cache key for courses grade WS calls.
      *
@@ -40,6 +77,87 @@ export class CoreGradesProvider {
         return this.ROOT_CACHE_KEY + 'coursesgrades';
     }
 
+    /**
+     * Get the grade items for a certain course.
+     *
+     * @param  {number}  courseId             ID of the course to get the grades from.
+     * @param  {number}  [userId]             ID of the user to get the grades from.
+     * @param  {number}  [groupId]            ID of the group to get the grades from. Default 0.
+     * @param  {string}  [siteId]             Site ID. If not defined, current site.
+     * @param  {boolean} [ignoreCache=false]  True if it should ignore cached data (it will always fail in offline or server down).
+     * @return {Promise<any>}                      Promise to be resolved when the grades table is retrieved.
+     */
+    getCourseGradesItems(courseId: number, userId?: number, groupId?: number, siteId?: string,
+            ignoreCache: boolean = false): Promise<any> {
+        return this.sitesProvider.getSite(siteId).then((site) => {
+            userId = userId || site.getUserId();
+            groupId = groupId || 0;
+
+            this.logger.debug(`Get grades for course '${courseId}' and user '${userId}'`);
+
+            const data = {
+                    courseid : courseId,
+                    userid   : userId,
+                    groupid  : groupId
+                },
+                preSets = {
+                    cacheKey: this.getCourseGradesItemsCacheKey(courseId, userId, groupId)
+                };
+
+            if (ignoreCache) {
+                preSets['getFromCache'] = 0;
+                preSets['emergencyCache'] = 0;
+            }
+
+            return site.read('gradereport_user_get_grade_items', data, preSets).then((grades) => {
+                if (grades && grades.usergrades && grades.usergrades[0]) {
+                    return grades.usergrades[0].gradeitems;
+                }
+
+                return Promise.reject(null);
+            });
+        });
+    }
+
+    /**
+     * Get the grades for a certain course.
+     * Using gradereport_user_get_grades_table in case is not avalaible.
+     *
+     * @param  {number}  courseId             ID of the course to get the grades from.
+     * @param  {number}  [userId]             ID of the user to get the grades from.
+     * @param  {string}  [siteId]             Site ID. If not defined, current site.
+     * @param  {boolean} [ignoreCache=false]  True if it should ignore cached data (it will always fail in offline or server down).
+     * @return {Promise<any>}                      Promise to be resolved when the grades table is retrieved.
+     */
+    getCourseGradesTable(courseId: number, userId?: number, siteId?: string, ignoreCache: boolean = false): Promise<any> {
+        return this.sitesProvider.getSite(siteId).then((site) => {
+            userId = userId || site.getUserId();
+
+            this.logger.debug(`Get grades for course '${courseId}' and user '${userId}'`);
+
+            const data = {
+                    courseid : courseId,
+                    userid   : userId
+                },
+                preSets = {
+                    cacheKey: this.getCourseGradesCacheKey(courseId, userId)
+                };
+
+            if (ignoreCache) {
+                preSets['getFromCache'] = 0;
+                preSets['emergencyCache'] = 0;
+            }
+
+            return site.read('gradereport_user_get_grades_table', data, preSets).then((table) => {
+                if (table && table.tables && table.tables[0]) {
+                    return table.tables[0];
+                }
+
+                return Promise.reject(null);
+            });
+        });
+    }
+
     /**
      * Get the grades for a certain course.
      *
@@ -64,6 +182,22 @@ export class CoreGradesProvider {
         });
     }
 
+    /**
+     * Invalidates grade table data WS calls.
+     *
+     * @param {number} courseId Course ID.
+     * @param {number} [userId]   User ID.
+     * @param {string} [siteId]   Site id (empty for current site).
+     * @return {Promise<any>}        Promise resolved when the data is invalidated.
+     */
+    invalidateCourseGradesData(courseId: number, userId?: number, siteId?: string): Promise<any> {
+        return this.sitesProvider.getSite(siteId).then((site) => {
+            userId = userId || site.getUserId();
+
+            return site.invalidateWsCacheForKey(this.getCourseGradesCacheKey(courseId, userId));
+        });
+    }
+
     /**
      * Invalidates courses grade data WS calls.
      *
@@ -96,9 +230,55 @@ export class CoreGradesProvider {
     }
 
     /**
-     * Log Courses grades view in Moodle.
+     * Returns whether or not the grade addon is enabled for a certain course.
+     *
+     * @param {number} courseId  Course ID.
+     * @param  {string} [siteId] Site ID. If not defined, current site.
+     * @return {Promisee<boolean>} Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise.
+     */
+    isPluginEnabledForCourse(courseId: number, siteId?: string): Promise<boolean> {
+        if (!courseId) {
+            return Promise.reject(null);
+        }
+
+        return this.coursesProvider.getUserCourse(courseId, true, siteId).then((course) => {
+            return !(course && typeof course.showgrades != 'undefined' && course.showgrades == 0);
+        });
+    }
+
+    /**
+     * Returns whether or not WS Grade Items is avalaible.
+     *
+     * @param  {string} [siteId] Site ID. If not defined, current site.
+     * @return {Promise<boolean>}         True if ws is avalaible, false otherwise.
+     * @since  Moodle 3.2
+     */
+    isGradeItemsAvalaible(siteId?: string): Promise<boolean> {
+        return this.sitesProvider.getSite(siteId).then((site) => {
+            return site.wsAvailable('gradereport_user_get_grade_items');
+        });
+    }
+
+    /**
+     * Log Course grades view in Moodle.
      *
      * @param  {number}  courseId Course ID.
+     * @param  {number}  userId   User ID.
+     * @return {Promise<any>}     Promise resolved when done.
+     */
+    logCourseGradesView(courseId: number, userId: number): Promise<any> {
+        userId = userId || this.sitesProvider.getCurrentSiteUserId();
+
+        return this.sitesProvider.getCurrentSite().write('gradereport_user_view_grade_report', {
+            courseid: courseId,
+            userid: userId
+        });
+    }
+
+    /**
+     * Log Courses grades view in Moodle.
+     *
+     * @param  {number}  [courseId] Course ID. If not defined, site Home ID.
      * @return {Promise<any>}     Promise resolved when done.
      */
     logCoursesGradesView(courseId?: number): Promise<any> {
diff --git a/src/core/grades/providers/helper.ts b/src/core/grades/providers/helper.ts
index b5cd905c0..a5e62a4d8 100644
--- a/src/core/grades/providers/helper.ts
+++ b/src/core/grades/providers/helper.ts
@@ -14,20 +14,130 @@
 
 import { Injectable } from '@angular/core';
 import { CoreLoggerProvider } from '../../../providers/logger';
+import { CoreSitesProvider } from '../../../providers/sites';
 import { TranslateService } from '@ngx-translate/core';
 import { CoreCoursesProvider } from '../../courses/providers/courses';
+import { CoreCourseProvider } from '../../course/providers/course';
+import { CoreGradesProvider } from './grades';
+import { CoreTextUtilsProvider } from '../../../providers/utils/text';
+import { CoreDomUtilsProvider } from '../../../providers/utils/dom';
 
 /**
- * Service that provides some features regarding users information.
+ * Service that provides some features regarding grades information.
  */
 @Injectable()
 export class CoreGradesHelperProvider {
     protected logger;
 
-    constructor(logger: CoreLoggerProvider, private coursesProvider: CoreCoursesProvider) {
+    constructor(logger: CoreLoggerProvider, private coursesProvider: CoreCoursesProvider,
+            private gradesProvider: CoreGradesProvider, private sitesProvider: CoreSitesProvider,
+            private textUtils: CoreTextUtilsProvider, private courseProvider: CoreCourseProvider,
+            private domUtils: CoreDomUtilsProvider, private translate: TranslateService) {
         this.logger = logger.getInstance('CoreGradesHelperProvider');
     }
 
+    /**
+     * Formats the response of gradereport_user_get_grades_table to be rendered.
+     *
+     * @param  {any}  table          JSON object representing a table with data.
+     * @return {any}             Formatted HTML table.
+     */
+    formatGradesTable(table: any): any {
+        const maxDepth = table.maxdepth,
+            formatted = {
+                columns: [],
+                rows: []
+            },
+            // Columns, in order.
+            columns = {
+                gradeitem: true,
+                weight: false,
+                grade: false,
+                range: false,
+                percentage: false,
+                lettergrade: false,
+                rank: false,
+                average: false,
+                feedback: false,
+                contributiontocoursetotal: false
+            };
+        formatted.rows = table.tabledata.map((row: any) => {
+            return this.getGradeRow(row);
+        }).filter((row: any) => {
+            return typeof row.gradeitem !== 'undefined';
+        });
+
+        // Get a row with some info.
+        let normalRow = formatted.rows.find((e) => {
+            return e.itemtype != 'leader' && (typeof e.grade != 'undefined' || typeof e.percentage != 'undefined');
+        });
+
+        // Decide if grades or percentage is being shown on phones.
+        if (normalRow && typeof normalRow.grade != 'undefined') {
+            columns.grade = true;
+        } else if (normalRow && typeof normalRow.percentage != 'undefined') {
+            columns.percentage = true;
+        } else {
+            normalRow = formatted.rows.find((e) => {
+                return e.itemtype != 'leader';
+            });
+            columns.grade = true;
+        }
+
+        for (const colName in columns) {
+            if (typeof normalRow[colName] != 'undefined') {
+                formatted.columns.push({
+                    name: colName,
+                    colspan: colName == 'gradeitem' ? maxDepth : 1,
+                    hiddenPhone: !columns[colName]
+                });
+            }
+        }
+
+        return formatted;
+    }
+
+    /**
+     * Get a row from the grades table.
+     *
+     * @param  {any}  tableRow JSON object representing row of grades table data.
+     * @return {any}           Formatted row object.
+     */
+    getGradeRow(tableRow: any): any {
+        const row = {};
+        for (let name in tableRow) {
+            if (typeof(tableRow[name].content) != 'undefined') {
+                let content = tableRow[name].content;
+
+                if (name == 'itemname') {
+                    this.setRowIcon(row, content);
+                    row['link'] = this.getModuleLink(content);
+                    row['rowclass'] = tableRow[name].class.indexOf('leveleven') < 0 ? 'odd' : 'even';
+                    row['rowclass'] += tableRow[name].class.indexOf('hidden') >= 0 ? ' hidden' : '';
+                    row['rowclass'] += tableRow[name].class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
+
+                    content = content.replace(/<\/span>/gi, '\n');
+                    content = this.textUtils.cleanTags(content);
+
+                    row['id'] = parseInt(tableRow[name].id.split('_')[1], 10);
+                    row['colspan'] = tableRow[name].colspan;
+                    row['rowspan'] = (tableRow['leader'] && tableRow['leader'].rowspan) || 1;
+                    name = 'gradeitem';
+                } else {
+                    content = this.textUtils.replaceNewLines(content, '<br>');
+                }
+
+                if (content == '&nbsp;') {
+                    content = '';
+                }
+
+                row[name] = content.trim();
+            }
+        }
+
+        return row;
+    }
+
     /**
      * Get course data for grades since they only have courseid.
      *
@@ -35,7 +145,7 @@ export class CoreGradesHelperProvider {
      * @return {Promise<any>}         Promise always resolved. Resolve param is the formatted grades.
      */
     getGradesCourseData(grades: any): Promise<any> {
-        // We ommit to use $mmCourses.getUserCourse for performance reasons.
+        // Using cache for performance reasons.
         return this.coursesProvider.getUserCourses(true).then((courses) => {
             const indexedCourses = {};
             courses.forEach((course) => {
@@ -52,4 +162,60 @@ export class CoreGradesHelperProvider {
         });
     }
 
+    /**
+     * Parses the image and sets it to the row.
+     *
+     * @param  {any} row  Formatted grade row object.
+     * @param  {string} text HTML where the image will be rendered.
+     * @return {any}      Row object with the image.
+     */
+    protected setRowIcon(row: any, text: string): any {
+        text = text.replace('%2F', '/').replace('%2f', '/');
+
+        if (text.indexOf('/agg_mean') > -1) {
+            row['itemtype'] = 'agg_mean';
+            row['image'] = 'assets/img/grades/agg_mean.png';
+        } else if (text.indexOf('/agg_sum') > -1) {
+            row['itemtype'] = 'agg_sum';
+            row['image'] = 'assets/img/grades/agg_sum.png';
+        } else if (text.indexOf('/outcomes') > -1 || text.indexOf('fa-tasks')  > -1) {
+            row['itemtype'] = 'outcome';
+            row['image'] = 'assets/img/grades/outcomes.png';
+        } else if (text.indexOf('i/folder') > -1 || text.indexOf('fa-folder')  > -1) {
+            row['itemtype'] = 'category';
+            row['icon'] = 'folder';
+        } else if (text.indexOf('/manual_item') > -1 || text.indexOf('fa-square-o')  > -1) {
+            row['itemtype'] = 'manual';
+            row['icon'] = 'square-outline';
+        } else if (text.indexOf('/mod/') > -1) {
+            const module = text.match(/mod\/([^\/]*)\//);
+            if (typeof module[1] != 'undefined') {
+                row['itemtype'] = 'mod';
+                row['itemmodule'] = module[1];
+                row['image'] = this.courseProvider.getModuleIconSrc(module[1]);
+            }
+        } else if (text.indexOf('src=') > -1) {
+            const src = text.match(/src="([^"]*)"/);
+            row['image'] = src[1];
+        }
+
+        return row;
+    }
+
+    /**
+     * Gets the link to the module for the selected grade.
+     *
+     * @param  {string} text HTML where the link is present.
+     * @return {string | false}      URL linking to the module.
+     */
+    protected getModuleLink(text: string): string | false {
+        const el = this.domUtils.toDom(text)[0],
+            link = el.attributes['href'] ? el.attributes['href'].value : false;
+
+        if (!link || link.indexOf('/mod/') < 0) {
+            return false;
+        }
+
+        return link;
+    }
 }
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 @@
-<ion-tabs *ngIf="loaded" #mainTabs [selectedIndex]="initialTab">
+<ion-tabs *ngIf="loaded" #mainTabs [selectedIndex]="initialTab" tabsPlacement="bottom" tabsLayout="title-hide">
     <ion-tab [enabled]="false" [show]="false" [root]="redirectPage" [rootParams]="redirectParams"></ion-tab>
     <ion-tab *ngFor="let tab of tabs" [root]="tab.page" [tabTitle]="tab.title | translate" [tabIcon]="tab.icon" class="{{tab.class}}"></ion-tab>
 </ion-tabs>
\ No newline at end of file
diff --git a/src/core/user/providers/course-option-handler.ts b/src/core/user/providers/course-option-handler.ts
index 633b5fde2..95ed8173d 100644
--- a/src/core/user/providers/course-option-handler.ts
+++ b/src/core/user/providers/course-option-handler.ts
@@ -17,7 +17,6 @@ import { NavController } from 'ionic-angular';
 import { CoreCourseOptionsHandler, CoreCourseOptionsHandlerData } from '../../course/providers/options-delegate';
 import { CoreCourseProvider } from '../../course/providers/course';
 import { CoreUserProvider } from './user';
-import { CoreLoginHelperProvider } from '../../login/providers/helper';
 import { CoreUserParticipantsComponent } from '../components/participants/participants';
 
 /**
@@ -25,10 +24,10 @@ import { CoreUserParticipantsComponent } from '../components/participants/partic
  */
 @Injectable()
 export class CoreUserParticipantsCourseOptionHandler implements CoreCourseOptionsHandler {
-    name = 'AddonParticipants';
+    name = 'CoreUserParticipants';
     priority = 600;
 
-    constructor(private userProvider: CoreUserProvider, private loginHelper: CoreLoginHelperProvider) {}
+    constructor(private userProvider: CoreUserProvider) {}
 
     /**
      * Should invalidate the data to determine if the handler is enabled for a certain course.
diff --git a/src/core/user/providers/participants-link-handler.ts b/src/core/user/providers/participants-link-handler.ts
index d2d46c695..f085458f8 100644
--- a/src/core/user/providers/participants-link-handler.ts
+++ b/src/core/user/providers/participants-link-handler.ts
@@ -23,7 +23,7 @@ import { CoreUserProvider } from './user';
  */
 @Injectable()
 export class CoreUserParticipantsLinkHandler extends CoreContentLinksHandlerBase {
-    name = 'AddonParticipants';
+    name = 'CoreUserParticipants';
     featureName = '$mmCoursesDelegate_mmaParticipants';
     pattern = /\/user\/index\.php/;
 
diff --git a/src/lang/en.json b/src/lang/en.json
index 6ca3ca4cf..cf901b5b4 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -61,7 +61,7 @@
     "download": "Download",
     "downloading": "Downloading",
     "edit": "Edit",
-    "emptysplit": "This page will appear blank if the left panel is empty or is loading.",
+    "emptysplit": "This page will appear blank if the side panel is empty or is loading.",
     "error": "Error",
     "errorchangecompletion": "An error occurred while changing the completion status. Please try again.",
     "errordeletefile": "Error deleting the file. Please try again.",
diff --git a/src/providers/sites.ts b/src/providers/sites.ts
index ef7441bd2..bcefaba14 100644
--- a/src/providers/sites.ts
+++ b/src/providers/sites.ts
@@ -664,6 +664,19 @@ export class CoreSitesProvider {
         return this.currentSite;
     }
 
+    /**
+     * Get the site home ID of the current site.
+     *
+     * @return {number} Current site home ID.
+     */
+    getCurrentSiteHomeId(): number {
+        if (this.currentSite) {
+            return this.currentSite.getSiteHomeId();
+        } else {
+            return 1;
+        }
+    }
+
     /**
      * Get current site ID.
      *
@@ -678,15 +691,15 @@ export class CoreSitesProvider {
     }
 
     /**
-     * Get the site home ID of the current site.
+     * Get current site User ID.
      *
-     * @return {number} Current site home ID.
+     * @return {number} Current site User ID.
      */
-    getCurrentSiteHomeId(): number {
+    getCurrentSiteUserId(): number {
         if (this.currentSite) {
-            return this.currentSite.getSiteHomeId();
+            return this.currentSite.getUserId();
         } else {
-            return 1;
+            return 0;
         }
     }
 
diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts
index e1a8a5c41..3578d8b9a 100644
--- a/src/providers/utils/dom.ts
+++ b/src/providers/utils/dom.ts
@@ -896,6 +896,18 @@ export class CoreDomUtilsProvider {
             (el.tagName.toLowerCase() == 'input' && this.INPUT_SUPPORT_KEYBOARD.indexOf(el.type) != -1));
     }
 
+    /**
+     * Converts HTML formatted text to DOM element.
+     * @param  {string}      text HTML text.
+     * @return {HTMLCollection}      Same text converted to HTMLCollection.
+     */
+    toDom(text: string): HTMLCollection {
+        const element = document.createElement('div');
+        element.innerHTML = text;
+
+        return element.children;
+    }
+
     /**
      * View an image in a new page or modal.
      *

From 38a9f019bb8c7fb6ee9c2e1fb1d1d4a7fbb01a72 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= <crazyserver@gmail.com>
Date: Wed, 7 Feb 2018 14:15:53 +0100
Subject: [PATCH 3/4] MOBILE-2326 grades: Course grade item page

---
 src/app/app.scss                              |   2 +
 src/core/grades/components/course/course.html |   2 +-
 src/core/grades/components/course/course.scss |  13 +-
 src/core/grades/components/course/course.ts   |  42 ++-
 src/core/grades/pages/courses/courses.ts      |   7 +-
 .../grades/pages/coursesplit/coursesplit.html |  10 +
 .../pages/coursesplit/coursesplit.module.ts   |  35 ++
 .../grades/pages/coursesplit/coursesplit.ts   |  38 +++
 src/core/grades/pages/grade/grade.html        |  72 +++++
 src/core/grades/pages/grade/grade.module.ts   |  33 ++
 src/core/grades/pages/grade/grade.ts          |  84 +++++
 src/core/grades/providers/grades.ts           |  48 ++-
 src/core/grades/providers/helper.ts           | 306 ++++++++++++++----
 src/providers/app.ts                          |   9 +
 14 files changed, 628 insertions(+), 73 deletions(-)
 create mode 100644 src/core/grades/pages/coursesplit/coursesplit.html
 create mode 100644 src/core/grades/pages/coursesplit/coursesplit.module.ts
 create mode 100644 src/core/grades/pages/coursesplit/coursesplit.ts
 create mode 100644 src/core/grades/pages/grade/grade.html
 create mode 100644 src/core/grades/pages/grade/grade.module.ts
 create mode 100644 src/core/grades/pages/grade/grade.ts

diff --git a/src/app/app.scss b/src/app/app.scss
index 30520d667..e21adbc2e 100644
--- a/src/app/app.scss
+++ b/src/app/app.scss
@@ -54,12 +54,14 @@
 @include media-breakpoint-down(md) {
   .hidden-phone {
     display: none !important;
+    opacity: 0 !important;
   }
 }
 
 @include media-breakpoint-up(md) {
   .hidden-tablet {
     display: none !important;
+    opacity: 0 !important;
   }
 }
 
diff --git a/src/core/grades/components/course/course.html b/src/core/grades/components/course/course.html
index 033744660..ca54f693d 100644
--- a/src/core/grades/components/course/course.html
+++ b/src/core/grades/components/course/course.html
@@ -19,7 +19,7 @@
                     <tr *ngFor="let row of gradesTable.rows" (click)="row.itemtype != 'category' && gotoGrade(row.id)" [class]="row.rowclass">
                         <td *ngIf="row.itemtype == 'category'" class="core-grades-table-category" [attr.rowspan]="row.rowspan">
                         </td>
-                        <th class="core-grades-table-gradeitem" [attr.colspan]="row.colspan">
+                        <th class="core-grades-table-gradeitem" [attr.colspan]="row.colspan" [class.core-split-item-selected]="gradeId == row.id">
                             <ion-icon *ngIf="row.icon" name="{{row.icon}}" item-start></ion-icon>
                             <img *ngIf="row.image" [src]="row.image" item-start/>
                             <span [innerHTML]="row.gradeitem"></span>
diff --git a/src/core/grades/components/course/course.scss b/src/core/grades/components/course/course.scss
index b02ce767b..a04232fb8 100644
--- a/src/core/grades/components/course/course.scss
+++ b/src/core/grades/components/course/course.scss
@@ -53,12 +53,12 @@ core-grades-course {
     }
 
     .odd {
-      td, th {
+      td, th, th.core-split-item-selected {
         background-color: $gray-lighter;
       }
     }
     .even {
-      td, th {
+      td, th, th.core-split-item-selected {
         background-color: $white;
       }
     }
@@ -69,4 +69,11 @@ core-grades-course {
       }
     }
   }
-}
\ No newline at end of file
+}
+
+.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
index 0985d25e6..5913ac22c 100644
--- a/src/core/grades/components/course/course.ts
+++ b/src/core/grades/components/course/course.ts
@@ -12,12 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { Component, ViewChild, Input } from '@angular/core';
+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.
@@ -31,13 +33,15 @@ export class CoreGradesCourseComponent {
 
     @Input() courseId: number;
     @Input() userId: number;
+    @Input() gradeId?: number;
 
     errorMessage: string;
     gradesLoaded = false;
     gradesTable: any;
 
     constructor(private gradesProvider: CoreGradesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams,
-        private gradesHelper: CoreGradesHelperProvider, private sitesProvider: CoreSitesProvider, private navCtrl: NavController) {
+        private gradesHelper: CoreGradesHelperProvider, private sitesProvider: CoreSitesProvider, private navCtrl: NavController,
+        private appProvider: CoreAppProvider, @Optional() private svComponent: CoreSplitViewComponent) {
     }
 
     /**
@@ -46,6 +50,11 @@ export class CoreGradesCourseComponent {
     ngOnInit(): void {
         // Get first participants.
         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(() => {
@@ -82,12 +91,37 @@ export class CoreGradesCourseComponent {
     }
 
     /**
-     * Navigate to the grades of the selected item.
+     * Navigate to the grade of the selected item.
      * @param {number} gradeId  Grade item ID where to navigate.
      */
     gotoGrade(gradeId: number): void {
         if (gradeId) {
-            this.navCtrl.push('CoreGradesGradePage', {courseId: this.courseId, userId: this.userId, gradeId: gradeId});
+            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/pages/courses/courses.ts b/src/core/grades/pages/courses/courses.ts
index 4dd345cff..fd54a58d8 100644
--- a/src/core/grades/pages/courses/courses.ts
+++ b/src/core/grades/pages/courses/courses.ts
@@ -37,7 +37,7 @@ export class CoreGradesCoursesPage {
     gradesLoaded = false;
 
     constructor(private gradesProvider: CoreGradesProvider, private domUtils: CoreDomUtilsProvider,
-        private courseHelper: CoreGradesHelperProvider) {
+        private gradesHelper: CoreGradesHelperProvider) {
     }
 
     /**
@@ -45,11 +45,10 @@ export class CoreGradesCoursesPage {
      */
     ionViewDidLoad(): void {
         if (this.courseId) {
-            // There is an event to load, open the event in a new state.
+            // There is the course to load, open the course in a new state.
             this.gotoCourseGrades(this.courseId);
         }
 
-        // Get first participants.
         this.fetchData().then(() => {
             if (!this.courseId && this.splitviewCtrl.isOn() && this.grades.length > 0) {
                 this.gotoCourseGrades(this.grades[0].courseid);
@@ -69,7 +68,7 @@ export class CoreGradesCoursesPage {
      */
     fetchData(): Promise<any> {
         return this.gradesProvider.getCoursesGrades().then((grades) => {
-            return this.courseHelper.getGradesCourseData(grades).then((grades) => {
+            return this.gradesHelper.getGradesCourseData(grades).then((grades) => {
                this.grades = grades;
             });
         }).catch((error) => {
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 @@
+<ion-header>
+    <ion-navbar>
+        <ion-title>{{ 'core.grades.grades' | translate }}</ion-title>
+    </ion-navbar>
+</ion-header>
+<core-split-view>
+    <core-grades-course class="core-avoid-header" [courseId]="courseId" [userId]="userId" [gradeId]="gradeId"></core-grades-course>
+</core-split-view>
+
+
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..57b081e99
--- /dev/null
+++ b/src/core/grades/pages/coursesplit/coursesplit.module.ts
@@ -0,0 +1,35 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicPageModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreGradesCourseSplitPage } from './coursesplit';
+import { CoreComponentsModule } from '../../../../components/components.module';
+import { CoreDirectivesModule } from '../../../../directives/directives.module';
+import { CoreGradesComponentsModule } from '../../components/components.module';
+
+@NgModule({
+    declarations: [
+        CoreGradesCourseSplitPage
+    ],
+    imports: [
+        CoreGradesComponentsModule,
+        CoreComponentsModule,
+        CoreDirectivesModule,
+        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..5f9268c9c
--- /dev/null
+++ b/src/core/grades/pages/grade/grade.html
@@ -0,0 +1,72 @@
+<ion-header>
+    <ion-navbar>
+        <ion-title>{{ 'core.grades.grade' | translate }}</ion-title>
+    </ion-navbar>
+</ion-header>
+<ion-content>
+    <ion-refresher [enabled]="gradeLoaded" (ionRefresh)="refreshGrade($event)">
+        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
+    </ion-refresher>
+    <core-loading [hideUntil]="gradeLoaded">
+        <core-empty-box *ngIf="!grade" icon="stats" [message]="errormessage"></core-empty-box>
+
+        <ion-list *ngIf="grade">
+            <a ion-item *ngIf="grade.itemname && grade.link" text-wrap detail-push [href]="grade.link" core-link captureLink="true">
+                <ion-icon *ngIf="grade.icon" name="{{grade.icon}}" item-start></ion-icon>
+                <img *ngIf="grade.image" [src]="grade.image" item-start/>
+                <h2><core-format-text [text]="grade.itemname"></core-format-text></h2>
+            </a>
+
+            <ion-item *ngIf="grade.itemname && !grade.link" text-wrap >
+                <ion-icon *ngIf="grade.icon" name="{{grade.icon}}" item-start></ion-icon>
+                <img *ngIf="grade.image" [src]="grade.image" item-start/>
+                <h2><core-format-text [text]="grade.itemname"></core-format-text></h2>
+            </ion-item>
+
+            <ion-item text-wrap *ngIf="grade.weight">
+                <h2>{{ 'core.grades.weight' | translate}}</h2>
+                <p><core-format-text [text]="grade.weight"></core-format-text></p>
+            </ion-item>
+
+            <ion-item text-wrap *ngIf="grade.grade">
+                <h2>{{ 'core.grades.grade' | translate}}</h2>
+                <p><core-format-text [text]="grade.grade"></core-format-text></p>
+            </ion-item>
+
+            <ion-item text-wrap *ngIf="grade.range">
+                <h2>{{ 'core.grades.range' | translate}}</h2>
+                <p><core-format-text [text]="grade.range"></core-format-text></p>
+            </ion-item>
+
+            <ion-item text-wrap *ngIf="grade.percentage">
+                <h2>{{ 'core.grades.percentage' | translate}}</h2>
+                <p><core-format-text [text]="grade.percentage"></core-format-text></p>
+            </ion-item>
+
+            <ion-item text-wrap *ngIf="grade.lettergrade">
+                <h2>{{ 'core.grades.lettergrade' | translate}}</h2>
+                <p><core-format-text [text]="grade.lettergrade"></core-format-text></p>
+            </ion-item>
+
+            <ion-item text-wrap *ngIf="grade.rank">
+                <h2>{{ 'core.grades.rank' | translate}}</h2>
+                <p><core-format-text [text]="grade.rank"></core-format-text></p>
+            </ion-item>
+
+            <ion-item text-wrap *ngIf="grade.average">
+                <h2>{{ 'core.grades.average' | translate}}</h2>
+                <p><core-format-text [text]="grade.average"></core-format-text></p>
+            </ion-item>
+
+            <ion-item text-wrap *ngIf="grade.feedback">
+                <h2>{{ 'core.grades.feedback' | translate}}</h2>
+                <p><core-format-text [fullTitle]="'core.grades.feedback' | translate" maxHeight="60" fullOnClick="true" [text]="grade.feedback"></core-format-text></p>
+            </ion-item>
+
+            <ion-item text-wrap *ngIf="grade.contributiontocoursetotal">
+                <h2>{{ 'core.grades.contributiontocoursetotal' | translate}}</h2>
+                <p><core-format-text [text]="grade.contributiontocoursetotal"></core-format-text></p>
+            </ion-item>
+        </ion-list>
+    </core-loading>
+</ion-content>
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..8ef99c85d
--- /dev/null
+++ b/src/core/grades/pages/grade/grade.ts
@@ -0,0 +1,84 @@
+// (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 { CoreSplitViewComponent } from '../../../../components/split-view/split-view';
+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;
+    errormessage: string;
+    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<any>} Resolved when done.
+     */
+    fetchData(): Promise<any> {
+        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');
+            this.errormessage = error || 'Grade not found';
+        });
+    }
+
+    /**
+     * 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/grades.ts b/src/core/grades/providers/grades.ts
index 6b9f833aa..f4efc010b 100644
--- a/src/core/grades/providers/grades.ts
+++ b/src/core/grades/providers/grades.ts
@@ -45,7 +45,7 @@ export class CoreGradesProvider {
     }
 
     /**
-     * Get cache key for grade table data WS calls.
+     * 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.
@@ -77,6 +77,37 @@ export class CoreGradesProvider {
         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<any>}                Promise to be resolved when the grades are retrieved.
+     */
+    getGradeItems(courseId: number, userId?: number, groupId?: number, siteId?: string, ignoreCache: boolean = false):
+            Promise<any> {
+        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.
      *
@@ -210,6 +241,21 @@ export class CoreGradesProvider {
         });
     }
 
+    /**
+     * 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<any>}     Promise resolved when the data is invalidated.
+     */
+    invalidateCourseGradesItemsData(courseId: number, userId: number, groupId: number, siteId?: string): Promise<any> {
+        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.
      *
diff --git a/src/core/grades/providers/helper.ts b/src/core/grades/providers/helper.ts
index a5e62a4d8..f0cb2d0f0 100644
--- a/src/core/grades/providers/helper.ts
+++ b/src/core/grades/providers/helper.ts
@@ -20,6 +20,7 @@ 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';
 
 /**
@@ -32,10 +33,103 @@ export class CoreGradesHelperProvider {
     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 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, '<br>');
+                }
+
+                if (content == '&nbsp;') {
+                    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, '<br>');
+                }
+
+                if (content == '&nbsp;') {
+                    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) {
+            let 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.
      *
@@ -62,7 +156,7 @@ export class CoreGradesHelperProvider {
                 contributiontocoursetotal: false
             };
         formatted.rows = table.tabledata.map((row: any) => {
-            return this.getGradeRow(row);
+            return this.formatGradeRowForTable(row);
         }).filter((row: any) => {
             return typeof row.gradeitem !== 'undefined';
         });
@@ -97,47 +191,6 @@ export class CoreGradesHelperProvider {
         return formatted;
     }
 
-    /**
-     * Get a row from the grades table.
-     *
-     * @param  {any}  tableRow JSON object representing row of grades table data.
-     * @return {any}           Formatted row object.
-     */
-    getGradeRow(tableRow: any): any {
-        const row = {};
-        for (let name in tableRow) {
-            if (typeof(tableRow[name].content) != 'undefined') {
-                let content = tableRow[name].content;
-
-                if (name == 'itemname') {
-                    this.setRowIcon(row, content);
-                    row['link'] = this.getModuleLink(content);
-                    row['rowclass'] = tableRow[name].class.indexOf('leveleven') < 0 ? 'odd' : 'even';
-                    row['rowclass'] += tableRow[name].class.indexOf('hidden') >= 0 ? ' hidden' : '';
-                    row['rowclass'] += tableRow[name].class.indexOf('dimmed_text') >= 0 ? ' dimmed_text' : '';
-
-                    content = content.replace(/<\/span>/gi, '\n');
-                    content = this.textUtils.cleanTags(content);
-
-                    row['id'] = parseInt(tableRow[name].id.split('_')[1], 10);
-                    row['colspan'] = tableRow[name].colspan;
-                    row['rowspan'] = (tableRow['leader'] && tableRow['leader'].rowspan) || 1;
-                    name = 'gradeitem';
-                } else {
-                    content = this.textUtils.replaceNewLines(content, '<br>');
-                }
-
-                if (content == '&nbsp;') {
-                    content = '';
-                }
-
-                row[name] = content.trim();
-            }
-        }
-
-        return row;
-    }
-
     /**
      * Get course data for grades since they only have courseid.
      *
@@ -162,6 +215,156 @@ export class CoreGradesHelperProvider {
         });
     }
 
+    /**
+     * 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<any>}                Promise to be resolved when the grades are retrieved.
+     */
+    getGradeItem(courseId: number, gradeId: number, userId?: number, siteId?: string, ignoreCache: boolean = false): Promise<any> {
+
+        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<any>}                Promise to be resolved when the grades are retrieved.
+     */
+    getGradeModuleItems(courseId: number, moduleId: number, userId?: number, groupId?: number, siteId?: string,
+            ignoreCache: boolean = false): Promise<any> {
+
+        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<any> {
+        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.
      *
@@ -201,21 +404,4 @@ export class CoreGradesHelperProvider {
 
         return row;
     }
-
-    /**
-     * Gets the link to the module for the selected grade.
-     *
-     * @param  {string} text HTML where the link is present.
-     * @return {string | false}      URL linking to the module.
-     */
-    protected getModuleLink(text: string): string | false {
-        const el = this.domUtils.toDom(text)[0],
-            link = el.attributes['href'] ? el.attributes['href'].value : false;
-
-        if (!link || link.indexOf('/mod/') < 0) {
-            return false;
-        }
-
-        return link;
-    }
 }
diff --git a/src/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.
      *

From 9df8e406e0fe2f902886faace2d66389cc4e5af3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= <crazyserver@gmail.com>
Date: Wed, 7 Feb 2018 15:36:10 +0100
Subject: [PATCH 4/4] MOBILE-2326 grades: Add links handlers

---
 src/core/grades/components/course/course.html |   2 +-
 src/core/grades/components/course/course.ts   |   3 -
 src/core/grades/grades.module.ts              |  32 ++++-
 src/core/grades/pages/course/course.module.ts |   4 -
 src/core/grades/pages/courses/courses.html    |   4 +-
 src/core/grades/pages/courses/courses.ts      |   2 +-
 .../pages/coursesplit/coursesplit.module.ts   |   2 -
 src/core/grades/pages/grade/grade.html        |   2 +-
 src/core/grades/pages/grade/grade.ts          |   3 -
 src/core/grades/providers/grades.ts           |   2 -
 src/core/grades/providers/helper.ts           |   8 +-
 .../grades/providers/overview-link-handler.ts |  65 ++++++++++
 src/core/grades/providers/user-handler.ts     | 120 ++++++++++++++++++
 .../grades/providers/user-link-handler.ts     |  73 +++++++++++
 14 files changed, 297 insertions(+), 25 deletions(-)
 create mode 100644 src/core/grades/providers/overview-link-handler.ts
 create mode 100644 src/core/grades/providers/user-handler.ts
 create mode 100644 src/core/grades/providers/user-link-handler.ts

diff --git a/src/core/grades/components/course/course.html b/src/core/grades/components/course/course.html
index ca54f693d..25798e609 100644
--- a/src/core/grades/components/course/course.html
+++ b/src/core/grades/components/course/course.html
@@ -3,7 +3,7 @@
         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
     </ion-refresher>
     <core-loading [hideUntil]="gradesLoaded">
-        <core-empty-box *ngIf="!gradesTable" icon="stats" [message]="errormessage">
+        <core-empty-box *ngIf="!gradesTable" icon="stats" [message]="'core.grades.nogradesreturned' | translate">
         </core-empty-box>
 
         <div *ngIf="gradesTable" class="core-grades-container">
diff --git a/src/core/grades/components/course/course.ts b/src/core/grades/components/course/course.ts
index 5913ac22c..bdbe66489 100644
--- a/src/core/grades/components/course/course.ts
+++ b/src/core/grades/components/course/course.ts
@@ -35,7 +35,6 @@ export class CoreGradesCourseComponent {
     @Input() userId: number;
     @Input() gradeId?: number;
 
-    errorMessage: string;
     gradesLoaded = false;
     gradesTable: any;
 
@@ -48,7 +47,6 @@ export class CoreGradesCourseComponent {
      * View loaded.
      */
     ngOnInit(): void {
-        // Get first participants.
         this.fetchData().then(() => {
             if (this.gradeId) {
                 // There is the grade to load.
@@ -73,7 +71,6 @@ export class CoreGradesCourseComponent {
             this.gradesTable = this.gradesHelper.formatGradesTable(table);
         }).catch((error) => {
             this.domUtils.showErrorModalDefault(error, 'Error loading grades');
-            this.errorMessage = error;
         });
     }
 
diff --git a/src/core/grades/grades.module.ts b/src/core/grades/grades.module.ts
index 8eb8b7442..44bd085fb 100644
--- a/src/core/grades/grades.module.ts
+++ b/src/core/grades/grades.module.ts
@@ -20,6 +20,14 @@ 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: [
@@ -31,13 +39,33 @@ import { CoreCourseOptionsDelegate } from '../course/providers/options-delegate'
         CoreGradesProvider,
         CoreGradesHelperProvider,
         CoreGradesMainMenuHandler,
-        CoreGradesCourseOptionHandler
+        CoreGradesCourseOptionHandler,
+        CoreGradesUserLinkHandler,
+        CoreGradesOverviewLinkHandler,
+        CoreGradesUserHandler
     ]
 })
 export class CoreGradesModule {
     constructor(mainMenuDelegate: CoreMainMenuDelegate, gradesMenuHandler: CoreGradesMainMenuHandler,
-            courseOptionHandler: CoreGradesCourseOptionHandler, courseOptionsDelegate: CoreCourseOptionsDelegate) {
+            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/pages/course/course.module.ts b/src/core/grades/pages/course/course.module.ts
index 25b0e2f34..903241e3b 100644
--- a/src/core/grades/pages/course/course.module.ts
+++ b/src/core/grades/pages/course/course.module.ts
@@ -16,8 +16,6 @@ import { NgModule } from '@angular/core';
 import { IonicPageModule } from 'ionic-angular';
 import { TranslateModule } from '@ngx-translate/core';
 import { CoreGradesCoursePage } from './course';
-import { CoreComponentsModule } from '../../../../components/components.module';
-import { CoreDirectivesModule } from '../../../../directives/directives.module';
 import { CoreGradesComponentsModule } from '../../components/components.module';
 
 @NgModule({
@@ -26,8 +24,6 @@ import { CoreGradesComponentsModule } from '../../components/components.module';
     ],
     imports: [
         CoreGradesComponentsModule,
-        CoreComponentsModule,
-        CoreDirectivesModule,
         IonicPageModule.forChild(CoreGradesCoursePage),
         TranslateModule.forChild()
     ],
diff --git a/src/core/grades/pages/courses/courses.html b/src/core/grades/pages/courses/courses.html
index 9541c0f75..f3de49188 100644
--- a/src/core/grades/pages/courses/courses.html
+++ b/src/core/grades/pages/courses/courses.html
@@ -13,8 +13,8 @@
             </core-empty-box>
 
             <ion-list *ngIf="grades && grades.length > 0">
-                <a ion-item text-wrap *ngFor="let grade of grades" [title]="grade.coursefullname" (click)="gotoCourseGrades(grade.courseid)" [class.core-split-item-selected]="grade.courseid == courseId">
-                    <h2><core-format-text [text]="grade.coursefullname"></core-format-text></h2>
+                <a ion-item text-wrap *ngFor="let grade of grades" [title]="grade.courseFullName" (click)="gotoCourseGrades(grade.courseid)" [class.core-split-item-selected]="grade.courseid == courseId">
+                    <h2><core-format-text [text]="grade.courseFullName"></core-format-text></h2>
                     <ion-badge item-end color="light">{{grade.grade}}</ion-badge>
                 </a>
             </ion-list>
diff --git a/src/core/grades/pages/courses/courses.ts b/src/core/grades/pages/courses/courses.ts
index fd54a58d8..4610fdce7 100644
--- a/src/core/grades/pages/courses/courses.ts
+++ b/src/core/grades/pages/courses/courses.ts
@@ -95,6 +95,6 @@ export class CoreGradesCoursesPage {
      */
     gotoCourseGrades(courseId: number): void {
         this.courseId = courseId;
-        this.splitviewCtrl.push('CoreGradesCoursePage', {courseId: courseId, userId: this.userId, forcephoneview: 1});
+        this.splitviewCtrl.push('CoreGradesCoursePage', {courseId: courseId, userId: this.userId});
     }
 }
diff --git a/src/core/grades/pages/coursesplit/coursesplit.module.ts b/src/core/grades/pages/coursesplit/coursesplit.module.ts
index 57b081e99..fb9bab05a 100644
--- a/src/core/grades/pages/coursesplit/coursesplit.module.ts
+++ b/src/core/grades/pages/coursesplit/coursesplit.module.ts
@@ -17,7 +17,6 @@ import { IonicPageModule } from 'ionic-angular';
 import { TranslateModule } from '@ngx-translate/core';
 import { CoreGradesCourseSplitPage } from './coursesplit';
 import { CoreComponentsModule } from '../../../../components/components.module';
-import { CoreDirectivesModule } from '../../../../directives/directives.module';
 import { CoreGradesComponentsModule } from '../../components/components.module';
 
 @NgModule({
@@ -27,7 +26,6 @@ import { CoreGradesComponentsModule } from '../../components/components.module';
     imports: [
         CoreGradesComponentsModule,
         CoreComponentsModule,
-        CoreDirectivesModule,
         IonicPageModule.forChild(CoreGradesCourseSplitPage),
         TranslateModule.forChild()
     ],
diff --git a/src/core/grades/pages/grade/grade.html b/src/core/grades/pages/grade/grade.html
index 5f9268c9c..dd9d671a1 100644
--- a/src/core/grades/pages/grade/grade.html
+++ b/src/core/grades/pages/grade/grade.html
@@ -8,7 +8,7 @@
         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
     </ion-refresher>
     <core-loading [hideUntil]="gradeLoaded">
-        <core-empty-box *ngIf="!grade" icon="stats" [message]="errormessage"></core-empty-box>
+        <core-empty-box *ngIf="!grade" icon="stats" [message]="'core.grades.nogradesreturned' | translate"></core-empty-box>
 
         <ion-list *ngIf="grade">
             <a ion-item *ngIf="grade.itemname && grade.link" text-wrap detail-push [href]="grade.link" core-link captureLink="true">
diff --git a/src/core/grades/pages/grade/grade.ts b/src/core/grades/pages/grade/grade.ts
index 8ef99c85d..181b1d6f1 100644
--- a/src/core/grades/pages/grade/grade.ts
+++ b/src/core/grades/pages/grade/grade.ts
@@ -16,7 +16,6 @@ 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 { CoreSplitViewComponent } from '../../../../components/split-view/split-view';
 import { CoreGradesHelperProvider } from '../../providers/helper';
 import { CoreSitesProvider } from '../../../../providers/sites';
 
@@ -35,7 +34,6 @@ export class CoreGradesGradePage {
     courseId: number;
     userId: number;
     gradeId: number;
-    errormessage: string;
     gradeLoaded = false;
 
     constructor(private gradesProvider: CoreGradesProvider, private domUtils: CoreDomUtilsProvider,
@@ -65,7 +63,6 @@ export class CoreGradesGradePage {
             this.grade = grade;
         }).catch((error) => {
             this.domUtils.showErrorModalDefault(error, 'Error loading grade item');
-            this.errormessage = error || 'Grade not found';
         });
     }
 
diff --git a/src/core/grades/providers/grades.ts b/src/core/grades/providers/grades.ts
index f4efc010b..21cf6afe3 100644
--- a/src/core/grades/providers/grades.ts
+++ b/src/core/grades/providers/grades.ts
@@ -16,7 +16,6 @@ import { Injectable } from '@angular/core';
 import { CoreLoggerProvider } from '../../../providers/logger';
 import { CoreSite } from '../../../classes/site';
 import { CoreSitesProvider } from '../../../providers/sites';
-import { CoreUtilsProvider } from '../../../providers/utils/utils';
 import { CoreCoursesProvider } from '../../courses/providers/courses';
 
 /**
@@ -152,7 +151,6 @@ export class CoreGradesProvider {
 
     /**
      * Get the grades for a certain course.
-     * Using gradereport_user_get_grades_table in case is not avalaible.
      *
      * @param  {number}  courseId             ID of the course to get the grades from.
      * @param  {number}  [userId]             ID of the user to get the grades from.
diff --git a/src/core/grades/providers/helper.ts b/src/core/grades/providers/helper.ts
index f0cb2d0f0..8c7a42dfc 100644
--- a/src/core/grades/providers/helper.ts
+++ b/src/core/grades/providers/helper.ts
@@ -121,7 +121,7 @@ export class CoreGradesHelperProvider {
      */
     protected formatGradeItem(item: any): any {
         for (const name in item) {
-            let index = name.indexOf('formatted');
+            const index = name.indexOf('formatted');
             if (index > 0) {
                 item[name.substr(0, index)] = item[name];
             }
@@ -194,8 +194,8 @@ export class CoreGradesHelperProvider {
     /**
      * Get course data for grades since they only have courseid.
      *
-     * @param  {Object[]} grades  Grades to get the data for.
-     * @return {Promise<any>}         Promise always resolved. Resolve param is the formatted grades.
+     * @param  {any} grades    Grades to get the data for.
+     * @return {Promise<any>}  Promise always resolved. Resolve param is the formatted grades.
      */
     getGradesCourseData(grades: any): Promise<any> {
         // Using cache for performance reasons.
@@ -207,7 +207,7 @@ export class CoreGradesHelperProvider {
 
             grades.forEach((grade) => {
                 if (typeof indexedCourses[grade.courseid] != 'undefined') {
-                    grade.coursefullname = indexedCourses[grade.courseid].fullname;
+                    grade.courseFullName = indexedCourses[grade.courseid].fullname;
                 }
             });
 
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<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
+     */
+    getActions(siteIds: string[], url: string, params: any, courseId?: number):
+            CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
+        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<boolean>} Whether the handler is enabled for the URL and site.
+     */
+    isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> {
+        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<boolean>}   Promise resolved with true if enabled, resolved with false otherwise.
+     */
+    isEnabledForUser(user: any, courseId: number, navOptions?: any, admOptions?: any): boolean | Promise<boolean> {
+        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<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
+     */
+    getActions(siteIds: string[], url: string, params: any, courseId?: number):
+            CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
+        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<boolean>} Whether the handler is enabled for the URL and site.
+     */
+    isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> {
+        if (!courseId) {
+            return false;
+        }
+
+        return this.gradesProvider.isPluginEnabledForCourse(courseId, siteId);
+    }
+}