From 02deb0f078efecfe68497bec400e48a6ca2645f2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= <crazyserver@gmail.com>
Date: Thu, 22 Mar 2018 12:43:05 +0100
Subject: [PATCH] MOBILE-2339 feedback: Add Respondents and non respondents
 page

---
 src/addon/mod/feedback/lang/en.json           |   8 +
 .../pages/nonrespondents/nonrespondents.html  |  45 ++++
 .../nonrespondents/nonrespondents.module.ts   |  35 +++
 .../pages/nonrespondents/nonrespondents.ts    | 159 ++++++++++++++
 .../pages/respondents/respondents.html        |  53 +++++
 .../pages/respondents/respondents.module.ts   |  37 ++++
 .../feedback/pages/respondents/respondents.ts | 202 ++++++++++++++++++
 src/addon/mod/feedback/providers/feedback.ts  | 118 ++++++++++
 src/addon/mod/feedback/providers/helper.ts    |  61 ++++++
 9 files changed, 718 insertions(+)
 create mode 100644 src/addon/mod/feedback/pages/nonrespondents/nonrespondents.html
 create mode 100644 src/addon/mod/feedback/pages/nonrespondents/nonrespondents.module.ts
 create mode 100644 src/addon/mod/feedback/pages/nonrespondents/nonrespondents.ts
 create mode 100644 src/addon/mod/feedback/pages/respondents/respondents.html
 create mode 100644 src/addon/mod/feedback/pages/respondents/respondents.module.ts
 create mode 100644 src/addon/mod/feedback/pages/respondents/respondents.ts

diff --git a/src/addon/mod/feedback/lang/en.json b/src/addon/mod/feedback/lang/en.json
index 113036f44..f9a3b44a2 100644
--- a/src/addon/mod/feedback/lang/en.json
+++ b/src/addon/mod/feedback/lang/en.json
@@ -1,18 +1,26 @@
 {
     "analysis": "Analysis",
     "anonymous": "Anonymous",
+    "anonymous_entries": "Anonymous entries ({{$a}})",
     "average": "Average",
     "completed_feedbacks": "Submitted answers",
     "complete_the_form": "Answer the questions...",
     "continue_the_form": "Continue the form",
     "feedbackclose": "Allow answers to",
     "feedbackopen": "Allow answers from",
+    "feedback_is_not_open": "The feedback is not open",
     "mode": "Mode",
     "non_anonymous": "User's name will be logged and shown with answers",
+    "non_anonymous_entries": "Non anonymous entries ({{$a}})",
+    "non_respondents_students": "Non respondents students ({{$a}})",
+    "not_started": "Not started",
     "overview": "Overview",
     "page_after_submit": "Completion message",
     "preview": "Preview",
     "questions": "Questions",
+    "responses": "Responses",
+    "response_nr": "Response number",
     "show_nonrespondents": "Show non-respondents",
+    "started": "Started",
     "this_feedback_is_already_submitted": "You've already completed this activity."
 }
\ No newline at end of file
diff --git a/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.html b/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.html
new file mode 100644
index 000000000..906dfbd6b
--- /dev/null
+++ b/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.html
@@ -0,0 +1,45 @@
+<ion-header>
+    <ion-navbar>
+        <ion-title>{{ 'addon.mod_feedback.responses' |translate }}</ion-title>
+    </ion-navbar>
+</ion-header>
+<ion-content>
+    <ion-refresher [enabled]="feedbackLoaded" (ionRefresh)="refreshFeedback($event)">
+        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
+    </ion-refresher>
+    <core-loading [hideUntil]="feedbackLoaded">
+        <ion-list no-margin>
+            <ion-item text-wrap *ngIf="groupInfo.separateGroups || groupInfo.visibleGroups">
+                <ion-label id="addon-feedback-groupslabel" *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ion-label>
+                <ion-label id="addon-feedback-groupslabel" *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ion-label>
+                <ion-select [(ngModel)]="selectedGroup" (ionChange)="loadAttempts(selectedGroup)" aria-labelledby="addon-feedback-groupslabel">
+                    <ion-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">{{groupOpt.name}}</ion-option>
+                </ion-select>
+            </ion-item>
+            <ion-item-divider color="light">
+                {{ 'addon.mod_feedback.non_respondents_students' | translate : {$a: total } }}
+            </ion-item-divider>
+            <ng-container *ngIf="total > 0">
+                <ion-item *ngFor="let user of users" text-wrap>
+                    <ion-avatar item-start>
+                        <img [src]="user.profileimageurl" [alt]="'core.pictureof' | translate:{$a: user.fullname}" core-external-content onError="this.src='assets/img/user-avatar.png'">
+                    </ion-avatar>
+                    <h2><core-format-text [text]="user.fullname"></core-format-text></h2>
+                    <p>
+                        <ion-badge color="success" *ngIf="user.started">
+                            {{ 'addon.mod_feedback.started' | translate}}
+                        </ion-badge>
+                        <ion-badge color="danger" *ngIf="!user.started">
+                            {{ 'addon.mod_feedback.not_started' | translate}}
+                        </ion-badge>
+                    </p>
+                </ion-item>
+            </ng-container>
+            <ion-item padding text-center *ngIf="canLoadMore">
+                <!-- Button and spinner to show more attempts. -->
+                <button ion-button block *ngIf="!loadingMore" (click)="loadAttempts()">{{ 'core.loadmore' | translate }}</button>
+                <ion-spinner *ngIf="loadingMore"></ion-spinner>
+            </ion-item>
+        </ion-list>
+    </core-loading>
+</ion-content>
diff --git a/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.module.ts b/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.module.ts
new file mode 100644
index 000000000..2197062a0
--- /dev/null
+++ b/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.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 { CoreDirectivesModule } from '@directives/directives.module';
+import { CoreComponentsModule } from '@components/components.module';
+import { AddonModFeedbackComponentsModule } from '../../components/components.module';
+import { AddonModFeedbackNonRespondentsPage } from './nonrespondents';
+
+@NgModule({
+    declarations: [
+        AddonModFeedbackNonRespondentsPage,
+    ],
+    imports: [
+        CoreDirectivesModule,
+        CoreComponentsModule,
+        AddonModFeedbackComponentsModule,
+        IonicPageModule.forChild(AddonModFeedbackNonRespondentsPage),
+        TranslateModule.forChild()
+    ],
+})
+export class AddonModFeedbackNonRespondentsPageModule {}
diff --git a/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.ts b/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.ts
new file mode 100644
index 000000000..1ca3de974
--- /dev/null
+++ b/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.ts
@@ -0,0 +1,159 @@
+// (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, NavController } from 'ionic-angular';
+import { AddonModFeedbackProvider } from '../../providers/feedback';
+import { AddonModFeedbackHelperProvider } from '../../providers/helper';
+import { CoreGroupInfo, CoreGroupsProvider } from '@providers/groups';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
+
+/**
+ * Page that displays feedback non respondents.
+ */
+@IonicPage({ segment: 'addon-mod-feedback-nonrespondents' })
+@Component({
+    selector: 'page-addon-mod-feedback-nonrespondents',
+    templateUrl: 'nonrespondents.html',
+})
+export class AddonModFeedbackNonRespondentsPage {
+
+    protected moduleId: number;
+    protected feedbackId: number;
+    protected courseId: number;
+    protected page = 0;
+
+    selectedGroup: number;
+    groupInfo: CoreGroupInfo = {
+        groups: [],
+        separateGroups: false,
+        visibleGroups: false
+    };
+
+    users = [];
+    total = 0;
+    canLoadMore = false;
+
+    feedbackLoaded = false;
+    loadingMore = false;
+
+    constructor(navParams: NavParams, protected feedbackProvider: AddonModFeedbackProvider,
+            protected groupsProvider: CoreGroupsProvider, protected domUtils: CoreDomUtilsProvider,
+            protected feedbackHelper: AddonModFeedbackHelperProvider, protected navCtrl: NavController) {
+        const module = navParams.get('module');
+        this.moduleId = module.id;
+        this.feedbackId = module.instance;
+        this.courseId = navParams.get('courseId');
+        this.selectedGroup = navParams.get('group') || 0;
+    }
+
+    /**
+     * View loaded.
+     */
+    ionViewDidLoad(): void {
+        this.fetchData();
+    }
+
+    /**
+     * Fetch all the data required for the view.
+     *
+     * @param {boolean} [refresh] Empty events array first.
+     * @return {Promise<any>} Promise resolved when done.
+     */
+    fetchData(refresh: boolean = false): Promise<any> {
+        this.page = 0;
+        this.total = 0;
+        this.users = [];
+
+        return this.groupsProvider.getActivityGroupInfo(this.moduleId).then((groupInfo) => {
+            this.groupInfo = groupInfo;
+
+            return this.loadGroupUsers(this.selectedGroup);
+        }).catch((message) => {
+            this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
+
+            if (!refresh) {
+                // Some call failed on first fetch, go back.
+                this.navCtrl.pop();
+            }
+
+            return Promise.reject(null);
+        });
+    }
+
+    /**
+     * Load Group responses.
+     *
+     * @param  {number} [groupId]   If defined it will change group if not, it will load more users for the same group.
+     * @return {Promise<any>}       Resolved with the attempts loaded.
+     */
+    protected loadGroupUsers(groupId?: number): Promise<any> {
+        if (typeof groupId == 'undefined') {
+            this.page++;
+            this.loadingMore = true;
+        } else {
+            this.selectedGroup = groupId;
+            this.page = 0;
+            this.total = 0;
+            this.users = [];
+            this.feedbackLoaded = false;
+        }
+
+        return this.feedbackHelper.getNonRespondents(this.feedbackId, this.selectedGroup, this.page).then((response) => {
+            this.total = response.total;
+
+            if (this.users.length < response.total) {
+                this.users = this.users.concat(response.users);
+            }
+
+            this.canLoadMore = this.users.length < response.total;
+
+            return response;
+        }).finally(() => {
+            this.loadingMore = false;
+            this.feedbackLoaded = true;
+        });
+    }
+
+    /**
+     * Change selected group or load more users.
+     *
+     * @param {number} [groupId] Group ID selected. If not defined, it will load more users.
+     */
+    loadAttempts(groupId?: number): void {
+        this.loadGroupUsers(groupId).catch((message) => {
+            this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
+        });
+    }
+
+    /**
+     * Refresh the attempts.
+     *
+     * @param {any} refresher Refresher.
+     */
+    refreshFeedback(refresher: any): void {
+        if (this.feedbackLoaded) {
+            const promises = [];
+
+            promises.push(this.feedbackProvider.invalidateNonRespondentsData(this.feedbackId));
+            promises.push(this.groupsProvider.invalidateActivityGroupInfo(this.moduleId));
+
+            Promise.all(promises).finally(() => {
+                return this.fetchData(true);
+            }).finally(() => {
+                refresher.complete();
+            });
+        }
+    }
+}
diff --git a/src/addon/mod/feedback/pages/respondents/respondents.html b/src/addon/mod/feedback/pages/respondents/respondents.html
new file mode 100644
index 000000000..f9e2c2dd5
--- /dev/null
+++ b/src/addon/mod/feedback/pages/respondents/respondents.html
@@ -0,0 +1,53 @@
+<ion-header>
+    <ion-navbar>
+        <ion-title>{{ 'addon.mod_feedback.responses' |translate }}</ion-title>
+    </ion-navbar>
+</ion-header>
+<core-split-view>
+    <ion-content>
+        <ion-refresher [enabled]="feedbackLoaded" (ionRefresh)="refreshFeedback($event)">
+            <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
+        </ion-refresher>
+        <core-loading [hideUntil]="feedbackLoaded">
+            <ion-list no-margin>
+                <ion-item text-wrap *ngIf="groupInfo.separateGroups || groupInfo.visibleGroups">
+                    <ion-label id="addon-feedback-groupslabel" *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ion-label>
+                    <ion-label id="addon-feedback-groupslabel" *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ion-label>
+                    <ion-select [(ngModel)]="selectedGroup" (ionChange)="loadAttempts(selectedGroup)" aria-labelledby="addon-feedback-groupslabel">
+                        <ion-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">{{groupOpt.name}}</ion-option>
+                    </ion-select>
+                </ion-item>
+                <ng-container *ngIf="responses.total > 0">
+                    <ion-item-divider color="light">
+                        {{ 'addon.mod_feedback.non_anonymous_entries' | translate : {$a: responses.total } }}
+                    </ion-item-divider>
+                    <a *ngFor="let attempt of responses.attempts" ion-item text-wrap (click)="gotoAttempt(attempt)" [class.core-split-item-selected]="attempt.id == attemptId">
+                        <ion-avatar item-start>
+                            <img [src]="attempt.profileimageurl" [alt]="'core.pictureof' | translate:{$a: attempt.fullname}" core-external-content onError="this.src='assets/img/user-avatar.png'">
+                        </ion-avatar>
+                        <h2><core-format-text [text]="attempt.fullname"></core-format-text></h2>
+                        <p *ngIf="attempt.timemodified">{{attempt.timemodified * 1000 | coreFormatDate: "LLL"}}</p>
+                    </a>
+                    <ion-item padding text-center *ngIf="responses.canLoadMore">
+                        <!-- Button and spinner to show more attempts. -->
+                        <button ion-button block *ngIf="!loadingMore" (click)="loadAttempts()">{{ 'core.loadmore' | translate }}</button>
+                        <ion-spinner *ngIf="loadingMore"></ion-spinner>
+                    </ion-item>
+                </ng-container>
+                <ng-container *ngIf="anonResponses.total > 0">
+                    <ion-item-divider color="light">
+                        {{ 'addon.mod_feedback.anonymous_entries' |translate : {$a: anonResponses.total } }}
+                    </ion-item-divider>
+                    <a *ngFor="let attempt of anonResponses.attempts" ion-item text-wrap (click)="gotoAttempt(attempt)">
+                        <h2>{{ 'addon.mod_feedback.response_nr' |translate }}: {{attempt.number}}</h2>
+                    </a>
+                    <ion-item padding text-center *ngIf="anonResponses.canLoadMore">
+                        <!-- Button and spinner to show more attempts. -->
+                        <button ion-button block *ngIf="!loadingMore" (click)="loadAttempts()">{{ 'core.loadmore' | translate }}</button>
+                        <ion-spinner *ngIf="loadingMore"></ion-spinner>
+                    </ion-item>
+                </ng-container>
+            </ion-list>
+        </core-loading>
+    </ion-content>
+</core-split-view>
diff --git a/src/addon/mod/feedback/pages/respondents/respondents.module.ts b/src/addon/mod/feedback/pages/respondents/respondents.module.ts
new file mode 100644
index 000000000..d81210084
--- /dev/null
+++ b/src/addon/mod/feedback/pages/respondents/respondents.module.ts
@@ -0,0 +1,37 @@
+// (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 { CoreDirectivesModule } from '@directives/directives.module';
+import { CoreComponentsModule } from '@components/components.module';
+import { CorePipesModule } from '@pipes/pipes.module';
+import { AddonModFeedbackComponentsModule } from '../../components/components.module';
+import { AddonModFeedbackRespondentsPage } from './respondents';
+
+@NgModule({
+    declarations: [
+        AddonModFeedbackRespondentsPage,
+    ],
+    imports: [
+        CoreDirectivesModule,
+        CoreComponentsModule,
+        CorePipesModule,
+        AddonModFeedbackComponentsModule,
+        IonicPageModule.forChild(AddonModFeedbackRespondentsPage),
+        TranslateModule.forChild()
+    ],
+})
+export class AddonModFeedbackRespondentsPageModule {}
diff --git a/src/addon/mod/feedback/pages/respondents/respondents.ts b/src/addon/mod/feedback/pages/respondents/respondents.ts
new file mode 100644
index 000000000..f281c5374
--- /dev/null
+++ b/src/addon/mod/feedback/pages/respondents/respondents.ts
@@ -0,0 +1,202 @@
+// (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, NavParams, NavController } from 'ionic-angular';
+import { AddonModFeedbackProvider } from '../../providers/feedback';
+import { AddonModFeedbackHelperProvider } from '../../providers/helper';
+import { CoreGroupInfo, CoreGroupsProvider } from '@providers/groups';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
+import { CoreSplitViewComponent } from '@components/split-view/split-view';
+
+/**
+ * Page that displays feedback respondents.
+ */
+@IonicPage({ segment: 'addon-mod-feedback-respondents' })
+@Component({
+    selector: 'page-addon-mod-feedback-respondents',
+    templateUrl: 'respondents.html',
+})
+export class AddonModFeedbackRespondentsPage {
+    @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
+
+    protected moduleId: number;
+    protected feedbackId: number;
+    protected courseId: number;
+    protected page = 0;
+
+    selectedGroup: number;
+    groupInfo: CoreGroupInfo = {
+        groups: [],
+        separateGroups: false,
+        visibleGroups: false
+    };
+
+    responses = {
+        attempts: [],
+        total: 0,
+        canLoadMore: false
+    };
+    anonResponses = {
+        attempts: [],
+        total: 0,
+        canLoadMore: false
+    };
+    feedbackLoaded = false;
+    loadingMore = false;
+    attemptId: number;
+
+    constructor(navParams: NavParams, protected feedbackProvider: AddonModFeedbackProvider,
+            protected groupsProvider: CoreGroupsProvider, protected domUtils: CoreDomUtilsProvider,
+            protected feedbackHelper: AddonModFeedbackHelperProvider, protected navCtrl: NavController) {
+        const module = navParams.get('module');
+        this.moduleId = module.id;
+        this.feedbackId = module.instance;
+        this.courseId = navParams.get('courseId');
+        this.selectedGroup = navParams.get('group') || 0;
+    }
+
+    /**
+     * View loaded.
+     */
+    ionViewDidLoad(): void {
+        this.fetchData().then(() => {
+            if (this.splitviewCtrl.isOn()) {
+                if (this.responses.attempts.length > 0) {
+                    // Take first and load it.
+                    this.gotoAttempt(this.responses.attempts[0]);
+                } else if (this.anonResponses.attempts.length > 0) {
+                    // Take first and load it.
+                    this.gotoAttempt(this.anonResponses.attempts[0]);
+                }
+            }
+        });
+    }
+
+    /**
+     * Fetch all the data required for the view.
+     *
+     * @param {boolean} [refresh] Empty events array first.
+     * @return {Promise<any>} Promise resolved when done.
+     */
+    fetchData(refresh: boolean = false): Promise<any> {
+        this.page = 0;
+        this.responses.total = 0;
+        this.responses.attempts = [];
+        this.anonResponses.total = 0;
+        this.anonResponses.attempts = [];
+
+        return this.groupsProvider.getActivityGroupInfo(this.moduleId).then((groupInfo) => {
+            this.groupInfo = groupInfo;
+
+            return this.loadGroupAttempts(this.selectedGroup);
+        }).catch((message) => {
+            this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
+
+            if (!refresh) {
+                // Some call failed on first fetch, go back.
+                this.navCtrl.pop();
+            }
+
+            return Promise.reject(null);
+        });
+    }
+
+    /**
+     * Load Group attempts.
+     *
+     * @param  {number} [groupId]   If defined it will change group if not, it will load more attempts for the same group.
+     * @return {Promise<any>}       Resolved with the attempts loaded.
+     */
+    protected loadGroupAttempts(groupId?: number): Promise<any> {
+        if (typeof groupId == 'undefined') {
+            this.page++;
+            this.loadingMore = true;
+        } else {
+            this.selectedGroup = groupId;
+            this.page = 0;
+            this.responses.total = 0;
+            this.responses.attempts = [];
+            this.anonResponses.total = 0;
+            this.anonResponses.attempts = [];
+            this.feedbackLoaded = false;
+        }
+
+        return this.feedbackHelper.getResponsesAnalysis(this.feedbackId, this.selectedGroup, this.page).then((responses) => {
+            this.responses.total = responses.totalattempts;
+            this.anonResponses.total = responses.totalanonattempts;
+
+            if (this.anonResponses.attempts.length < responses.totalanonattempts) {
+                this.anonResponses.attempts = this.anonResponses.attempts.concat(responses.anonattempts);
+            }
+            if (this.responses.attempts.length < responses.totalattempts) {
+                this.responses.attempts = this.responses.attempts.concat(responses.attempts);
+            }
+
+            this.anonResponses.canLoadMore = this.anonResponses.attempts.length < responses.totalanonattempts;
+            this.responses.canLoadMore = this.responses.attempts.length < responses.totalattempts;
+
+            return responses;
+        }).finally(() => {
+            this.loadingMore = false;
+            this.feedbackLoaded = true;
+        });
+    }
+
+    /**
+     * Navigate to a particular attempt.
+     *
+     * @param {any} attempt Attempt object to load.
+     */
+    gotoAttempt(attempt: any): void {
+        this.attemptId = attempt.id;
+        this.splitviewCtrl.push('AddonModFeedbackAttemptPage', {
+            attemptid: attempt.id,
+            attempt: attempt,
+            feedbackId: this.feedbackId,
+            moduleId: this.moduleId
+        });
+    }
+
+    /**
+     * Change selected group or load more attempts.
+     *
+     * @param {number} [groupId] Group ID selected. If not defined, it will load more attempts.
+     */
+    loadAttempts(groupId?: number): void {
+        this.loadGroupAttempts(groupId).catch((message) => {
+            this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
+        });
+    }
+
+    /**
+     * Refresh the attempts.
+     *
+     * @param {any} refresher Refresher.
+     */
+    refreshFeedback(refresher: any): void {
+        if (this.feedbackLoaded) {
+            const promises = [];
+
+            promises.push(this.feedbackProvider.invalidateResponsesAnalysisData(this.feedbackId));
+            promises.push(this.groupsProvider.invalidateActivityGroupInfo(this.moduleId));
+
+            Promise.all(promises).finally(() => {
+                return this.fetchData(true);
+            }).finally(() => {
+                refresher.complete();
+            });
+        }
+    }
+}
diff --git a/src/addon/mod/feedback/providers/feedback.ts b/src/addon/mod/feedback/providers/feedback.ts
index e954024d8..7c960b4e5 100644
--- a/src/addon/mod/feedback/providers/feedback.ts
+++ b/src/addon/mod/feedback/providers/feedback.ts
@@ -298,6 +298,96 @@ export class AddonModFeedbackProvider {
         return this.getFeedbackDataByKey(courseId, 'id', id, siteId, forceCache);
     }
 
+    /**
+     * Retrieves a list of students who didn't submit the feedback.
+     *
+     * @param   {number}    feedbackId      Feedback ID.
+     * @param   {number}    [groupId=0]     Group id, 0 means that the function will determine the user group.
+     * @param   {number}    [page=0]        The page of records to return.
+     * @param   {string}    [siteId]        Site ID. If not defined, current site.
+     * @return  {Promise<any>}              Promise resolved when the info is retrieved.
+     */
+    getNonRespondents(feedbackId: number, groupId: number = 0, page: number = 0, siteId?: string): Promise<any> {
+        return this.sitesProvider.getSite(siteId).then((site) => {
+            const params = {
+                    feedbackid: feedbackId,
+                    groupid: groupId,
+                    page: page
+                },
+                preSets = {
+                    cacheKey: this.getNonRespondentsDataCacheKey(feedbackId, groupId)
+                };
+
+            return site.read('mod_feedback_get_non_respondents', params, preSets);
+        });
+    }
+
+    /**
+     * Get cache key for non respondents feedback data WS calls.
+     *
+     * @param  {number} feedbackId  Feedback ID.
+     * @param  {number} [groupId=0] Group id, 0 means that the function will determine the user group.
+     * @return {string}             Cache key.
+     */
+    protected getNonRespondentsDataCacheKey(feedbackId: number, groupId: number = 0): string {
+        return this.getNonRespondentsDataPrefixCacheKey(feedbackId) + groupId;
+    }
+
+    /**
+     * Get prefix cache key for feedback non respondents data WS calls.
+     *
+     * @param {number} feedbackId Feedback ID.
+     * @return {string}           Cache key.
+     */
+    protected getNonRespondentsDataPrefixCacheKey(feedbackId: number): string {
+        return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':nonrespondents:';
+    }
+
+    /**
+     * Returns the feedback user responses.
+     *
+     * @param   {number}    feedbackId      Feedback ID.
+     * @param   {number}    groupId         Group id, 0 means that the function will determine the user group.
+     * @param   {number}    page            The page of records to return.
+     * @param   {string}    [siteId]        Site ID. If not defined, current site.
+     * @return  {Promise<any>}              Promise resolved when the info is retrieved.
+     */
+    getResponsesAnalysis(feedbackId: number, groupId: number, page: number, siteId?: string): Promise<any> {
+        return this.sitesProvider.getSite(siteId).then((site) => {
+            const params = {
+                    feedbackid: feedbackId,
+                    groupid: groupId || 0,
+                    page: page || 0
+                },
+                preSets = {
+                    cacheKey: this.getResponsesAnalysisDataCacheKey(feedbackId, groupId)
+                };
+
+            return site.read('mod_feedback_get_responses_analysis', params, preSets);
+        });
+    }
+
+    /**
+     * Get cache key for responses analysis feedback data WS calls.
+     *
+     * @param  {number} feedbackId  Feedback ID.
+     * @param  {number} [groupId=0] Group id, 0 means that the function will determine the user group.
+     * @return {string}             Cache key.
+     */
+    protected getResponsesAnalysisDataCacheKey(feedbackId: number, groupId: number = 0): string {
+        return this.getResponsesAnalysisDataPrefixCacheKey(feedbackId) + groupId;
+    }
+
+    /**
+     * Get prefix cache key for feedback responses analysis data WS calls.
+     *
+     * @param {number} feedbackId Feedback ID.
+     * @return {string}         Cache key.
+     */
+    protected getResponsesAnalysisDataPrefixCacheKey(feedbackId: number): string {
+        return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':responsesanalysis:';
+    }
+
     /**
      * Gets the resume page information.
      *
@@ -449,6 +539,34 @@ export class AddonModFeedbackProvider {
         return this.filepoolProvider.invalidateFilesByComponent(siteId, AddonModFeedbackProvider.COMPONENT, moduleId);
     }
 
+    /**
+     * Invalidates feedback non respondents record data.
+     *
+     * @param  {number} feedbackId   Feedback ID.
+     * @param  {string} [siteId]     Site ID. If not defined, current site.
+     * @return {Promise<any>}        Promise resolved when the data is invalidated.
+     */
+    invalidateNonRespondentsData(feedbackId: number, siteId?: string): Promise<any> {
+        return this.sitesProvider.getSite(siteId).then((site) => {
+            return site.invalidateWsCacheForKeyStartingWith(this.getNonRespondentsDataPrefixCacheKey(feedbackId));
+
+        });
+    }
+
+    /**
+     * Invalidates feedback user responses record data.
+     *
+     * @param  {number} feedbackId   Feedback ID.
+     * @param  {string} [siteId]     Site ID. If not defined, current site.
+     * @return {Promise<any>}        Promise resolved when the data is invalidated.
+     */
+    invalidateResponsesAnalysisData(feedbackId: number, siteId?: string): Promise<any> {
+        return this.sitesProvider.getSite(siteId).then((site) => {
+            return site.invalidateWsCacheForKeyStartingWith(this.getResponsesAnalysisDataPrefixCacheKey(feedbackId));
+
+        });
+    }
+
     /**
      * Invalidates launch feedback data.
      *
diff --git a/src/addon/mod/feedback/providers/helper.ts b/src/addon/mod/feedback/providers/helper.ts
index 7d83e1fd1..81a04a919 100644
--- a/src/addon/mod/feedback/providers/helper.ts
+++ b/src/addon/mod/feedback/providers/helper.ts
@@ -14,6 +14,8 @@
 
 import { Injectable } from '@angular/core';
 import { NavController } from 'ionic-angular';
+import { AddonModFeedbackProvider } from './feedback';
+import { CoreUserProvider } from '@core/user/providers/user';
 
 /**
  * Service that provides helper functions for feedbacks.
@@ -21,6 +23,9 @@ import { NavController } from 'ionic-angular';
 @Injectable()
 export class AddonModFeedbackHelperProvider {
 
+      constructor(protected feedbackProvider: AddonModFeedbackProvider, protected userProvider: CoreUserProvider) {
+    }
+
     /**
      * Check if the page we are going to open is in the history and returns the number of pages in the stack to go back.
      *
@@ -63,6 +68,62 @@ export class AddonModFeedbackHelperProvider {
         return 0;
     }
 
+    /**
+     * Retrieves a list of students who didn't submit the feedback with extra info.
+     *
+     * @param   {number}    feedbackId      Feedback ID.
+     * @param   {number}    groupId         Group id, 0 means that the function will determine the user group.
+     * @param   {number}    page            The page of records to return.
+     * @return  {Promise<any>}              Promise resolved when the info is retrieved.
+     */
+    getNonRespondents(feedbackId: number, groupId: number, page: number): Promise<any> {
+        return this.feedbackProvider.getNonRespondents(feedbackId, groupId, page).then((responses) => {
+            return this.addImageProfileToAttempts(responses.users).then((users) => {
+                responses.users = users;
+
+                return responses;
+            });
+        });
+    }
+
+    /**
+     * Returns the feedback user responses with extra info.
+     *
+     * @param   {number}    feedbackId      Feedback ID.
+     * @param   {number}    groupId         Group id, 0 means that the function will determine the user group.
+     * @param   {number}    page            The page of records to return.
+     * @return  {Promise<any>}              Promise resolved when the info is retrieved.
+     */
+    getResponsesAnalysis(feedbackId: number, groupId: number, page: number): Promise<any> {
+        return this.feedbackProvider.getResponsesAnalysis(feedbackId, groupId, page).then((responses) => {
+            return this.addImageProfileToAttempts(responses.attempts).then((attempts) => {
+                responses.attempts = attempts;
+
+                return responses;
+            });
+        });
+    }
+
+    /**
+     * Add Image profile url field on attempts
+     *
+     * @param  {any}          attempts Attempts array to get profile from.
+     * @return {Promise<any>}          Returns the same array with the profileimageurl added if found.
+     */
+    protected addImageProfileToAttempts(attempts: any): Promise<any> {
+        const promises = attempts.map((attempt) => {
+            return this.userProvider.getProfile(attempt.userid, attempt.courseid, true).then((user) => {
+                attempt.profileimageurl = user.profileimageurl;
+            }).catch(() => {
+                // Error getting profile, resolve promise without adding any extra data.
+            });
+        });
+
+        return Promise.all(promises).then(() => {
+            return attempts;
+        });
+    }
+
     /**
      * Helper function to open a feature in the app.
      *