commit
1dd8ce4432
|
@ -65,6 +65,7 @@
|
||||||
"@types/cordova-plugin-network-information": "0.0.3",
|
"@types/cordova-plugin-network-information": "0.0.3",
|
||||||
"@types/node": "^8.0.47",
|
"@types/node": "^8.0.47",
|
||||||
"@types/promise.prototype.finally": "^2.0.2",
|
"@types/promise.prototype.finally": "^2.0.2",
|
||||||
|
"chart.js": "^2.7.2",
|
||||||
"electron-builder-squirrel-windows": "^19.3.0",
|
"electron-builder-squirrel-windows": "^19.3.0",
|
||||||
"electron-windows-notifications": "^1.1.13",
|
"electron-windows-notifications": "^1.1.13",
|
||||||
"ionic-angular": "^3.9.2",
|
"ionic-angular": "^3.9.2",
|
||||||
|
|
|
@ -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 { CoreComponentsModule } from '@components/components.module';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
|
||||||
|
import { AddonModFeedbackIndexComponent } from './index/index';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModFeedbackIndexComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreComponentsModule,
|
||||||
|
CoreDirectivesModule,
|
||||||
|
CoreCourseComponentsModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AddonModFeedbackIndexComponent
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
AddonModFeedbackIndexComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AddonModFeedbackComponentsModule {}
|
|
@ -0,0 +1,174 @@
|
||||||
|
<!-- Buttons to add to the header. -->
|
||||||
|
<core-navbar-buttons end>
|
||||||
|
<core-context-menu>
|
||||||
|
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch()" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="size" [priority]="400" [content]="size" [iconDescription]="'cube'" (action)="removeFiles()" [iconAction]="'trash'"></core-context-menu-item>
|
||||||
|
</core-context-menu>
|
||||||
|
</core-navbar-buttons>
|
||||||
|
|
||||||
|
<!-- Content. -->
|
||||||
|
<core-loading [hideUntil]="loaded" class="core-loading-center">
|
||||||
|
|
||||||
|
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"></core-course-module-description>
|
||||||
|
|
||||||
|
<ng-container *ngIf="showTabs">
|
||||||
|
<core-tabs [hideUntil]="tabsReady" [selectedIndex]="firstSelectedTab">
|
||||||
|
<core-tab [title]="'addon.mod_feedback.overview' | translate" (ionSelect)="tabChanged('overview')">
|
||||||
|
<ng-template>
|
||||||
|
<ng-container *ngTemplateOutlet="tabOverview"></ng-container>
|
||||||
|
</ng-template>
|
||||||
|
</core-tab>
|
||||||
|
<core-tab [show]="access.canviewreports" [title]="'addon.mod_feedback.analysis' | translate" (ionSelect)="tabChanged('analysis')">
|
||||||
|
<ng-template>
|
||||||
|
<ng-container *ngTemplateOutlet="tabAnalysis"></ng-container>
|
||||||
|
</ng-template>
|
||||||
|
</core-tab>
|
||||||
|
|
||||||
|
<core-tab [show]="!access.canviewreports" [title]="'addon.mod_feedback.completed_feedbacks' | translate" (ionSelect)="tabChanged('analysis')">
|
||||||
|
<ng-template>
|
||||||
|
<ng-container *ngTemplateOutlet="tabAnalysis"></ng-container>
|
||||||
|
</ng-template>
|
||||||
|
</core-tab>
|
||||||
|
</core-tabs>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="!showTabs">
|
||||||
|
<ng-container *ngTemplateOutlet="tabOverview"></ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</core-loading>
|
||||||
|
|
||||||
|
|
||||||
|
<ng-template #basicInfo>
|
||||||
|
<ion-list *ngIf="(access.canedititems || access.canviewreports) && !access.isempty">
|
||||||
|
<ion-item text-wrap *ngIf="access.canedititems && (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)]="group" (ionChange)="setGroup(group)" 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 text-wrap *ngIf="access.canviewreports || access.canedititems" (click)="access.canviewreports && openFeature('Respondents')" [attr.detail-push]="access.canviewreports ? true : null">
|
||||||
|
<h2>{{ 'addon.mod_feedback.completed_feedbacks' | translate }}</h2>
|
||||||
|
<ion-badge item-end>{{feedback.completedCount}}</ion-badge>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap *ngIf="!access.isanonymous && access.canviewreports" (click)="openFeature('NonRespondents')" detail-push>
|
||||||
|
<h2>{{ 'addon.mod_feedback.show_nonrespondents' | translate }}</h2>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap *ngIf="access.canedititems">
|
||||||
|
<h2>{{ 'addon.mod_feedback.questions' | translate }}</h2>
|
||||||
|
<ion-badge item-end>{{feedback.itemsCount}}</ion-badge>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<!-- Template to render the overview. -->
|
||||||
|
<ng-template #tabOverview>
|
||||||
|
<core-loading [hideUntil]="tabsLoaded.overview">
|
||||||
|
<ng-container *ngTemplateOutlet="basicInfo"></ng-container>
|
||||||
|
|
||||||
|
<!-- Feedback done in offline but not synchronized -->
|
||||||
|
<div class="core-warning-card" icon-start *ngIf="hasOffline">
|
||||||
|
<ion-icon name="warning"></ion-icon>
|
||||||
|
{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="core-info-card" icon-start *ngIf="access.cancomplete && !access.isopen">
|
||||||
|
<ion-icon name="information-circle"></ion-icon>
|
||||||
|
{{ 'addon.mod_feedback.feedback_is_not_open' | translate }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="core-success-card" *ngIf="access.cancomplete && access.isopen && !access.cansubmit">
|
||||||
|
<ion-icon name="checkmark"></ion-icon>
|
||||||
|
{{ 'addon.mod_feedback.this_feedback_is_already_submitted' | translate }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ion-list *ngIf="access.canedititems || access.canviewreports || !access.isempty">
|
||||||
|
<ion-item text-wrap *ngIf="access.canedititems && overview.timeopen">
|
||||||
|
<h2>{{ 'addon.mod_feedback.feedbackopen' | translate }}</h2>
|
||||||
|
<p>{{overview.openTimeReadable}}</p>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap *ngIf="access.canedititems && overview.timeclose">
|
||||||
|
<h2>{{ 'addon.mod_feedback.feedbackclose' | translate }}</h2>
|
||||||
|
<p>{{overview.closeTimeReadable}}</p>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap *ngIf="access.canedititems && feedback.page_after_submit">
|
||||||
|
<h2>{{ 'addon.mod_feedback.page_after_submit' | translate }}</h2>
|
||||||
|
<core-format-text [component]="component" [componentId]="componentId" [text]=" feedback.page_after_submit"></core-format-text>
|
||||||
|
</ion-item>
|
||||||
|
<ng-container *ngIf="!access.isempty">
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_feedback.mode' | translate }}</h2>
|
||||||
|
<p *ngIf="access.isanonymous">{{ 'addon.mod_feedback.anonymous' | translate }}</p>
|
||||||
|
<p *ngIf="!access.isanonymous">{{ 'addon.mod_feedback.non_anonymous' | translate }}</p>
|
||||||
|
</ion-item>
|
||||||
|
<ion-grid>
|
||||||
|
<ion-row align-items-center>
|
||||||
|
<ion-col>
|
||||||
|
<button ion-button block outline icon-start (click)="gotoAnswerQuestions(true)">
|
||||||
|
<ion-icon name="search"></ion-icon>
|
||||||
|
{{ 'addon.mod_feedback.preview' | translate }}
|
||||||
|
</button>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col *ngIf="access.cancomplete && access.cansubmit">
|
||||||
|
<button ion-button block icon-end *ngIf="!goPage" (click)="gotoAnswerQuestions()">
|
||||||
|
{{ 'addon.mod_feedback.complete_the_form' | translate }}
|
||||||
|
<ion-icon name="arrow-forward"></ion-icon>
|
||||||
|
</button>
|
||||||
|
<button ion-button block icon-end *ngIf="goPage" (click)="gotoAnswerQuestions()">
|
||||||
|
{{ 'addon.mod_feedback.continue_the_form' | translate }}
|
||||||
|
<ion-icon name="arrow-forward"></ion-icon>
|
||||||
|
</button>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
</ion-list>
|
||||||
|
</core-loading>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<!-- Template to render the analysis. -->
|
||||||
|
<ng-template #tabAnalysis>
|
||||||
|
<core-loading [hideUntil]="tabsLoaded.analysis">
|
||||||
|
<ng-container *ngTemplateOutlet="basicInfo"></ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="access.canedititems || !access.isempty">
|
||||||
|
<div class="core-info-card" icon-start *ngIf="warning">
|
||||||
|
<ion-icon name="information-circle"></ion-icon>
|
||||||
|
{{ warning }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ion-list *ngIf="items && items.length">
|
||||||
|
<ion-item text-wrap *ngFor="let item of items" class="core-analysis">
|
||||||
|
<h2>{{item.number}}. <core-format-text [component]="component" [componentId]="componentId" [text]="item.name"></core-format-text></h2>
|
||||||
|
<p><core-format-text [component]="component" [componentId]="componentId" [text]="item.label"></core-format-text></p>
|
||||||
|
<ng-container [ngSwitch]="item.template">
|
||||||
|
<ng-container *ngSwitchCase="'numeric'">
|
||||||
|
<ul>
|
||||||
|
<li *ngFor="let data of item.data">{{ data }}</li>
|
||||||
|
</ul>
|
||||||
|
<p>{{ 'addon.mod_feedback.average' | translate }}: {{item.average | number : '1.2-2'}}</p>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'list'">
|
||||||
|
<ul>
|
||||||
|
<ng-container *ngFor="let data of item.data">
|
||||||
|
<li *ngIf="data">
|
||||||
|
<core-format-text [text]="data"></core-format-text>
|
||||||
|
</li>
|
||||||
|
</ng-container>
|
||||||
|
</ul>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'chart'">
|
||||||
|
<canvas core-chart [type]="item.chartType" [data]="item.chartData" [labels]="item.labels" height="300"></canvas>
|
||||||
|
<p *ngIf="item.average">{{ 'addon.mod_feedback.average' | translate }}: {{item.average | number : '1.2-2'}}</p>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
</ng-container>
|
||||||
|
</core-loading>
|
||||||
|
</ng-template>
|
|
@ -0,0 +1,439 @@
|
||||||
|
// (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, Input, Optional, Injector } from '@angular/core';
|
||||||
|
import { Content, NavController } from 'ionic-angular';
|
||||||
|
import { CoreGroupInfo, CoreGroupsProvider } from '@providers/groups';
|
||||||
|
import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
|
||||||
|
import { AddonModFeedbackProvider } from '../../providers/feedback';
|
||||||
|
import { AddonModFeedbackHelperProvider } from '../../providers/helper';
|
||||||
|
import { AddonModFeedbackOfflineProvider } from '../../providers/offline';
|
||||||
|
import { AddonModFeedbackSyncProvider } from '../../providers/sync';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays a feedback index page.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-mod-feedback-index',
|
||||||
|
templateUrl: 'index.html',
|
||||||
|
})
|
||||||
|
export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivityComponent {
|
||||||
|
@Input() tab = 'overview';
|
||||||
|
@Input() group = 0;
|
||||||
|
|
||||||
|
moduleName = 'feedback';
|
||||||
|
|
||||||
|
access = {
|
||||||
|
canviewreports: false,
|
||||||
|
canviewanalysis: false,
|
||||||
|
isempty: true
|
||||||
|
};
|
||||||
|
feedback: any;
|
||||||
|
goPage: number;
|
||||||
|
groupInfo: CoreGroupInfo = {
|
||||||
|
groups: [],
|
||||||
|
separateGroups: false,
|
||||||
|
visibleGroups: false
|
||||||
|
};
|
||||||
|
items: any[];
|
||||||
|
overview = {
|
||||||
|
timeopen: 0,
|
||||||
|
openTimeReadable: '',
|
||||||
|
timeclose: 0,
|
||||||
|
closeTimeReadable: ''
|
||||||
|
};
|
||||||
|
warning = '';
|
||||||
|
tabsLoaded = {
|
||||||
|
overview: false,
|
||||||
|
analysis: false
|
||||||
|
};
|
||||||
|
showTabs = false;
|
||||||
|
tabsReady = false;
|
||||||
|
firstSelectedTab: number;
|
||||||
|
|
||||||
|
protected submitObserver: any;
|
||||||
|
|
||||||
|
constructor(injector: Injector, private feedbackProvider: AddonModFeedbackProvider, @Optional() private content: Content,
|
||||||
|
private feedbackOffline: AddonModFeedbackOfflineProvider, private groupsProvider: CoreGroupsProvider,
|
||||||
|
private feedbackSync: AddonModFeedbackSyncProvider, private navCtrl: NavController,
|
||||||
|
private feedbackHelper: AddonModFeedbackHelperProvider) {
|
||||||
|
super(injector);
|
||||||
|
|
||||||
|
// Listen for form submit events.
|
||||||
|
this.submitObserver = this.eventsProvider.on(AddonModFeedbackProvider.FORM_SUBMITTED, (data) => {
|
||||||
|
if (this.feedback && data.feedbackId == this.feedback.id) {
|
||||||
|
// Go to review attempt if an attempt in this quiz was finished and synced.
|
||||||
|
this.tabsLoaded['analysis'] = false;
|
||||||
|
this.tabsLoaded['overview'] = false;
|
||||||
|
this.loaded = false;
|
||||||
|
if (data.tab != this.tab) {
|
||||||
|
this.tabChanged(data.tab);
|
||||||
|
} else {
|
||||||
|
this.loadContent(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, this.siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
|
||||||
|
this.loadContent(false, true).then(() => {
|
||||||
|
this.feedbackProvider.logView(this.feedback.id);
|
||||||
|
}).finally(() => {
|
||||||
|
this.tabsReady = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the invalidate content function.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Resolved when done.
|
||||||
|
*/
|
||||||
|
protected invalidateContent(): Promise<any> {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
promises.push(this.feedbackProvider.invalidateFeedbackData(this.courseId));
|
||||||
|
if (this.feedback) {
|
||||||
|
promises.push(this.feedbackProvider.invalidateFeedbackAccessInformationData(this.feedback.id));
|
||||||
|
promises.push(this.feedbackProvider.invalidateAnalysisData(this.feedback.id));
|
||||||
|
promises.push(this.groupsProvider.invalidateActivityAllowedGroups(this.feedback.coursemodule));
|
||||||
|
promises.push(this.groupsProvider.invalidateActivityGroupMode(this.feedback.coursemodule));
|
||||||
|
promises.push(this.feedbackProvider.invalidateResumePageData(this.feedback.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tabsLoaded['analysis'] = false;
|
||||||
|
this.tabsLoaded['overview'] = false;
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares sync event data with current data to check if refresh content is needed.
|
||||||
|
*
|
||||||
|
* @param {any} syncEventData Data receiven on sync observer.
|
||||||
|
* @return {boolean} True if refresh is needed, false otherwise.
|
||||||
|
*/
|
||||||
|
protected isRefreshSyncNeeded(syncEventData: any): boolean {
|
||||||
|
if (this.feedback && syncEventData.feedbackId == this.feedback.id) {
|
||||||
|
// Refresh the data.
|
||||||
|
this.content.scrollToTop();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download feedback contents.
|
||||||
|
*
|
||||||
|
* @param {boolean} [refresh=false] If it's refreshing content.
|
||||||
|
* @param {boolean} [sync=false] If the refresh is needs syncing.
|
||||||
|
* @param {boolean} [showErrors=false] If show errors to the user of hide them.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<any> {
|
||||||
|
return this.feedbackProvider.getFeedback(this.courseId, this.module.id).then((feedback) => {
|
||||||
|
this.feedback = feedback;
|
||||||
|
|
||||||
|
this.description = feedback.intro || feedback.description;
|
||||||
|
this.dataRetrieved.emit(feedback);
|
||||||
|
|
||||||
|
if (sync) {
|
||||||
|
// Try to synchronize the feedback.
|
||||||
|
return this.syncActivity(showErrors);
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
// Check if there are answers stored in offline.
|
||||||
|
return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id);
|
||||||
|
}).then((accessData) => {
|
||||||
|
this.access = accessData;
|
||||||
|
this.showTabs = (accessData.canviewreports || accessData.canviewanalysis) && !accessData.isempty;
|
||||||
|
|
||||||
|
this.firstSelectedTab = 0;
|
||||||
|
if (this.tab == 'analysis') {
|
||||||
|
this.firstSelectedTab = 1;
|
||||||
|
|
||||||
|
return this.fetchFeedbackAnalysisData(this.access);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.fetchFeedbackOverviewData(this.access);
|
||||||
|
}).then(() => {
|
||||||
|
// All data obtained, now fill the context menu.
|
||||||
|
this.fillContextMenu(refresh);
|
||||||
|
|
||||||
|
// Check if there are responses stored in offline.
|
||||||
|
return this.feedbackOffline.hasFeedbackOfflineData(this.feedback.id);
|
||||||
|
}).then((hasOffline) => {
|
||||||
|
this.hasOffline = hasOffline;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to get feedback overview data.
|
||||||
|
*
|
||||||
|
* @param {any} accessData Retrieved access data.
|
||||||
|
* @return {Promise<any>} Resolved when done.
|
||||||
|
*/
|
||||||
|
protected fetchFeedbackOverviewData(accessData: any): Promise<any> {
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
if (accessData.cancomplete && accessData.cansubmit && accessData.isopen) {
|
||||||
|
promises.push(this.feedbackProvider.getResumePage(this.feedback.id).then((goPage) => {
|
||||||
|
this.goPage = goPage > 0 ? goPage : false;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessData.canedititems) {
|
||||||
|
this.overview.timeopen = parseInt(this.feedback.timeopen) * 1000 || 0;
|
||||||
|
this.overview.openTimeReadable = this.overview.timeopen ?
|
||||||
|
moment(this.overview.timeopen).format('LLL') : '';
|
||||||
|
this.overview.timeclose = parseInt(this.feedback.timeclose) * 1000 || 0;
|
||||||
|
this.overview.closeTimeReadable = this.overview.timeclose ?
|
||||||
|
moment(this.overview.timeclose).format('LLL') : '';
|
||||||
|
|
||||||
|
// Get groups (only for teachers).
|
||||||
|
promises.push(this.fetchGroupInfo(this.feedback.coursemodule));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises).finally(() => {
|
||||||
|
this.tabsLoaded['overview'] = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to get feedback analysis data.
|
||||||
|
*
|
||||||
|
* @param {any} accessData Retrieved access data.
|
||||||
|
* @return {Promise<any>} Resolved when done.
|
||||||
|
*/
|
||||||
|
protected fetchFeedbackAnalysisData(accessData: any): Promise<any> {
|
||||||
|
let promise;
|
||||||
|
|
||||||
|
if (accessData.canviewanalysis) {
|
||||||
|
// Get groups (only for teachers).
|
||||||
|
promise = this.fetchGroupInfo(this.feedback.coursemodule);
|
||||||
|
} else {
|
||||||
|
this.tabChanged('overview');
|
||||||
|
promise = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.finally(() => {
|
||||||
|
this.tabsLoaded['analysis'] = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Group info data.
|
||||||
|
*
|
||||||
|
* @param {number} cmId Course module ID.
|
||||||
|
* @return {Promise<any>} Resolved when done.
|
||||||
|
*/
|
||||||
|
protected fetchGroupInfo(cmId: number): Promise<any> {
|
||||||
|
return this.groupsProvider.getActivityGroupInfo(cmId).then((groupInfo) => {
|
||||||
|
this.groupInfo = groupInfo;
|
||||||
|
|
||||||
|
return this.setGroup(this.group);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the analysis info to show the info correctly formatted.
|
||||||
|
*
|
||||||
|
* @param {any} item Item to parse.
|
||||||
|
* @return {any} Parsed item.
|
||||||
|
*/
|
||||||
|
protected parseAnalysisInfo(item: any): any {
|
||||||
|
switch (item.typ) {
|
||||||
|
case 'numeric':
|
||||||
|
item.average = item.data.reduce((prev, current) => {
|
||||||
|
return prev + parseInt(current, 10);
|
||||||
|
}, 0) / item.data.length;
|
||||||
|
item.template = 'numeric';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'info':
|
||||||
|
item.data = item.data.map((dataItem) => {
|
||||||
|
dataItem = this.textUtils.parseJSON(dataItem);
|
||||||
|
|
||||||
|
return typeof dataItem.show != 'undefined' ? dataItem.show : false;
|
||||||
|
}).filter((dataItem) => {
|
||||||
|
// Filter false entries.
|
||||||
|
return dataItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
case 'textfield':
|
||||||
|
case 'textarea':
|
||||||
|
item.template = 'list';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'multichoicerated':
|
||||||
|
case 'multichoice':
|
||||||
|
item.data = item.data.map((dataItem) => {
|
||||||
|
dataItem = this.textUtils.parseJSON(dataItem);
|
||||||
|
|
||||||
|
return typeof dataItem.answertext != 'undefined' ? dataItem : false;
|
||||||
|
}).filter((dataItem) => {
|
||||||
|
// Filter false entries.
|
||||||
|
return dataItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Format labels.
|
||||||
|
item.labels = item.data.map((dataItem) => {
|
||||||
|
dataItem.quotient = (dataItem.quotient * 100).toFixed(2);
|
||||||
|
let label = '';
|
||||||
|
|
||||||
|
if (typeof dataItem.value != 'undefined') {
|
||||||
|
label = '(' + dataItem.value + ') ';
|
||||||
|
}
|
||||||
|
label += dataItem.answertext;
|
||||||
|
label += dataItem.quotient > 0 ? ' (' + dataItem.quotient + '%)' : '';
|
||||||
|
|
||||||
|
return label;
|
||||||
|
});
|
||||||
|
|
||||||
|
item.chartData = item.data.map((dataItem) => {
|
||||||
|
return dataItem.answercount;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (item.typ == 'multichoicerated') {
|
||||||
|
item.average = item.data.reduce((prev, current) => {
|
||||||
|
return prev + parseFloat(current.avg);
|
||||||
|
}, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const subtype = item.presentation.charAt(0);
|
||||||
|
|
||||||
|
const single = subtype != 'c';
|
||||||
|
item.chartType = single ? 'doughnut' : 'bar';
|
||||||
|
|
||||||
|
item.template = 'chart';
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to go to the questions form.
|
||||||
|
*
|
||||||
|
* @param {boolean} preview Preview or edit the form.
|
||||||
|
*/
|
||||||
|
gotoAnswerQuestions(preview: boolean = false): void {
|
||||||
|
const stateParams = {
|
||||||
|
module: this.module,
|
||||||
|
moduleId: this.module.id,
|
||||||
|
courseId: this.courseId,
|
||||||
|
preview: preview
|
||||||
|
};
|
||||||
|
this.navCtrl.push('AddonModFeedbackFormPage', stateParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to link implemented features.
|
||||||
|
*
|
||||||
|
* @param {string} feature Feature to navigate.
|
||||||
|
*/
|
||||||
|
openFeature(feature: string): void {
|
||||||
|
this.feedbackHelper.openFeature(feature, this.navCtrl, this.module, this.courseId, this.group);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tab changed, fetch content again.
|
||||||
|
*
|
||||||
|
* @param {string} tabName New tab name.
|
||||||
|
*/
|
||||||
|
tabChanged(tabName: string): void {
|
||||||
|
this.tab = tabName;
|
||||||
|
|
||||||
|
if (!this.tabsLoaded[this.tab]) {
|
||||||
|
this.loadContent(false, false, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set group to see the analysis.
|
||||||
|
*
|
||||||
|
* @param {number} groupId Group ID.
|
||||||
|
* @return {Promise<any>} Resolved when done.
|
||||||
|
*/
|
||||||
|
setGroup(groupId: number): Promise<any> {
|
||||||
|
this.group = groupId;
|
||||||
|
|
||||||
|
return this.feedbackProvider.getAnalysis(this.feedback.id, groupId).then((analysis) => {
|
||||||
|
this.feedback.completedCount = analysis.completedcount;
|
||||||
|
this.feedback.itemsCount = analysis.itemscount;
|
||||||
|
|
||||||
|
if (this.tab == 'analysis') {
|
||||||
|
let num = 1;
|
||||||
|
|
||||||
|
this.items = analysis.itemsdata.map((item) => {
|
||||||
|
// Move data inside item.
|
||||||
|
item.item.data = item.data;
|
||||||
|
item = item.item;
|
||||||
|
item.number = num++;
|
||||||
|
if (item.data && item.data.length) {
|
||||||
|
return this.parseAnalysisInfo(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}).filter((item) => {
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.warning = '';
|
||||||
|
if (analysis.warnings.length) {
|
||||||
|
this.warning = analysis.warnings.find((warning) => {
|
||||||
|
return warning.warningcode == 'insufficientresponsesforthisgroup';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the sync of the activity.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected sync(): Promise<any> {
|
||||||
|
return this.feedbackSync.syncFeedback(this.feedback.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if sync has succeed from result sync data.
|
||||||
|
*
|
||||||
|
* @param {any} result Data returned on the sync function.
|
||||||
|
* @return {boolean} If suceed or not.
|
||||||
|
*/
|
||||||
|
protected hasSyncSucceed(result: any): boolean {
|
||||||
|
return result.updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
super.ngOnDestroy();
|
||||||
|
this.submitObserver && this.submitObserver.off();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
// (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 { CoreCronDelegate } from '@providers/cron';
|
||||||
|
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
|
||||||
|
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
|
||||||
|
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
|
||||||
|
import { AddonModFeedbackComponentsModule } from './components/components.module';
|
||||||
|
import { AddonModFeedbackModuleHandler } from './providers/module-handler';
|
||||||
|
import { AddonModFeedbackProvider } from './providers/feedback';
|
||||||
|
import { AddonModFeedbackLinkHandler } from './providers/link-handler';
|
||||||
|
import { AddonModFeedbackAnalysisLinkHandler } from './providers/analysis-link-handler';
|
||||||
|
import { AddonModFeedbackShowEntriesLinkHandler } from './providers/show-entries-link-handler';
|
||||||
|
import { AddonModFeedbackShowNonRespondentsLinkHandler } from './providers/show-non-respondents-link-handler';
|
||||||
|
import { AddonModFeedbackCompleteLinkHandler } from './providers/complete-link-handler';
|
||||||
|
import { AddonModFeedbackPrintLinkHandler } from './providers/print-link-handler';
|
||||||
|
import { AddonModFeedbackHelperProvider } from './providers/helper';
|
||||||
|
import { AddonModFeedbackPrefetchHandler } from './providers/prefetch-handler';
|
||||||
|
import { AddonModFeedbackSyncProvider } from './providers/sync';
|
||||||
|
import { AddonModFeedbackSyncCronHandler } from './providers/sync-cron-handler';
|
||||||
|
import { AddonModFeedbackOfflineProvider } from './providers/offline';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
AddonModFeedbackComponentsModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AddonModFeedbackProvider,
|
||||||
|
AddonModFeedbackModuleHandler,
|
||||||
|
AddonModFeedbackPrefetchHandler,
|
||||||
|
AddonModFeedbackHelperProvider,
|
||||||
|
AddonModFeedbackLinkHandler,
|
||||||
|
AddonModFeedbackAnalysisLinkHandler,
|
||||||
|
AddonModFeedbackShowEntriesLinkHandler,
|
||||||
|
AddonModFeedbackShowNonRespondentsLinkHandler,
|
||||||
|
AddonModFeedbackCompleteLinkHandler,
|
||||||
|
AddonModFeedbackPrintLinkHandler,
|
||||||
|
AddonModFeedbackSyncCronHandler,
|
||||||
|
AddonModFeedbackSyncProvider,
|
||||||
|
AddonModFeedbackOfflineProvider
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AddonModFeedbackModule {
|
||||||
|
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModFeedbackModuleHandler,
|
||||||
|
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModFeedbackPrefetchHandler,
|
||||||
|
contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModFeedbackLinkHandler,
|
||||||
|
cronDelegate: CoreCronDelegate, syncHandler: AddonModFeedbackSyncCronHandler,
|
||||||
|
analysisLinkHandler: AddonModFeedbackAnalysisLinkHandler,
|
||||||
|
showEntriesLinkHandler: AddonModFeedbackShowEntriesLinkHandler,
|
||||||
|
showNonRespondentsLinkHandler: AddonModFeedbackShowNonRespondentsLinkHandler,
|
||||||
|
completeLinkHandler: AddonModFeedbackCompleteLinkHandler,
|
||||||
|
printLinkHandler: AddonModFeedbackPrintLinkHandler) {
|
||||||
|
moduleDelegate.registerHandler(moduleHandler);
|
||||||
|
prefetchDelegate.registerHandler(prefetchHandler);
|
||||||
|
contentLinksDelegate.registerHandler(linkHandler);
|
||||||
|
contentLinksDelegate.registerHandler(analysisLinkHandler);
|
||||||
|
contentLinksDelegate.registerHandler(showEntriesLinkHandler);
|
||||||
|
contentLinksDelegate.registerHandler(showNonRespondentsLinkHandler);
|
||||||
|
contentLinksDelegate.registerHandler(completeLinkHandler);
|
||||||
|
contentLinksDelegate.registerHandler(printLinkHandler);
|
||||||
|
cronDelegate.register(syncHandler);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"analysis": "Analysis",
|
||||||
|
"anonymous": "Anonymous",
|
||||||
|
"anonymous_entries": "Anonymous entries ({{$a}})",
|
||||||
|
"average": "Average",
|
||||||
|
"captchaofflinewarning": "Feedback with CAPTCHA cannot be completed offline, or if not configured, or if the server is down.",
|
||||||
|
"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",
|
||||||
|
"feedback_submitted_offline": "This feedback has been saved to be submitted later.",
|
||||||
|
"mapcourses": "Map feedback to courses",
|
||||||
|
"mode": "Mode",
|
||||||
|
"next_page": "Next page",
|
||||||
|
"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_selected": "Not selected",
|
||||||
|
"not_started": "Not started",
|
||||||
|
"numberoutofrange": "Number out of range",
|
||||||
|
"overview": "Overview",
|
||||||
|
"page_after_submit": "Completion message",
|
||||||
|
"preview": "Preview",
|
||||||
|
"previous_page": "Previous page",
|
||||||
|
"questions": "Questions",
|
||||||
|
"responses": "Responses",
|
||||||
|
"response_nr": "Response number",
|
||||||
|
"save_entries": "Submit your answers",
|
||||||
|
"show_entries": "Show responses",
|
||||||
|
"show_nonrespondents": "Show non-respondents",
|
||||||
|
"started": "Started",
|
||||||
|
"this_feedback_is_already_submitted": "You've already completed this activity."
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar>
|
||||||
|
<ion-title><core-format-text [text]=" attempt.fullname "></core-format-text></ion-title>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<core-loading [hideUntil]="feedbackLoaded">
|
||||||
|
<ion-list no-margin>
|
||||||
|
<a *ngIf="attempt.fullname" ion-item text-wrap core-user-link [userId]="attempt.userid" [attr.aria-label]=" 'core.user.viewprofile' | translate" core-user-link [courseId]="attempt.courseid" [title]="attempt.fullname">
|
||||||
|
<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>{{attempt.fullname}}</h2>
|
||||||
|
<p *ngIf="attempt.timemodified">{{attempt.timemodified * 1000 | coreFormatDate:"LLL"}}</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ion-item text-wrap *ngIf="!attempt.fullname">
|
||||||
|
<h2>{{ 'addon.mod_feedback.response_nr' |translate }}: {{attempt.number}} ({{ 'addon.mod_feedback.anonymous' |translate }})</h2>
|
||||||
|
<p *ngIf="attempt.timemodified">{{attempt.timemodified * 1000 | coreFormatDate:"LLL"}}</p>
|
||||||
|
</ion-item >
|
||||||
|
<ng-container *ngIf="items && items.length">
|
||||||
|
<ng-container *ngFor="let item of items">
|
||||||
|
<ion-item-divider *ngIf="item.typ == 'pagebreak'" color="light"></ion-item-divider>
|
||||||
|
<ion-item text-wrap *ngIf="item.typ != 'pagebreak'" [color]="item.dependitem > 0 ? 'light' : ''">
|
||||||
|
<h2 *ngIf="item.name" [core-mark-required]="item.required">
|
||||||
|
<span *ngIf="item.itemnumber">{{item.itemnumber}}. </span><core-format-text [component]="component" [componentId]="componentId" [text]="item.name"></core-format-text>
|
||||||
|
</h2>
|
||||||
|
<p *ngIf="item.submittedValue"><core-format-text [component]="component" [componentId]="componentId" [text]=" item.submittedValue"></core-format-text></p>
|
||||||
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ion-list>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
|
@ -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 { AddonModFeedbackAttemptPage } from './attempt';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModFeedbackAttemptPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreDirectivesModule,
|
||||||
|
CoreComponentsModule,
|
||||||
|
CorePipesModule,
|
||||||
|
AddonModFeedbackComponentsModule,
|
||||||
|
IonicPageModule.forChild(AddonModFeedbackAttemptPage),
|
||||||
|
TranslateModule.forChild()
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModFeedbackAttemptPageModule {}
|
|
@ -0,0 +1,89 @@
|
||||||
|
// (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 { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays a feedback attempt review.
|
||||||
|
*/
|
||||||
|
@IonicPage({ segment: 'addon-mod-feedback-attempt' })
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-mod-feedback-attempt',
|
||||||
|
templateUrl: 'attempt.html',
|
||||||
|
})
|
||||||
|
export class AddonModFeedbackAttemptPage {
|
||||||
|
|
||||||
|
protected feedbackId: number;
|
||||||
|
|
||||||
|
attempt: any;
|
||||||
|
items: any;
|
||||||
|
componentId: number;
|
||||||
|
component = AddonModFeedbackProvider.COMPONENT;
|
||||||
|
feedbackLoaded = false;
|
||||||
|
|
||||||
|
constructor(navParams: NavParams, protected feedbackProvider: AddonModFeedbackProvider, protected navCtrl: NavController,
|
||||||
|
protected domUtils: CoreDomUtilsProvider, protected feedbackHelper: AddonModFeedbackHelperProvider,
|
||||||
|
protected textUtils: CoreTextUtilsProvider) {
|
||||||
|
this.feedbackId = navParams.get('feedbackId') || 0;
|
||||||
|
this.attempt = navParams.get('attempt') || false;
|
||||||
|
this.componentId = navParams.get('moduleId');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View loaded.
|
||||||
|
*/
|
||||||
|
ionViewDidLoad(): void {
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all the data required for the view.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
fetchData(): Promise<any> {
|
||||||
|
return this.feedbackProvider.getItems(this.feedbackId).then((items) => {
|
||||||
|
// Add responses and format items.
|
||||||
|
this.items = items.items.map((item) => {
|
||||||
|
if (item.typ == 'label') {
|
||||||
|
item.submittedValue = this.textUtils.replacePluginfileUrls(item.presentation, item.itemfiles);
|
||||||
|
} else {
|
||||||
|
for (const x in this.attempt.responses) {
|
||||||
|
if (this.attempt.responses[x].id == item.id) {
|
||||||
|
item.submittedValue = this.attempt.responses[x].printval;
|
||||||
|
delete this.attempt.responses[x];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.feedbackHelper.getItemForm(item, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
}).catch((message) => {
|
||||||
|
this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
|
||||||
|
// Some call failed on first fetch, go back.
|
||||||
|
this.navCtrl.pop();
|
||||||
|
|
||||||
|
return Promise.reject(null);
|
||||||
|
}).finally(() => {
|
||||||
|
this.feedbackLoaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar>
|
||||||
|
<ion-title><core-format-text [text]=" title "></core-format-text></ion-title>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<core-loading [hideUntil]="feedbackLoaded">
|
||||||
|
<ng-container *ngIf="items && items.length">
|
||||||
|
<ion-list no-margin>
|
||||||
|
<ion-item text-wrap>
|
||||||
|
<h2>{{ 'addon.mod_feedback.mode' | translate }}</h2>
|
||||||
|
<p *ngIf="access.isanonymous">{{ 'addon.mod_feedback.anonymous' | translate }}</p>
|
||||||
|
<p *ngIf="!access.isanonymous">{{ 'addon.mod_feedback.non_anonymous' | translate }}</p>
|
||||||
|
</ion-item>
|
||||||
|
<ng-container *ngFor="let item of items">
|
||||||
|
<ion-item-divider *ngIf="item.typ == 'pagebreak'" color="light"></ion-item-divider>
|
||||||
|
<ion-item text-wrap *ngIf="item.typ != 'pagebreak'" [color]="item.dependitem > 0 ? 'light' : ''" [class.core-danger-item]="item.isEmpty || item.hasError">
|
||||||
|
<ion-label *ngIf="item.name" [core-mark-required]="item.required" stacked>
|
||||||
|
<span *ngIf="item.itemnumber">{{item.itemnumber}}. </span>
|
||||||
|
<core-format-text [component]="component" [componentId]="componentId" [text]="item.name"></core-format-text>
|
||||||
|
</ion-label>
|
||||||
|
<div item-content class="addon-mod_feedback-form-content" *ngIf="item.template">
|
||||||
|
<ng-container [ngSwitch]="item.template">
|
||||||
|
<ng-container *ngSwitchCase="'label'">
|
||||||
|
<p><core-format-text [component]="component" [componentId]="componentId" [text]="item.presentation"></core-format-text></p>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'textfield'">
|
||||||
|
<ion-input type="text" [(ngModel)]="item.value" autocorrect="off" name="{{item.typ}}_{{item.id}}" maxlength="{{item.maxlength}}" [required]="item.required"></ion-input>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'numeric'">
|
||||||
|
<ion-input [required]="item.required" name="{{item.typ}}_{{item.id}}" type="number" [(ngModel)]="item.value"></ion-input>
|
||||||
|
<p *ngIf="item.hasError" color="error">{{ 'addon.mod_feedback.numberoutofrange' | translate }} [{{item.rangefrom}}<span *ngIf="item.rangefrom && item.rangeto">, </span>{{item.rangeto}}]</p>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'textarea'">
|
||||||
|
<ion-textarea [required]="item.required" name="{{item.typ}}_{{item.id}}" [attr.aria-multiline]="true" [(ngModel)]="item.value"></ion-textarea>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'multichoice-r'">
|
||||||
|
<ion-list radio-group [(ngModel)]="item.value" [required]="item.required" name="{{item.typ}}_{{item.id}}">
|
||||||
|
<ion-item *ngFor="let option of item.choices">
|
||||||
|
<ion-label><core-format-text [component]="component" [componentId]="componentId" [text]="option.label"></core-format-text></ion-label>
|
||||||
|
<ion-radio [value]="option.value"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
</ng-container>
|
||||||
|
<ion-list *ngSwitchCase="'multichoice-c'">
|
||||||
|
<ion-item *ngFor="let option of item.choices">
|
||||||
|
<ion-label><core-format-text [component]="component" [componentId]="componentId" [text]="option.label"></core-format-text></ion-label>
|
||||||
|
<ion-checkbox [required]="item.required" name="{{item.typ}}_{{item.id}}" [(ngModel)]="option.checked" value="option.value"></ion-checkbox>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
<ng-container *ngSwitchCase="'multichoice-d'">
|
||||||
|
<ion-select [required]="item.required" name="{{item.typ}}_{{item.id}}" [(ngModel)]="item.value">
|
||||||
|
<ion-option *ngFor="let option of item.choices" [value]="option.value"><core-format-text [component]="component" [componentId]="componentId" [text]="option.label"></core-format-text></ion-option>
|
||||||
|
</ion-select>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container *ngSwitchCase="'captcha'">
|
||||||
|
<core-recaptcha *ngIf="!preview && !offline" [publicKey]="item.captcha.recaptchapublickey" [model]="item" modelValueName="value"></core-recaptcha>
|
||||||
|
<div *ngIf="!preview && (!item.captcha || offline)" class="core-warning-card" icon-start>
|
||||||
|
<ion-icon name="warning"></ion-icon>
|
||||||
|
{{ 'addon.mod_feedback.captchaofflinewarning' | translate }}
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
<ion-grid>
|
||||||
|
<ion-row align-items-center>
|
||||||
|
<ion-col *ngIf="hasPrevPage">
|
||||||
|
<button ion-button block outline icon-start (click)="gotoPage(true)">
|
||||||
|
<ion-icon name="arrow-back"></ion-icon>
|
||||||
|
{{ 'addon.mod_feedback.previous_page' | translate }}
|
||||||
|
</button>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col *ngIf="hasNextPage">
|
||||||
|
<button ion-button block icon-end (click)="gotoPage(false)">
|
||||||
|
{{ 'addon.mod_feedback.next_page' | translate }}
|
||||||
|
<ion-icon name="arrow-forward"></ion-icon>
|
||||||
|
</button>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col *ngIf="!hasNextPage">
|
||||||
|
<button ion-button block (click)="gotoPage(false)">
|
||||||
|
{{ 'addon.mod_feedback.save_entries' | translate }}
|
||||||
|
</button>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
|
</ion-list>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<div class="core-success-card" icon-start *ngIf="completed">
|
||||||
|
<ion-icon name="checkmark"></ion-icon>
|
||||||
|
<p *ngIf="!completionPageContents && !completedOffline">{{ 'addon.mod_feedback.this_feedback_is_already_submitted' | translate }}</p>
|
||||||
|
<p *ngIf="!completionPageContents && completedOffline">{{ 'addon.mod_feedback.feedback_submitted_offline' | translate }}</p>
|
||||||
|
<p *ngIf="completionPageContents"><core-format-text [component]="component" componentId="componentId" [text]="completionPageContents"></core-format-text></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ion-grid *ngIf="completed">
|
||||||
|
<ion-row align-items-center>
|
||||||
|
<ion-col *ngIf="access.canviewanalysis">
|
||||||
|
<button ion-button block outline icon-start (click)="showAnalysis()">
|
||||||
|
<ion-icon name="stats"></ion-icon>
|
||||||
|
{{ 'addon.mod_feedback.completed_feedbacks' | translate }}
|
||||||
|
</button>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col *ngIf="hasNextPage">
|
||||||
|
<button ion-button block icon-end (click)="continue()">
|
||||||
|
{{ 'core.continue' | translate }}
|
||||||
|
<ion-icon name="arrow-forward"></ion-icon>
|
||||||
|
</button>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
|
@ -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 { AddonModFeedbackFormPage } from './form';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModFeedbackFormPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreDirectivesModule,
|
||||||
|
CoreComponentsModule,
|
||||||
|
AddonModFeedbackComponentsModule,
|
||||||
|
IonicPageModule.forChild(AddonModFeedbackFormPage),
|
||||||
|
TranslateModule.forChild()
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModFeedbackFormPageModule {}
|
|
@ -0,0 +1,15 @@
|
||||||
|
page-addon-mod-feedback-form {
|
||||||
|
.addon-mod_feedback-form-content {
|
||||||
|
align-self: self-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.item-md .addon-mod_feedback-form-content {
|
||||||
|
@include margin($item-md-padding-media-top, ($item-md-padding-end / 2), $item-md-padding-media-bottom, 0);
|
||||||
|
}
|
||||||
|
.item-ios .addon-mod_feedback-form-content {
|
||||||
|
@include margin($item-ios-padding-media-top, $item-ios-padding-start, $item-ios-padding-media-bottom, 0);
|
||||||
|
}
|
||||||
|
.item-wp .addon-mod_feedback-form-content {
|
||||||
|
@include margin($item-wp-padding-media-top, ($item-wp-padding-end / 2), $item-wp-padding-media-bottom, 0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,341 @@
|
||||||
|
// (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, OnDestroy, Optional } from '@angular/core';
|
||||||
|
import { IonicPage, NavParams, NavController, Content } from 'ionic-angular';
|
||||||
|
import { Network } from '@ionic-native/network';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { AddonModFeedbackProvider } from '../../providers/feedback';
|
||||||
|
import { AddonModFeedbackHelperProvider } from '../../providers/helper';
|
||||||
|
import { AddonModFeedbackSyncProvider } from '../../providers/sync';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { CoreAppProvider } from '@providers/app';
|
||||||
|
import { CoreEventsProvider } from '@providers/events';
|
||||||
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
|
import { CoreLoginHelperProvider } from '@core/login/providers/helper';
|
||||||
|
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays feedback form.
|
||||||
|
*/
|
||||||
|
@IonicPage({ segment: 'addon-mod-feedback-form' })
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-mod-feedback-form',
|
||||||
|
templateUrl: 'form.html',
|
||||||
|
})
|
||||||
|
export class AddonModFeedbackFormPage implements OnDestroy {
|
||||||
|
|
||||||
|
protected module: any;
|
||||||
|
protected currentPage: number;
|
||||||
|
protected submitted: any;
|
||||||
|
protected feedback;
|
||||||
|
protected siteAfterSubmit;
|
||||||
|
protected onlineObserver;
|
||||||
|
protected originalData;
|
||||||
|
protected currentSite;
|
||||||
|
protected forceLeave = false;
|
||||||
|
|
||||||
|
title: string;
|
||||||
|
preview = false;
|
||||||
|
courseId: number;
|
||||||
|
componentId: number;
|
||||||
|
completionPageContents: string;
|
||||||
|
component = AddonModFeedbackProvider.COMPONENT;
|
||||||
|
offline = false;
|
||||||
|
feedbackLoaded = false;
|
||||||
|
access: any;
|
||||||
|
items = [];
|
||||||
|
hasPrevPage = false;
|
||||||
|
hasNextPage = false;
|
||||||
|
completed = false;
|
||||||
|
completedOffline = false;
|
||||||
|
|
||||||
|
constructor(navParams: NavParams, protected feedbackProvider: AddonModFeedbackProvider, protected appProvider: CoreAppProvider,
|
||||||
|
protected utils: CoreUtilsProvider, protected domUtils: CoreDomUtilsProvider, protected navCtrl: NavController,
|
||||||
|
protected feedbackHelper: AddonModFeedbackHelperProvider, protected courseProvider: CoreCourseProvider,
|
||||||
|
protected eventsProvider: CoreEventsProvider, protected feedbackSync: AddonModFeedbackSyncProvider, network: Network,
|
||||||
|
protected translate: TranslateService, protected loginHelper: CoreLoginHelperProvider,
|
||||||
|
protected linkHelper: CoreContentLinksHelperProvider, sitesProvider: CoreSitesProvider,
|
||||||
|
@Optional() private content: Content) {
|
||||||
|
|
||||||
|
this.module = navParams.get('module');
|
||||||
|
this.courseId = navParams.get('courseId');
|
||||||
|
this.currentPage = navParams.get('page');
|
||||||
|
this.title = navParams.get('title');
|
||||||
|
this.preview = !!navParams.get('preview');
|
||||||
|
this.componentId = navParams.get('moduleId') || this.module.id;
|
||||||
|
|
||||||
|
this.currentSite = sitesProvider.getCurrentSite();
|
||||||
|
|
||||||
|
// Refresh online status when changes.
|
||||||
|
this.onlineObserver = network.onchange().subscribe((online) => {
|
||||||
|
this.offline = !online;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View loaded.
|
||||||
|
*/
|
||||||
|
ionViewDidLoad(): void {
|
||||||
|
this.fetchData().then(() => {
|
||||||
|
this.feedbackProvider.logView(this.feedback.id, true).then(() => {
|
||||||
|
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View entered.
|
||||||
|
*/
|
||||||
|
ionViewDidEnter(): void {
|
||||||
|
this.forceLeave = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we can leave the page or not.
|
||||||
|
*
|
||||||
|
* @return {boolean | Promise<void>} Resolved if we can leave it, rejected if not.
|
||||||
|
*/
|
||||||
|
ionViewCanLeave(): boolean | Promise<void> {
|
||||||
|
if (this.forceLeave) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.preview) {
|
||||||
|
const responses = this.feedbackHelper.getPageItemsResponses(this.items);
|
||||||
|
|
||||||
|
if (this.items && !this.completed && this.originalData) {
|
||||||
|
// Form submitted. Check if there is any change.
|
||||||
|
if (!this.utils.basicLeftCompare(responses, this.originalData, 3)) {
|
||||||
|
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all the data required for the view.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected fetchData(): Promise<any> {
|
||||||
|
this.offline = !this.appProvider.isOnline();
|
||||||
|
|
||||||
|
return this.feedbackProvider.getFeedback(this.courseId, this.module.id).then((feedbackData) => {
|
||||||
|
this.feedback = feedbackData;
|
||||||
|
|
||||||
|
this.title = this.feedback.name || this.title;
|
||||||
|
|
||||||
|
return this.fetchAccessData();
|
||||||
|
}).then((accessData) => {
|
||||||
|
if (!this.preview && accessData.cansubmit && !accessData.isempty) {
|
||||||
|
return typeof this.currentPage == 'undefined' ?
|
||||||
|
this.feedbackProvider.getResumePage(this.feedback.id, this.offline, true) :
|
||||||
|
Promise.resolve(this.currentPage);
|
||||||
|
} else {
|
||||||
|
this.preview = true;
|
||||||
|
|
||||||
|
return Promise.resolve(0);
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
if (!this.offline && !this.utils.isWebServiceError(error)) {
|
||||||
|
// If it fails, go offline.
|
||||||
|
this.offline = true;
|
||||||
|
|
||||||
|
return this.feedbackProvider.getResumePage(this.feedback.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
}).then((page) => {
|
||||||
|
return this.fetchFeedbackPageData(page || 0);
|
||||||
|
}).catch((message) => {
|
||||||
|
this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
|
||||||
|
this.forceLeave = true;
|
||||||
|
this.navCtrl.pop();
|
||||||
|
|
||||||
|
return Promise.reject(null);
|
||||||
|
}).finally(() => {
|
||||||
|
this.feedbackLoaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch access information.
|
||||||
|
*
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected fetchAccessData(): Promise<any> {
|
||||||
|
return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, this.offline, true).catch((error) => {
|
||||||
|
if (!this.offline && !this.utils.isWebServiceError(error)) {
|
||||||
|
// If it fails, go offline.
|
||||||
|
this.offline = true;
|
||||||
|
|
||||||
|
return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
}).then((accessData) => {
|
||||||
|
this.access = accessData;
|
||||||
|
|
||||||
|
return accessData;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fetchFeedbackPageData(page: number = 0): Promise<void> {
|
||||||
|
let promise;
|
||||||
|
this.items = [];
|
||||||
|
|
||||||
|
if (this.preview) {
|
||||||
|
promise = this.feedbackProvider.getItems(this.feedback.id);
|
||||||
|
} else {
|
||||||
|
this.currentPage = page;
|
||||||
|
|
||||||
|
promise = this.feedbackProvider.getPageItemsWithValues(this.feedback.id, page, this.offline, true).catch((error) => {
|
||||||
|
if (!this.offline && !this.utils.isWebServiceError(error)) {
|
||||||
|
// If it fails, go offline.
|
||||||
|
this.offline = true;
|
||||||
|
|
||||||
|
return this.feedbackProvider.getPageItemsWithValues(this.feedback.id, page, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
}).then((response) => {
|
||||||
|
this.hasPrevPage = !!response.hasprevpage;
|
||||||
|
this.hasNextPage = !!response.hasnextpage;
|
||||||
|
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.then((response) => {
|
||||||
|
this.items = response.items.map((itemData) => {
|
||||||
|
return this.feedbackHelper.getItemForm(itemData, this.preview);
|
||||||
|
}).filter((itemData) => {
|
||||||
|
// Filter items with errors.
|
||||||
|
return itemData;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.preview) {
|
||||||
|
const itemsCopy = this.utils.clone(this.items); // Copy the array to avoid modifications.
|
||||||
|
this.originalData = this.feedbackHelper.getPageItemsResponses(itemsCopy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to allow page navigation through the questions form.
|
||||||
|
*
|
||||||
|
* @param {boolean} goPrevious If true it will go back to the previous page, if false, it will go forward.
|
||||||
|
* @return {Promise<void>} Resolved when done.
|
||||||
|
*/
|
||||||
|
gotoPage(goPrevious: boolean): Promise<void> {
|
||||||
|
this.content && this.content.scrollToTop();
|
||||||
|
this.feedbackLoaded = false;
|
||||||
|
|
||||||
|
const responses = this.feedbackHelper.getPageItemsResponses(this.items),
|
||||||
|
formHasErrors = this.items.some((item) => {
|
||||||
|
return item.isEmpty || item.hasError;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync other pages first.
|
||||||
|
return this.feedbackSync.syncFeedback(this.feedback.id).catch(() => {
|
||||||
|
// Ignore errors.
|
||||||
|
}).then(() => {
|
||||||
|
return this.feedbackProvider.processPage(this.feedback.id, this.currentPage, responses, goPrevious, formHasErrors,
|
||||||
|
this.courseId).then((response) => {
|
||||||
|
const jumpTo = parseInt(response.jumpto, 10);
|
||||||
|
|
||||||
|
if (response.completed) {
|
||||||
|
// Form is completed, show completion message and buttons.
|
||||||
|
this.items = [];
|
||||||
|
this.completed = true;
|
||||||
|
this.completedOffline = !!response.offline;
|
||||||
|
this.completionPageContents = response.completionpagecontents;
|
||||||
|
this.siteAfterSubmit = response.siteaftersubmit;
|
||||||
|
this.submitted = true;
|
||||||
|
|
||||||
|
// Invalidate access information so user will see home page updated (continue form or completion messages).
|
||||||
|
const promises = [];
|
||||||
|
promises.push(this.feedbackProvider.invalidateFeedbackAccessInformationData(this.feedback.id));
|
||||||
|
promises.push(this.feedbackProvider.invalidateResumePageData(this.feedback.id));
|
||||||
|
|
||||||
|
return Promise.all(promises).then(() => {
|
||||||
|
return this.fetchAccessData();
|
||||||
|
});
|
||||||
|
} else if (isNaN(jumpTo) || jumpTo == this.currentPage) {
|
||||||
|
// Errors on questions, stay in page.
|
||||||
|
return Promise.resolve();
|
||||||
|
} else {
|
||||||
|
this.submitted = true;
|
||||||
|
// Invalidate access information so user will see home page updated (continue form).
|
||||||
|
this.feedbackProvider.invalidateResumePageData(this.feedback.id);
|
||||||
|
|
||||||
|
// Fetch the new page.
|
||||||
|
return this.fetchFeedbackPageData(jumpTo);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch((message) => {
|
||||||
|
this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
|
||||||
|
|
||||||
|
return Promise.reject(null);
|
||||||
|
}).finally(() => {
|
||||||
|
this.feedbackLoaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to link implemented features.
|
||||||
|
*/
|
||||||
|
showAnalysis(): void {
|
||||||
|
this.submitted = 'analysis';
|
||||||
|
this.feedbackHelper.openFeature('analysis', this.navCtrl, this.module, this.courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to go to the page after submit.
|
||||||
|
*/
|
||||||
|
continue(): void {
|
||||||
|
if (this.siteAfterSubmit) {
|
||||||
|
const modal = this.domUtils.showModalLoading();
|
||||||
|
this.linkHelper.handleLink(this.siteAfterSubmit).then((treated) => {
|
||||||
|
if (!treated) {
|
||||||
|
return this.currentSite.openInBrowserWithAutoLoginIfSameSite(this.siteAfterSubmit);
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
modal.dismiss();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Use redirect to make the course the new history root (to avoid "loops" in history).
|
||||||
|
this.loginHelper.redirect('CoreCourseSectionPage', {
|
||||||
|
course: { id: this.courseId }
|
||||||
|
}, this.currentSite.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.submitted) {
|
||||||
|
const tab = this.submitted == 'analysis' ? 'analysis' : 'overview';
|
||||||
|
// If form has been submitted, the info has been already invalidated but we should update index view.
|
||||||
|
this.eventsProvider.trigger(AddonModFeedbackProvider.FORM_SUBMITTED, {feedbackId: this.feedback.id, tab: tab});
|
||||||
|
}
|
||||||
|
this.onlineObserver && this.onlineObserver.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar>
|
||||||
|
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
|
||||||
|
|
||||||
|
<ion-buttons end>
|
||||||
|
<!-- The buttons defined by the component will be added in here. -->
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher [enabled]="feedbackComponent.loaded" (ionRefresh)="feedbackComponent.doRefresh($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
|
||||||
|
<addon-mod-feedback-index [module]="module" [courseId]="courseId" [group]="selectedGroup" [tab]="selectedTab" (dataRetrieved)="updateData($event)"></addon-mod-feedback-index>
|
||||||
|
</ion-content>
|
|
@ -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 { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { AddonModFeedbackComponentsModule } from '../../components/components.module';
|
||||||
|
import { AddonModFeedbackIndexPage } from './index';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModFeedbackIndexPage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreDirectivesModule,
|
||||||
|
AddonModFeedbackComponentsModule,
|
||||||
|
IonicPageModule.forChild(AddonModFeedbackIndexPage),
|
||||||
|
TranslateModule.forChild()
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModFeedbackIndexPageModule {}
|
|
@ -0,0 +1,52 @@
|
||||||
|
// (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 } from 'ionic-angular';
|
||||||
|
import { AddonModFeedbackIndexComponent } from '../../components/index/index';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays a feedback.
|
||||||
|
*/
|
||||||
|
@IonicPage({ segment: 'addon-mod-feedback-index' })
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-mod-feedback-index',
|
||||||
|
templateUrl: 'index.html',
|
||||||
|
})
|
||||||
|
export class AddonModFeedbackIndexPage {
|
||||||
|
@ViewChild(AddonModFeedbackIndexComponent) feedbackComponent: AddonModFeedbackIndexComponent;
|
||||||
|
|
||||||
|
title: string;
|
||||||
|
module: any;
|
||||||
|
courseId: number;
|
||||||
|
selectedTab: string;
|
||||||
|
selectedGroup: number;
|
||||||
|
|
||||||
|
constructor(navParams: NavParams) {
|
||||||
|
this.module = navParams.get('module') || {};
|
||||||
|
this.courseId = navParams.get('courseId');
|
||||||
|
this.selectedGroup = navParams.get('group') || 0;
|
||||||
|
this.selectedTab = navParams.get('tab') || 'overview';
|
||||||
|
this.title = this.module.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update some data based on the feedback instance.
|
||||||
|
*
|
||||||
|
* @param {any} feedback Feedback instance.
|
||||||
|
*/
|
||||||
|
updateData(feedback: any): void {
|
||||||
|
this.title = feedback.name || this.title;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 { Injectable } from '@angular/core';
|
||||||
|
import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
|
||||||
|
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
|
||||||
|
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
|
||||||
|
import { AddonModFeedbackProvider } from './feedback';
|
||||||
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content links handler for a feedback analysis.
|
||||||
|
* Match mod/feedback/analysis.php with a valid feedback id.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModFeedbackAnalysisLinkHandler extends CoreContentLinksHandlerBase {
|
||||||
|
name = 'AddonModFeedbackAnalysisLinkHandler';
|
||||||
|
featureName = 'CoreCourseModuleDelegate_AddonModFeedback';
|
||||||
|
pattern = /\/mod\/feedback\/analysis\.php.*([\&\?]id=\d+)/;
|
||||||
|
|
||||||
|
constructor(private linkHelper: CoreContentLinksHelperProvider, private feedbackProvider: AddonModFeedbackProvider,
|
||||||
|
private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider) {
|
||||||
|
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 modal = this.domUtils.showModalLoading(),
|
||||||
|
moduleId = params.id;
|
||||||
|
|
||||||
|
this.courseProvider.getModuleBasicInfo(moduleId, siteId).then((module) => {
|
||||||
|
const stateParams = {
|
||||||
|
module: module,
|
||||||
|
courseId: module.course,
|
||||||
|
tab: 'analysis'
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.linkHelper.goInSite(navCtrl, 'AddonModFeedbackIndexPage', stateParams, siteId);
|
||||||
|
}).finally(() => {
|
||||||
|
modal.dismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.feedbackProvider.isPluginEnabled(siteId).then((enabled) => {
|
||||||
|
if (!enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof params.id == 'undefined') {
|
||||||
|
// Cannot treat the URL.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
// (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 '@core/contentlinks/classes/base-handler';
|
||||||
|
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
|
||||||
|
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
|
||||||
|
import { AddonModFeedbackProvider } from './feedback';
|
||||||
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content links handler for feedback complete questions.
|
||||||
|
* Match mod/feedback/complete.php with a valid feedback id.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModFeedbackCompleteLinkHandler extends CoreContentLinksHandlerBase {
|
||||||
|
name = 'AddonModFeedbackCompleteLinkHandler';
|
||||||
|
featureName = 'CoreCourseModuleDelegate_AddonModFeedback';
|
||||||
|
pattern = /\/mod\/feedback\/complete\.php.*([\?\&](id|gopage)=\d+)/;
|
||||||
|
|
||||||
|
constructor(private linkHelper: CoreContentLinksHelperProvider, private feedbackProvider: AddonModFeedbackProvider,
|
||||||
|
private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider) {
|
||||||
|
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 modal = this.domUtils.showModalLoading(),
|
||||||
|
moduleId = params.id;
|
||||||
|
|
||||||
|
this.courseProvider.getModuleBasicInfo(moduleId, siteId).then((module) => {
|
||||||
|
const stateParams = {
|
||||||
|
module: module,
|
||||||
|
moduleId: module.id,
|
||||||
|
courseId: module.course
|
||||||
|
};
|
||||||
|
if (typeof params.gopage == 'undefined') {
|
||||||
|
stateParams['page'] = parseInt(params.gopage, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.linkHelper.goInSite(navCtrl, 'AddonModFeedbackFormPage', stateParams, siteId);
|
||||||
|
}).finally(() => {
|
||||||
|
modal.dismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (typeof params.id == 'undefined') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.feedbackProvider.isPluginEnabled(siteId);
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,452 @@
|
||||||
|
// (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 { AddonModFeedbackProvider } from './feedback';
|
||||||
|
import { CoreUserProvider } from '@core/user/providers/user';
|
||||||
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service that provides helper functions for feedbacks.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModFeedbackHelperProvider {
|
||||||
|
|
||||||
|
protected MODE_RESPONSETIME = 1;
|
||||||
|
protected MODE_COURSE = 2;
|
||||||
|
protected MODE_CATEGORY = 3;
|
||||||
|
|
||||||
|
constructor(protected feedbackProvider: AddonModFeedbackProvider, protected userProvider: CoreUserProvider,
|
||||||
|
protected textUtils: CoreTextUtilsProvider, protected translate: TranslateService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @param {string} pageName Name of the page we want to navigate.
|
||||||
|
* @param {number} instance Activity instance Id. I.e FeedbackId.
|
||||||
|
* @param {string} paramName Param name where to find the instance number.
|
||||||
|
* @param {string} prefix Prefix to check if we are out of the activity context.
|
||||||
|
* @param {NavController} navCtrl Nav Controller of the view.
|
||||||
|
* @return {number} Returns the number of times the history needs to go back to find the specified page.
|
||||||
|
*/
|
||||||
|
protected getActivityHistoryBackCounter(pageName: string, instance: number, paramName: string, prefix: string,
|
||||||
|
navCtrl: NavController): number {
|
||||||
|
let historyInstance, params,
|
||||||
|
backTimes = 0,
|
||||||
|
view = navCtrl.getActive();
|
||||||
|
|
||||||
|
while (!view.isFirst()) {
|
||||||
|
if (!view.name.startsWith(prefix)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
params = view.getNavParams();
|
||||||
|
|
||||||
|
historyInstance = params.get(paramName) ? params.get(paramName) : params.get('module').instance;
|
||||||
|
|
||||||
|
// Check we are not changing to another activity.
|
||||||
|
if (historyInstance && historyInstance == instance) {
|
||||||
|
backTimes++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page found.
|
||||||
|
if (view.name == pageName) {
|
||||||
|
return view.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
view = navCtrl.getPrevious(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get page items responses to be sent.
|
||||||
|
*
|
||||||
|
* @param {any[]} items Items where the values are.
|
||||||
|
* @return {any} Responses object to be sent.
|
||||||
|
*/
|
||||||
|
getPageItemsResponses(items: any[]): any {
|
||||||
|
const responses = {};
|
||||||
|
|
||||||
|
items.forEach((itemData) => {
|
||||||
|
let answered = false;
|
||||||
|
|
||||||
|
itemData.hasError = false;
|
||||||
|
|
||||||
|
if (itemData.typ == 'captcha') {
|
||||||
|
const value = itemData.value || '',
|
||||||
|
name = itemData.typ + '_' + itemData.id;
|
||||||
|
|
||||||
|
answered = !!value;
|
||||||
|
responses[name] = 1;
|
||||||
|
responses['g-recaptcha-response'] = value;
|
||||||
|
responses['recaptcha_element'] = 'dummyvalue';
|
||||||
|
|
||||||
|
if (itemData.required && !answered) {
|
||||||
|
// Check if it has any value.
|
||||||
|
itemData.isEmpty = true;
|
||||||
|
} else {
|
||||||
|
itemData.isEmpty = false;
|
||||||
|
}
|
||||||
|
} else if (itemData.hasvalue) {
|
||||||
|
let name, value;
|
||||||
|
const nameTemp = itemData.typ + '_' + itemData.id;
|
||||||
|
|
||||||
|
if (itemData.typ == 'multichoice' && itemData.subtype == 'c') {
|
||||||
|
name = nameTemp + '[0]';
|
||||||
|
responses[name] = 0;
|
||||||
|
itemData.choices.forEach((choice, index) => {
|
||||||
|
name = nameTemp + '[' + (index + 1) + ']';
|
||||||
|
value = choice.checked ? choice.value : 0;
|
||||||
|
if (!answered && value) {
|
||||||
|
answered = true;
|
||||||
|
}
|
||||||
|
responses[name] = value;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (itemData.typ == 'multichoice') {
|
||||||
|
name = nameTemp + '[0]';
|
||||||
|
} else {
|
||||||
|
name = nameTemp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemData.typ == 'multichoice' || itemData.typ == 'multichoicerated') {
|
||||||
|
value = itemData.value || 0;
|
||||||
|
} else if (itemData.typ == 'numeric') {
|
||||||
|
value = itemData.value || itemData.value == 0 ? itemData.value : '';
|
||||||
|
|
||||||
|
if (value != '') {
|
||||||
|
if ((itemData.rangefrom != '' && value < itemData.rangefrom) ||
|
||||||
|
(itemData.rangeto != '' && value > itemData.rangeto)) {
|
||||||
|
itemData.hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = itemData.value || itemData.value == 0 ? itemData.value : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
answered = !!value;
|
||||||
|
responses[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemData.required && !answered) {
|
||||||
|
// Check if it has any value.
|
||||||
|
itemData.isEmpty = true;
|
||||||
|
} else {
|
||||||
|
itemData.isEmpty = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* @param {string} feature Name of the feature to open.
|
||||||
|
* @param {NavController} navCtrl NavController.
|
||||||
|
* @param {any} module Course module activity object.
|
||||||
|
* @param {number} courseId Course Id.
|
||||||
|
* @param {number} [group=0] Course module activity object.
|
||||||
|
* @return {Promise<void>} Resolved when navigation animation is done.
|
||||||
|
*/
|
||||||
|
openFeature(feature: string, navCtrl: NavController, module: any, courseId: number, group: number = 0): Promise<void> {
|
||||||
|
const pageName = feature && feature != 'analysis' ? 'AddonModFeedback' + feature + 'Page' : 'AddonModFeedbackIndexPage';
|
||||||
|
let backTimes = 0;
|
||||||
|
|
||||||
|
const stateParams = {
|
||||||
|
module: module,
|
||||||
|
moduleId: module.id,
|
||||||
|
courseId: courseId,
|
||||||
|
feedbackId: module.instance,
|
||||||
|
group: group
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only check history if navigating through tabs.
|
||||||
|
if (pageName == 'AddonModFeedbackIndexPage') {
|
||||||
|
stateParams['tab'] = feature == 'analysis' ? 'analysis' : 'overview';
|
||||||
|
backTimes = this.getActivityHistoryBackCounter(pageName, module.instance, 'feedbackId', 'AddonModFeedback', navCtrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backTimes > 0) {
|
||||||
|
// Go back X times until the the page we want to reach.
|
||||||
|
return navCtrl.remove(navCtrl.getActive().index, backTimes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not found, open new state.
|
||||||
|
return navCtrl.push(pageName, stateParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper funtion for item type Label.
|
||||||
|
*
|
||||||
|
* @param {any} item Item to process.
|
||||||
|
* @return {any} Item processed to show form.
|
||||||
|
*/
|
||||||
|
protected getItemFormLabel(item: any): any {
|
||||||
|
item.template = 'label';
|
||||||
|
item.name = '';
|
||||||
|
item.presentation = this.textUtils.replacePluginfileUrls(item.presentation, item.itemfiles);
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper funtion for item type Info.
|
||||||
|
*
|
||||||
|
* @param {any} item Item to process.
|
||||||
|
* @return {any} Item processed to show form.
|
||||||
|
*/
|
||||||
|
protected getItemFormInfo(item: any): any {
|
||||||
|
item.template = 'label';
|
||||||
|
|
||||||
|
const type = parseInt(item.presentation, 10);
|
||||||
|
|
||||||
|
if (type == this.MODE_COURSE || type == this.MODE_CATEGORY) {
|
||||||
|
item.presentation = item.otherdata;
|
||||||
|
item.value = typeof item.rawValue != 'undefined' ? item.rawValue : item.otherdata;
|
||||||
|
} else if (type == this.MODE_RESPONSETIME) {
|
||||||
|
item.value = '__CURRENT__TIMESTAMP__';
|
||||||
|
const tempValue = typeof item.rawValue != 'undefined' ? item.rawValue * 1000 : new Date().getTime();
|
||||||
|
item.presentation = moment(tempValue).format('LLL');
|
||||||
|
} else {
|
||||||
|
// Errors on item, return false.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper funtion for item type Numeric.
|
||||||
|
*
|
||||||
|
* @param {any} item Item to process.
|
||||||
|
* @return {any} Item processed to show form.
|
||||||
|
*/
|
||||||
|
protected getItemFormNumeric(item: any): any {
|
||||||
|
item.template = 'numeric';
|
||||||
|
|
||||||
|
const range = item.presentation.split(AddonModFeedbackProvider.LINE_SEP) || [];
|
||||||
|
item.rangefrom = range.length > 0 ? parseInt(range[0], 10) || '' : '';
|
||||||
|
item.rangeto = range.length > 1 ? parseInt(range[1], 10) || '' : '';
|
||||||
|
item.value = typeof item.rawValue != 'undefined' ? parseFloat(item.rawValue) : '';
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper funtion for item type Text field.
|
||||||
|
*
|
||||||
|
* @param {any} item Item to process.
|
||||||
|
* @return {any} Item processed to show form.
|
||||||
|
*/
|
||||||
|
protected getItemFormTextfield(item: any): any {
|
||||||
|
item.template = 'textfield';
|
||||||
|
item.length = item.presentation.split(AddonModFeedbackProvider.LINE_SEP)[1] || 255;
|
||||||
|
item.value = typeof item.rawValue != 'undefined' ? item.rawValue : '';
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper funtion for item type Textarea.
|
||||||
|
*
|
||||||
|
* @param {any} item Item to process.
|
||||||
|
* @return {any} Item processed to show form.
|
||||||
|
*/
|
||||||
|
protected getItemFormTextarea(item: any): any {
|
||||||
|
item.template = 'textarea';
|
||||||
|
item.value = typeof item.rawValue != 'undefined' ? item.rawValue : '';
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper funtion for item type Multichoice.
|
||||||
|
*
|
||||||
|
* @param {any} item Item to process.
|
||||||
|
* @return {any} Item processed to show form.
|
||||||
|
*/
|
||||||
|
protected getItemFormMultichoice(item: any): any {
|
||||||
|
let parts = item.presentation.split(AddonModFeedbackProvider.MULTICHOICE_TYPE_SEP) || [];
|
||||||
|
item.subtype = parts.length > 0 && parts[0] ? parts[0] : 'r';
|
||||||
|
item.template = 'multichoice-' + item.subtype;
|
||||||
|
|
||||||
|
item.presentation = parts.length > 1 ? parts[1] : '';
|
||||||
|
if (item.subtype != 'd') {
|
||||||
|
parts = item.presentation.split(AddonModFeedbackProvider.MULTICHOICE_ADJUST_SEP) || [];
|
||||||
|
item.presentation = parts.length > 0 ? parts[0] : '';
|
||||||
|
// Horizontal are not supported right now. item.horizontal = parts.length > 1 && !!parts[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
item.choices = item.presentation.split(AddonModFeedbackProvider.LINE_SEP) || [];
|
||||||
|
item.choices = item.choices.map((choice, index) => {
|
||||||
|
const weightValue = choice.split(AddonModFeedbackProvider.MULTICHOICERATED_VALUE_SEP) || [''];
|
||||||
|
choice = weightValue.length == 1 ? weightValue[0] : '(' + weightValue[0] + ') ' + weightValue[1];
|
||||||
|
|
||||||
|
return {value: index + 1, label: choice};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (item.subtype === 'r' && item.options.search(AddonModFeedbackProvider.MULTICHOICE_HIDENOSELECT) == -1) {
|
||||||
|
item.choices.unshift({value: 0, label: this.translate.instant('addon.mod_feedback.not_selected')});
|
||||||
|
item.value = typeof item.rawValue != 'undefined' ? parseInt(item.rawValue, 10) : 0;
|
||||||
|
} else if (item.subtype === 'd') {
|
||||||
|
item.choices.unshift({value: 0, label: ''});
|
||||||
|
item.value = typeof item.rawValue != 'undefined' ? parseInt(item.rawValue, 10) : 0;
|
||||||
|
} else if (item.subtype === 'c') {
|
||||||
|
if (typeof item.rawValue == 'undefined') {
|
||||||
|
item.value = '';
|
||||||
|
} else {
|
||||||
|
item.rawValue = '' + item.rawValue;
|
||||||
|
const values = item.rawValue.split(AddonModFeedbackProvider.LINE_SEP);
|
||||||
|
item.choices.forEach((choice) => {
|
||||||
|
for (const x in values) {
|
||||||
|
if (choice.value == values[x]) {
|
||||||
|
choice.checked = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.value = typeof item.rawValue != 'undefined' ? parseInt(item.rawValue, 10) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper funtion for item type Captcha.
|
||||||
|
*
|
||||||
|
* @param {any} item Item to process.
|
||||||
|
* @return {any} Item processed to show form.
|
||||||
|
*/
|
||||||
|
protected getItemFormCaptcha(item: any): any {
|
||||||
|
const data = this.textUtils.parseJSON(item.otherdata);
|
||||||
|
if (data && data.length > 3) {
|
||||||
|
item.captcha = {
|
||||||
|
recaptchapublickey: data[3]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
item.template = 'captcha';
|
||||||
|
item.value = '';
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process and returns item to print form.
|
||||||
|
*
|
||||||
|
* @param {any} item Item to process.
|
||||||
|
* @param {boolean} preview Previewing options.
|
||||||
|
* @return {any} Item processed to show form.
|
||||||
|
*/
|
||||||
|
getItemForm(item: any, preview: boolean): any {
|
||||||
|
switch (item.typ) {
|
||||||
|
case 'label':
|
||||||
|
return this.getItemFormLabel(item);
|
||||||
|
case 'info':
|
||||||
|
return this.getItemFormInfo(item);
|
||||||
|
case 'numeric':
|
||||||
|
return this.getItemFormNumeric(item);
|
||||||
|
case 'textfield':
|
||||||
|
return this.getItemFormTextfield(item);
|
||||||
|
case 'textarea':
|
||||||
|
return this.getItemFormTextarea(item);
|
||||||
|
case 'multichoice':
|
||||||
|
return this.getItemFormMultichoice(item);
|
||||||
|
case 'multichoicerated':
|
||||||
|
return this.getItemFormMultichoice(item);
|
||||||
|
case 'pagebreak':
|
||||||
|
if (!preview) {
|
||||||
|
// Pagebreaks are only used on preview.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'captcha':
|
||||||
|
// Captcha is not supported right now. However label will be shown.
|
||||||
|
return this.getItemFormCaptcha(item);
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
// (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 { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler';
|
||||||
|
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
|
||||||
|
import { AddonModFeedbackProvider } from './feedback';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to treat links to feedback.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModFeedbackLinkHandler extends CoreContentLinksModuleIndexHandler {
|
||||||
|
name = 'AddonModFeedbackLinkHandler';
|
||||||
|
|
||||||
|
constructor(courseHelper: CoreCourseHelperProvider) {
|
||||||
|
super(courseHelper, AddonModFeedbackProvider.COMPONENT, 'feedback');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
// (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, NavOptions } from 'ionic-angular';
|
||||||
|
import { AddonModFeedbackIndexComponent } from '../components/index/index';
|
||||||
|
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate';
|
||||||
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
|
import { AddonModFeedbackProvider } from './feedback';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to support feedback modules.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModFeedbackModuleHandler implements CoreCourseModuleHandler {
|
||||||
|
name = 'AddonModFeedback';
|
||||||
|
modName = 'feedback';
|
||||||
|
|
||||||
|
constructor(private courseProvider: CoreCourseProvider, private feedbackProvider: AddonModFeedbackProvider) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return {Promise<boolean>} Whether or not the handler is enabled on a site level.
|
||||||
|
*/
|
||||||
|
isEnabled(): Promise<boolean> {
|
||||||
|
return this.feedbackProvider.isPluginEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the data required to display the module in the course contents view.
|
||||||
|
*
|
||||||
|
* @param {any} module The module object.
|
||||||
|
* @param {number} courseId The course ID.
|
||||||
|
* @param {number} sectionId The section ID.
|
||||||
|
* @return {CoreCourseModuleHandlerData} Data to render the module.
|
||||||
|
*/
|
||||||
|
getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData {
|
||||||
|
return {
|
||||||
|
icon: this.courseProvider.getModuleIconSrc('feedback'),
|
||||||
|
title: module.name,
|
||||||
|
class: 'addon-mod_feedback-handler',
|
||||||
|
showDownloadButton: true,
|
||||||
|
action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void {
|
||||||
|
navCtrl.push('AddonModFeedbackIndexPage', {module: module, courseId: courseId}, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the component to render the module. This is needed to support singleactivity course format.
|
||||||
|
* The component returned must implement CoreCourseModuleMainComponent.
|
||||||
|
*
|
||||||
|
* @param {any} course The course object.
|
||||||
|
* @param {any} module The module object.
|
||||||
|
* @return {any} The component to use, undefined if not found.
|
||||||
|
*/
|
||||||
|
getMainComponent(course: any, module: any): any {
|
||||||
|
return AddonModFeedbackIndexComponent;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CoreLoggerProvider } from '@providers/logger';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to handle Offline feedback.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModFeedbackOfflineProvider {
|
||||||
|
|
||||||
|
protected logger;
|
||||||
|
|
||||||
|
// Variables for database.
|
||||||
|
protected FEEDBACK_TABLE = 'addon_mod_feedback_answers';
|
||||||
|
protected tablesSchema = [
|
||||||
|
{
|
||||||
|
name: this.FEEDBACK_TABLE,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'feedbackid',
|
||||||
|
type: 'INTEGER'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'page',
|
||||||
|
type: 'TEXT'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'courseid',
|
||||||
|
type: 'INTEGER'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'responses',
|
||||||
|
type: 'TEXT'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timemodified',
|
||||||
|
type: 'INTEGER'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
primaryKeys: ['feedbackid', 'page']
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider,
|
||||||
|
private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider) {
|
||||||
|
this.logger = logger.getInstance('AddonModFeedbackOfflineProvider');
|
||||||
|
this.sitesProvider.createTablesFromSchema(this.tablesSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the stored for a certain feedback page.
|
||||||
|
*
|
||||||
|
* @param {number} feedbackId Feedback ID.
|
||||||
|
* @param {number} page Page of the form to delete responses from.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved if deleted, rejected if failure.
|
||||||
|
*/
|
||||||
|
deleteFeedbackPageResponses(feedbackId: number, page: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.getDb().deleteRecords(this.FEEDBACK_TABLE, {feedbackid: feedbackId, page: page});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the stored feedback responses data from all the feedback.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with entries.
|
||||||
|
*/
|
||||||
|
getAllFeedbackResponses(siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.getDb().getAllRecords(this.FEEDBACK_TABLE).then((entries) => {
|
||||||
|
return entries.map((entry) => {
|
||||||
|
entry.responses = this.textUtils.parseJSON(entry.responses);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the stored responses from a certain feedback.
|
||||||
|
*
|
||||||
|
* @param {number} feedbackId Feedback ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with responses.
|
||||||
|
*/
|
||||||
|
getFeedbackResponses(feedbackId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.getDb().getRecords(this.FEEDBACK_TABLE, {feedbackid: feedbackId}).then((entries) => {
|
||||||
|
return entries.map((entry) => {
|
||||||
|
entry.responses = this.textUtils.parseJSON(entry.responses);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the stored responses for a certain feedback page.
|
||||||
|
*
|
||||||
|
* @param {number} feedbackId Feedback ID.
|
||||||
|
* @param {number} page Page of the form to get responses from.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with responses.
|
||||||
|
*/
|
||||||
|
getFeedbackPageResponses(feedbackId: number, page: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
return site.getDb().getRecord(this.FEEDBACK_TABLE, {feedbackid: feedbackId, page: page}).then((entry) => {
|
||||||
|
entry.responses = this.textUtils.parseJSON(entry.responses);
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get if the feedback have something to be synced.
|
||||||
|
*
|
||||||
|
* @param {number} feedbackId Feedback ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with true if the feedback have something to be synced.
|
||||||
|
*/
|
||||||
|
hasFeedbackOfflineData(feedbackId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.getFeedbackResponses(feedbackId, siteId).then((responses) => {
|
||||||
|
return !!responses.length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save page responses to be sent later.
|
||||||
|
*
|
||||||
|
* @param {number} feedbackId Feedback ID.
|
||||||
|
* @param {number} page The page being processed.
|
||||||
|
* @param {any} responses The data to be processed the key is the field name (usually type[index]_id)
|
||||||
|
* @param {number} courseId Course ID the feedback belongs to.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved if stored, rejected if failure.
|
||||||
|
*/
|
||||||
|
saveResponses(feedbackId: number, page: number, responses: any, courseId: number, siteId?: string): Promise<any> {
|
||||||
|
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||||
|
const entry = {
|
||||||
|
feedbackid: feedbackId,
|
||||||
|
page: page,
|
||||||
|
courseid: courseId,
|
||||||
|
responses: JSON.stringify(responses),
|
||||||
|
timemodified: this.timeUtils.timestamp()
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.getDb().insertRecord(this.FEEDBACK_TABLE, entry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,226 @@
|
||||||
|
// (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, Injector } from '@angular/core';
|
||||||
|
import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler';
|
||||||
|
import { AddonModFeedbackProvider } from './feedback';
|
||||||
|
import { AddonModFeedbackHelperProvider } from './helper';
|
||||||
|
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||||
|
import { CoreTimeUtilsProvider } from '@providers/utils/time';
|
||||||
|
import { CoreGroupsProvider } from '@providers/groups';
|
||||||
|
import { CoreUserProvider } from '@core/user/providers/user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to prefetch feedbacks.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModFeedbackPrefetchHandler extends CoreCourseModulePrefetchHandlerBase {
|
||||||
|
name = 'feedback';
|
||||||
|
component = AddonModFeedbackProvider.COMPONENT;
|
||||||
|
updatesNames = /^configuration$|^.*files$|^attemptsfinished|^attemptsunfinished$/;
|
||||||
|
|
||||||
|
constructor(injector: Injector, protected feedbackProvider: AddonModFeedbackProvider, protected userProvider: CoreUserProvider,
|
||||||
|
protected filepoolProvider: CoreFilepoolProvider, protected feedbackHelper: AddonModFeedbackHelperProvider,
|
||||||
|
protected timeUtils: CoreTimeUtilsProvider, protected groupsProvider: CoreGroupsProvider) {
|
||||||
|
super(injector);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download or prefetch the content.
|
||||||
|
*
|
||||||
|
* @param {any} module The module object returned by WS.
|
||||||
|
* @param {number} courseId Course ID.
|
||||||
|
* @param {boolean} [prefetch] True to prefetch, false to download right away.
|
||||||
|
* @param {string} [dirPath] Path of the directory where to store all the content files. This is to keep the files
|
||||||
|
* relative paths and make the package work in an iframe. Undefined to download the files
|
||||||
|
* in the filepool root feedback.
|
||||||
|
* @return {Promise<any>} Promise resolved when all content is downloaded. Data returned is not reliable.
|
||||||
|
*/
|
||||||
|
downloadOrPrefetch(module: any, courseId: number, prefetch?: boolean, dirPath?: string): Promise<any> {
|
||||||
|
const promises = [],
|
||||||
|
siteId = this.sitesProvider.getCurrentSiteId();
|
||||||
|
|
||||||
|
promises.push(super.downloadOrPrefetch(module, courseId, prefetch));
|
||||||
|
promises.push(this.feedbackProvider.getFeedback(courseId, module.id).then((feedback) => {
|
||||||
|
const p1 = [];
|
||||||
|
|
||||||
|
p1.push(this.getFiles(module, courseId).then((files) => {
|
||||||
|
return this.filepoolProvider.addFilesToQueue(siteId, files, this.component, module.id);
|
||||||
|
}));
|
||||||
|
|
||||||
|
p1.push(this.feedbackProvider.getFeedbackAccessInformation(feedback.id, false, true, siteId).then((accessData) => {
|
||||||
|
const p2 = [];
|
||||||
|
if (accessData.canedititems || accessData.canviewreports) {
|
||||||
|
// Get all groups analysis.
|
||||||
|
p2.push(this.feedbackProvider.getAnalysis(feedback.id, undefined, siteId));
|
||||||
|
p2.push(this.groupsProvider.getActivityGroupInfo(feedback.coursemodule, true, undefined, siteId)
|
||||||
|
.then((groupInfo) => {
|
||||||
|
const p3 = [],
|
||||||
|
userIds = [];
|
||||||
|
|
||||||
|
if (!groupInfo.groups || groupInfo.groups.length == 0) {
|
||||||
|
groupInfo.groups = [{id: 0}];
|
||||||
|
}
|
||||||
|
groupInfo.groups.forEach((group) => {
|
||||||
|
p3.push(this.feedbackProvider.getAnalysis(feedback.id, group.id, siteId));
|
||||||
|
p3.push(this.feedbackProvider.getAllResponsesAnalysis(feedback.id, group.id, siteId)
|
||||||
|
.then((responses) => {
|
||||||
|
responses.attempts.forEach((attempt) => {
|
||||||
|
userIds.push(attempt.userid);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!accessData.isanonymous) {
|
||||||
|
p3.push(this.feedbackProvider.getAllNonRespondents(feedback.id, group.id, siteId)
|
||||||
|
.then((responses) => {
|
||||||
|
responses.users.forEach((user) => {
|
||||||
|
userIds.push(user.userid);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(p3).then(() => {
|
||||||
|
// Prefetch user profiles.
|
||||||
|
return this.userProvider.prefetchProfiles(userIds, courseId, siteId);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
p2.push(this.feedbackProvider.getItems(feedback.id, siteId));
|
||||||
|
|
||||||
|
if (accessData.cancomplete && accessData.cansubmit && !accessData.isempty) {
|
||||||
|
// Send empty data, so it will recover last completed feedback attempt values.
|
||||||
|
p2.push(this.feedbackProvider.processPageOnline(feedback.id, 0, {}, undefined, siteId).finally(() => {
|
||||||
|
const p4 = [];
|
||||||
|
|
||||||
|
p4.push(this.feedbackProvider.getCurrentValues(feedback.id, false, true, siteId));
|
||||||
|
p4.push(this.feedbackProvider.getResumePage(feedback.id, false, true, siteId));
|
||||||
|
|
||||||
|
return Promise.all(p4);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(p2);
|
||||||
|
}));
|
||||||
|
|
||||||
|
return Promise.all(p1);
|
||||||
|
}));
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of downloadable files.
|
||||||
|
*
|
||||||
|
* @param {any} module Module to get the files.
|
||||||
|
* @param {number} courseId Course ID the module belongs to.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved with the list of files.
|
||||||
|
*/
|
||||||
|
getFiles(module: any, courseId: number, single?: boolean): Promise<any[]> {
|
||||||
|
let files = [];
|
||||||
|
|
||||||
|
return this.feedbackProvider.getFeedback(courseId, module.id).then((feedback) => {
|
||||||
|
|
||||||
|
// Get intro files and page after submit files.
|
||||||
|
files = feedback.pageaftersubmitfiles || [];
|
||||||
|
files = files.concat(this.getIntroFilesFromInstance(module, feedback));
|
||||||
|
|
||||||
|
return this.feedbackProvider.getItems(feedback.id);
|
||||||
|
}).then((response) => {
|
||||||
|
response.items.forEach((item) => {
|
||||||
|
files = files.concat(item.itemfiles);
|
||||||
|
});
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}).catch(() => {
|
||||||
|
// Any error, return the list we have.
|
||||||
|
return files;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns feedback intro files.
|
||||||
|
*
|
||||||
|
* @param {any} module The module object returned by WS.
|
||||||
|
* @param {number} courseId Course ID.
|
||||||
|
* @return {Promise<any[]>} Promise resolved with list of intro files.
|
||||||
|
*/
|
||||||
|
getIntroFiles(module: any, courseId: number): Promise<any[]> {
|
||||||
|
return this.feedbackProvider.getFeedback(courseId, module.id).catch(() => {
|
||||||
|
// Not found, return undefined so module description is used.
|
||||||
|
}).then((feedback) => {
|
||||||
|
return this.getIntroFilesFromInstance(module, feedback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the prefetched content.
|
||||||
|
*
|
||||||
|
* @param {number} moduleId The module ID.
|
||||||
|
* @param {number} courseId Course ID the module belongs to.
|
||||||
|
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
invalidateContent(moduleId: number, courseId: number): Promise<any> {
|
||||||
|
return this.feedbackProvider.invalidateContent(moduleId, courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate WS calls needed to determine module status.
|
||||||
|
*
|
||||||
|
* @param {any} module Module.
|
||||||
|
* @param {number} courseId Course ID the module belongs to.
|
||||||
|
* @return {Promise<any>} Promise resolved when invalidated.
|
||||||
|
*/
|
||||||
|
invalidateModule(module: any, courseId: number): Promise<any> {
|
||||||
|
return this.feedbackProvider.invalidateFeedbackData(courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a feedback is downloadable.
|
||||||
|
* A feedback isn't downloadable if it's not open yet.
|
||||||
|
* Closed feedback are downloadable because teachers can always see the results.
|
||||||
|
*
|
||||||
|
* @param {any} module Module to check.
|
||||||
|
* @param {number} courseId Course ID the module belongs to.
|
||||||
|
* @return {Promise<any>} Promise resolved with true if downloadable, resolved with false otherwise.
|
||||||
|
*/
|
||||||
|
isDownloadable(module: any, courseId: number): boolean | Promise<boolean> {
|
||||||
|
return this.feedbackProvider.getFeedback(courseId, module.id, undefined, true).then((feedback) => {
|
||||||
|
const now = this.timeUtils.timestamp();
|
||||||
|
|
||||||
|
// Check time first if available.
|
||||||
|
if (feedback.timeopen && feedback.timeopen > now) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (feedback.timeclose && feedback.timeclose < now) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.feedbackProvider.getFeedbackAccessInformation(feedback.id).then((accessData) => {
|
||||||
|
return accessData.isopen;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return {boolean|Promise<boolean>} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
|
||||||
|
*/
|
||||||
|
isEnabled(): boolean | Promise<boolean> {
|
||||||
|
return this.feedbackProvider.isPluginEnabled();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
// (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 '@core/contentlinks/classes/base-handler';
|
||||||
|
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
|
||||||
|
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
|
||||||
|
import { AddonModFeedbackProvider } from './feedback';
|
||||||
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content links handler for feedback print questions.
|
||||||
|
* Match mod/feedback/print.php with a valid feedback id.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModFeedbackPrintLinkHandler extends CoreContentLinksHandlerBase {
|
||||||
|
name = 'AddonModFeedbackPrintLinkHandler';
|
||||||
|
featureName = 'CoreCourseModuleDelegate_AddonModFeedback';
|
||||||
|
pattern = /\/mod\/feedback\/print\.php.*([\?\&](id)=\d+)/;
|
||||||
|
|
||||||
|
constructor(private linkHelper: CoreContentLinksHelperProvider, private feedbackProvider: AddonModFeedbackProvider,
|
||||||
|
private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider) {
|
||||||
|
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 modal = this.domUtils.showModalLoading(),
|
||||||
|
moduleId = params.id;
|
||||||
|
|
||||||
|
this.courseProvider.getModuleBasicInfo(moduleId, siteId).then((module) => {
|
||||||
|
const stateParams = {
|
||||||
|
module: module,
|
||||||
|
moduleId: module.id,
|
||||||
|
courseId: module.course,
|
||||||
|
preview: true
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.linkHelper.goInSite(navCtrl, 'AddonModFeedbackFormPage', stateParams, siteId);
|
||||||
|
}).finally(() => {
|
||||||
|
modal.dismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (typeof params.id == 'undefined') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.feedbackProvider.isPluginEnabled(siteId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
// (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 '@core/contentlinks/classes/base-handler';
|
||||||
|
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
|
||||||
|
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
|
||||||
|
import { AddonModFeedbackProvider } from './feedback';
|
||||||
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content links handler for feedback show entries questions.
|
||||||
|
* Match mod/feedback/show_entries.php with a valid feedback id.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModFeedbackShowEntriesLinkHandler extends CoreContentLinksHandlerBase {
|
||||||
|
name = 'AddonModFeedbackShowEntriesLinkHandler';
|
||||||
|
featureName = 'CoreCourseModuleDelegate_AddonModFeedback';
|
||||||
|
pattern = /\/mod\/feedback\/show_entries\.php.*([\?\&](id|showcompleted)=\d+)/;
|
||||||
|
|
||||||
|
constructor(private linkHelper: CoreContentLinksHelperProvider, private feedbackProvider: AddonModFeedbackProvider,
|
||||||
|
private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider) {
|
||||||
|
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 modal = this.domUtils.showModalLoading(),
|
||||||
|
moduleId = params.id;
|
||||||
|
|
||||||
|
this.courseProvider.getModuleBasicInfo(moduleId, siteId).then((module) => {
|
||||||
|
let stateParams;
|
||||||
|
|
||||||
|
if (typeof params.showcompleted == 'undefined') {
|
||||||
|
// Param showcompleted not defined. Show entry list.
|
||||||
|
stateParams = {
|
||||||
|
moduleId: module.id,
|
||||||
|
module: module,
|
||||||
|
courseId: module.course
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.linkHelper.goInSite(navCtrl, 'AddonModFeedbackRespondentsPage', stateParams, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.feedbackProvider.getAttempt(module.instance, params.showcompleted, siteId).then((attempt) => {
|
||||||
|
stateParams = {
|
||||||
|
moduleId: module.id,
|
||||||
|
attempt: attempt,
|
||||||
|
attemptId: attempt.id,
|
||||||
|
feedbackId: module.instance
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.linkHelper.goInSite(navCtrl, 'AddonModFeedbackAttemptPage', stateParams, siteId);
|
||||||
|
});
|
||||||
|
}).finally(() => {
|
||||||
|
modal.dismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.feedbackProvider.isPluginEnabled(siteId).then((enabled) => {
|
||||||
|
if (!enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof params.id == 'undefined') {
|
||||||
|
// Cannot treat the URL.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 { Injectable } from '@angular/core';
|
||||||
|
import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
|
||||||
|
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
|
||||||
|
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
|
||||||
|
import { AddonModFeedbackProvider } from './feedback';
|
||||||
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content links handler for feedback show non respondents.
|
||||||
|
* Match mod/feedback/show_nonrespondents.php with a valid feedback id.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModFeedbackShowNonRespondentsLinkHandler extends CoreContentLinksHandlerBase {
|
||||||
|
name = 'AddonModFeedbackShowNonRespondentsLinkHandler';
|
||||||
|
featureName = 'CoreCourseModuleDelegate_AddonModFeedback';
|
||||||
|
pattern = /\/mod\/feedback\/show_nonrespondents\.php.*([\?\&](id)=\d+)/;
|
||||||
|
|
||||||
|
constructor(private linkHelper: CoreContentLinksHelperProvider, private feedbackProvider: AddonModFeedbackProvider,
|
||||||
|
private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider) {
|
||||||
|
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 modal = this.domUtils.showModalLoading(),
|
||||||
|
moduleId = params.id;
|
||||||
|
|
||||||
|
this.courseProvider.getModuleBasicInfo(moduleId, siteId).then((module) => {
|
||||||
|
const stateParams = {
|
||||||
|
module: module,
|
||||||
|
moduleId: module.id,
|
||||||
|
courseId: module.course
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.linkHelper.goInSite(navCtrl, 'AddonModFeedbackNonRespondentsPage', stateParams, siteId);
|
||||||
|
}).finally(() => {
|
||||||
|
modal.dismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.feedbackProvider.isPluginEnabled(siteId).then((enabled) => {
|
||||||
|
if (!enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof params.id == 'undefined') {
|
||||||
|
// Cannot treat the URL.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
// (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 { CoreCronHandler } from '@providers/cron';
|
||||||
|
import { AddonModFeedbackSyncProvider } from './sync';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronization cron handler.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModFeedbackSyncCronHandler implements CoreCronHandler {
|
||||||
|
name = 'AddonModFeedbackSyncCronHandler';
|
||||||
|
|
||||||
|
constructor(private feedbackSync: AddonModFeedbackSyncProvider) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the process.
|
||||||
|
* Receives the ID of the site affected, undefined for all sites.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] ID of the site affected, undefined for all sites.
|
||||||
|
* @return {Promise<any>} Promise resolved when done, rejected if failure.
|
||||||
|
*/
|
||||||
|
execute(siteId?: string): Promise<any> {
|
||||||
|
return this.feedbackSync.syncAllFeedbacks(siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the time between consecutive executions.
|
||||||
|
*
|
||||||
|
* @return {number} Time between consecutive executions (in ms).
|
||||||
|
*/
|
||||||
|
getInterval(): number {
|
||||||
|
return 600000; // 10 minutes.
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,263 @@
|
||||||
|
// (C) Copyright 2015 Martin Dougiamas
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { CoreLoggerProvider } from '@providers/logger';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreSyncBaseProvider } from '@classes/base-sync';
|
||||||
|
import { CoreAppProvider } from '@providers/app';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { AddonModFeedbackOfflineProvider } from './offline';
|
||||||
|
import { AddonModFeedbackProvider } from './feedback';
|
||||||
|
import { CoreEventsProvider } from '@providers/events';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||||
|
import { CoreSyncProvider } from '@providers/sync';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to sync feedbacks.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AddonModFeedbackSyncProvider extends CoreSyncBaseProvider {
|
||||||
|
|
||||||
|
static AUTO_SYNCED = 'addon_mod_feedback_autom_synced';
|
||||||
|
protected componentTranslate: string;
|
||||||
|
|
||||||
|
constructor(protected sitesProvider: CoreSitesProvider, protected loggerProvider: CoreLoggerProvider,
|
||||||
|
protected appProvider: CoreAppProvider, private feedbackOffline: AddonModFeedbackOfflineProvider,
|
||||||
|
private eventsProvider: CoreEventsProvider, private feedbackProvider: AddonModFeedbackProvider,
|
||||||
|
protected translate: TranslateService, private utils: CoreUtilsProvider, protected textUtils: CoreTextUtilsProvider,
|
||||||
|
courseProvider: CoreCourseProvider, syncProvider: CoreSyncProvider) {
|
||||||
|
super('AddonModFeedbackSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate);
|
||||||
|
this.componentTranslate = courseProvider.translateModuleName('feedback');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to synchronize all the feedbacks in a certain site or in all sites.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
|
||||||
|
* @return {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
|
||||||
|
*/
|
||||||
|
syncAllFeedbacks(siteId?: string): Promise<any> {
|
||||||
|
return this.syncOnSites('all feedbacks', this.syncAllFeedbacksFunc.bind(this), undefined, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync all pending feedbacks on a site.
|
||||||
|
*
|
||||||
|
* @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
|
||||||
|
* @param {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
|
||||||
|
*/
|
||||||
|
protected syncAllFeedbacksFunc(siteId?: string): Promise<any> {
|
||||||
|
// Sync all new responses.
|
||||||
|
return this.feedbackOffline.getAllFeedbackResponses(siteId).then((responses) => {
|
||||||
|
const promises = {};
|
||||||
|
|
||||||
|
// Do not sync same feedback twice.
|
||||||
|
for (const i in responses) {
|
||||||
|
const response = responses[i];
|
||||||
|
|
||||||
|
if (typeof promises[response.feedbackid] != 'undefined') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
promises[response.feedbackid] = this.syncFeedbackIfNeeded(response.feedbackid, siteId).then((result) => {
|
||||||
|
if (result && result.updated) {
|
||||||
|
// Sync successful, send event.
|
||||||
|
this.eventsProvider.trigger(AddonModFeedbackSyncProvider.AUTO_SYNCED, {
|
||||||
|
feedbackId: response.feedbackid,
|
||||||
|
userId: response.userid,
|
||||||
|
warnings: result.warnings
|
||||||
|
}, siteId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Promises will be an object so, convert to an array first;
|
||||||
|
return Promise.all(this.utils.objectToArray(promises));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync a feedback only if a certain time has passed since the last time.
|
||||||
|
*
|
||||||
|
* @param {number} feedbackId Feedback ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the feedback is synced or if it doesn't need to be synced.
|
||||||
|
*/
|
||||||
|
syncFeedbackIfNeeded(feedbackId: number, siteId?: string): Promise<any> {
|
||||||
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||||
|
|
||||||
|
return this.isSyncNeeded(feedbackId, siteId).then((needed) => {
|
||||||
|
if (needed) {
|
||||||
|
return this.syncFeedback(feedbackId, siteId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize all offline responses of a feedback.
|
||||||
|
*
|
||||||
|
* @param {number} feedbackId Feedback ID to be synced.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved if sync is successful, rejected otherwise.
|
||||||
|
*/
|
||||||
|
syncFeedback(feedbackId: number, siteId?: string): Promise<any> {
|
||||||
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||||
|
|
||||||
|
const syncId = feedbackId;
|
||||||
|
|
||||||
|
if (this.isSyncing(syncId, siteId)) {
|
||||||
|
// There's already a sync ongoing for this feedback, return the promise.
|
||||||
|
return this.getOngoingSync(syncId, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that feedback isn't blocked.
|
||||||
|
if (this.syncProvider.isBlocked(AddonModFeedbackProvider.COMPONENT, syncId, siteId)) {
|
||||||
|
this.logger.debug(`Cannot sync feedback '${syncId}' because it is blocked.`);
|
||||||
|
|
||||||
|
return Promise.reject(this.translate.instant('core.errorsyncblocked', {$a: this.componentTranslate}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
warnings: [],
|
||||||
|
updated: false
|
||||||
|
};
|
||||||
|
|
||||||
|
let courseId,
|
||||||
|
feedback;
|
||||||
|
|
||||||
|
this.logger.debug(`Try to sync feedback '${feedbackId}'`);
|
||||||
|
|
||||||
|
// Get offline responses to be sent.
|
||||||
|
const syncPromise = this.feedbackOffline.getFeedbackResponses(feedbackId, siteId).catch(() => {
|
||||||
|
// No offline data found, return empty array.
|
||||||
|
return [];
|
||||||
|
}).then((responses) => {
|
||||||
|
if (!responses.length) {
|
||||||
|
// Nothing to sync.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.appProvider.isOnline()) {
|
||||||
|
// Cannot sync in offline.
|
||||||
|
return Promise.reject(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
courseId = responses[0].courseid;
|
||||||
|
|
||||||
|
return this.feedbackProvider.getFeedbackById(courseId, feedbackId, siteId).then((feedbackData) => {
|
||||||
|
feedback = feedbackData;
|
||||||
|
|
||||||
|
if (!feedback.multiple_submit) {
|
||||||
|
// If it does not admit multiple submits, check if it is completed to know if we can submit.
|
||||||
|
return this.feedbackProvider.isCompleted(feedbackId);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}).then((isCompleted) => {
|
||||||
|
if (isCompleted) {
|
||||||
|
// Cannot submit again, delete resposes.
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
responses.forEach((data) => {
|
||||||
|
promises.push(this.feedbackOffline.deleteFeedbackPageResponses(feedbackId, data.page, siteId));
|
||||||
|
});
|
||||||
|
|
||||||
|
result.updated = true;
|
||||||
|
result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', {
|
||||||
|
component: this.componentTranslate,
|
||||||
|
name: feedback.name,
|
||||||
|
error: this.translate.instant('addon.mod_feedback.this_feedback_is_already_submitted')
|
||||||
|
}));
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.feedbackProvider.getCurrentCompletedTimeModified(feedbackId, siteId).then((timemodified) => {
|
||||||
|
// Sort by page.
|
||||||
|
responses.sort((a, b) => {
|
||||||
|
return a.page - b.page;
|
||||||
|
});
|
||||||
|
|
||||||
|
responses = responses.map((data) => {
|
||||||
|
return {
|
||||||
|
func: this.processPage.bind(this),
|
||||||
|
params: [feedback, data, siteId, timemodified, result],
|
||||||
|
blocking: true
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Execute all the processes in order to solve dependencies.
|
||||||
|
return this.utils.executeOrderedPromises(responses);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
if (result.updated) {
|
||||||
|
// Data has been sent to server. Now invalidate the WS calls.
|
||||||
|
return this.feedbackProvider.invalidateAllFeedbackData(feedbackId, siteId).catch(() => {
|
||||||
|
// Ignore errors.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
// Sync finished, set sync time.
|
||||||
|
return this.setSyncTime(syncId, siteId);
|
||||||
|
}).then(() => {
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.addOngoingSync(syncId, syncPromise, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to sync process page calls.
|
||||||
|
*
|
||||||
|
* @param {any} feedback Feedback object.
|
||||||
|
* @param {any} data Response data.
|
||||||
|
* @param {string} siteId Site Id.
|
||||||
|
* @param {number} timemodified Current completed modification time.
|
||||||
|
* @param {any} result Result object to be modified.
|
||||||
|
* @return {Promise<any>} Resolve when done or rejected with error.
|
||||||
|
*/
|
||||||
|
protected processPage(feedback: any, data: any, siteId: string, timemodified: number, result: any): Promise<any> {
|
||||||
|
// Delete all pages that are submitted before changing website.
|
||||||
|
if (timemodified > data.timemodified) {
|
||||||
|
return this.feedbackOffline.deleteFeedbackPageResponses(feedback.id, data.page, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.feedbackProvider.processPageOnline(feedback.id, data.page, data.responses, false, siteId).then(() => {
|
||||||
|
result.updated = true;
|
||||||
|
|
||||||
|
return this.feedbackOffline.deleteFeedbackPageResponses(feedback.id, data.page, siteId);
|
||||||
|
}).catch((error) => {
|
||||||
|
if (error && error.wserror) {
|
||||||
|
// The WebService has thrown an error, this means that responses cannot be submitted. Delete them.
|
||||||
|
result.updated = true;
|
||||||
|
|
||||||
|
return this.feedbackOffline.deleteFeedbackPageResponses(feedback.id, data.page, siteId).then(() => {
|
||||||
|
// Responses deleted, add a warning.
|
||||||
|
result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', {
|
||||||
|
component: this.componentTranslate,
|
||||||
|
name: feedback.name,
|
||||||
|
error: error.error
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Couldn't connect to server, reject.
|
||||||
|
return Promise.reject(error && error.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,7 +78,7 @@ export class AddonModSurveySyncProvider extends CoreSyncBaseProvider {
|
||||||
return this.surveyOffline.getAllData(siteId).then((entries) => {
|
return this.surveyOffline.getAllData(siteId).then((entries) => {
|
||||||
// Sync all surveys.
|
// Sync all surveys.
|
||||||
const promises = entries.map((entry) => {
|
const promises = entries.map((entry) => {
|
||||||
return this.syncSurvey(entry.surveyid, entry.userid, siteId).then((result) => {
|
return this.syncSurveyIfNeeded(entry.surveyid, entry.userid, siteId).then((result) => {
|
||||||
if (result && result.answersSent) {
|
if (result && result.answersSent) {
|
||||||
// Sync successful, send event.
|
// Sync successful, send event.
|
||||||
this.eventsProvider.trigger(AddonModSurveySyncProvider.AUTO_SYNCED, {
|
this.eventsProvider.trigger(AddonModSurveySyncProvider.AUTO_SYNCED, {
|
||||||
|
@ -94,6 +94,26 @@ export class AddonModSurveySyncProvider extends CoreSyncBaseProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync a survey only if a certain time has passed since the last time.
|
||||||
|
*
|
||||||
|
* @param {number} surveyId Survey ID.
|
||||||
|
* @param {number} userId User the answers belong to.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when the survey is synced or if it doesn't need to be synced.
|
||||||
|
*/
|
||||||
|
syncSurveyIfNeeded(surveyId: number, userId: number, siteId?: string): Promise<any> {
|
||||||
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||||
|
|
||||||
|
const syncId = this.getSyncId(surveyId, userId);
|
||||||
|
|
||||||
|
return this.isSyncNeeded(syncId, siteId).then((needed) => {
|
||||||
|
if (needed) {
|
||||||
|
return this.syncSurvey(surveyId, userId, siteId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronize a survey.
|
* Synchronize a survey.
|
||||||
*
|
*
|
||||||
|
|
|
@ -79,6 +79,7 @@ import { AddonFilesModule } from '@addon/files/files.module';
|
||||||
import { AddonModBookModule } from '@addon/mod/book/book.module';
|
import { AddonModBookModule } from '@addon/mod/book/book.module';
|
||||||
import { AddonModLabelModule } from '@addon/mod/label/label.module';
|
import { AddonModLabelModule } from '@addon/mod/label/label.module';
|
||||||
import { AddonModResourceModule } from '@addon/mod/resource/resource.module';
|
import { AddonModResourceModule } from '@addon/mod/resource/resource.module';
|
||||||
|
import { AddonModFeedbackModule } from '@addon/mod/feedback/feedback.module';
|
||||||
import { AddonModFolderModule } from '@addon/mod/folder/folder.module';
|
import { AddonModFolderModule } from '@addon/mod/folder/folder.module';
|
||||||
import { AddonModPageModule } from '@addon/mod/page/page.module';
|
import { AddonModPageModule } from '@addon/mod/page/page.module';
|
||||||
import { AddonModQuizModule } from '@addon/mod/quiz/quiz.module';
|
import { AddonModQuizModule } from '@addon/mod/quiz/quiz.module';
|
||||||
|
@ -173,6 +174,7 @@ export const CORE_PROVIDERS: any[] = [
|
||||||
AddonModBookModule,
|
AddonModBookModule,
|
||||||
AddonModLabelModule,
|
AddonModLabelModule,
|
||||||
AddonModResourceModule,
|
AddonModResourceModule,
|
||||||
|
AddonModFeedbackModule,
|
||||||
AddonModFolderModule,
|
AddonModFolderModule,
|
||||||
AddonModPageModule,
|
AddonModPageModule,
|
||||||
AddonModQuizModule,
|
AddonModQuizModule,
|
||||||
|
|
|
@ -567,8 +567,17 @@ textarea {
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Message cards
|
canvas[core-chart] {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-circle:before {
|
||||||
|
content: ' \25CF';
|
||||||
|
}
|
||||||
|
|
||||||
@each $color-name, $color-base, $color-contrast in get-colors($colors) {
|
@each $color-name, $color-base, $color-contrast in get-colors($colors) {
|
||||||
|
// Message cards.
|
||||||
.core-#{$color-name}-card {
|
.core-#{$color-name}-card {
|
||||||
@extend ion-card;
|
@extend ion-card;
|
||||||
border-bottom: 3px solid $color-base;
|
border-bottom: 3px solid $color-base;
|
||||||
|
@ -589,21 +598,24 @@ textarea {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.core-circle:before {
|
.core-#{$color-name}-item {
|
||||||
content: ' \25CF';
|
border-bottom: 3px solid $color-base !important;
|
||||||
}
|
ion-icon {
|
||||||
|
color: $color-base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@each $color-name, $color-base, $color-contrast in get-colors($colors) {
|
|
||||||
.core-#{$color-name}-circle {
|
.core-#{$color-name}-circle {
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.core-#{$color-name}-circle:before {
|
.core-#{$color-name}-circle:before {
|
||||||
@extend .core-circle:before;
|
@extend .core-circle:before;
|
||||||
color: $color-base;
|
color: $color-base;
|
||||||
}
|
}
|
||||||
.text-#{$color-name} {
|
|
||||||
|
.text-#{$color-name}, p.#{$color-name}, .item p.text-#{$color-name} {
|
||||||
color: $color-base;
|
color: $color-base;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -626,4 +638,15 @@ textarea {
|
||||||
|
|
||||||
[ion-fixed] {
|
[ion-fixed] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.core-modal-fullscreen {
|
||||||
|
.modal-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
display: block;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@ import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
|
||||||
import { CoreDynamicComponent } from './dynamic-component/dynamic-component';
|
import { CoreDynamicComponent } from './dynamic-component/dynamic-component';
|
||||||
import { CoreSendMessageFormComponent } from './send-message-form/send-message-form';
|
import { CoreSendMessageFormComponent } from './send-message-form/send-message-form';
|
||||||
import { CoreTimerComponent } from './timer/timer';
|
import { CoreTimerComponent } from './timer/timer';
|
||||||
|
import { CoreRecaptchaComponent, CoreRecaptchaModalComponent } from './recaptcha/recaptcha';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -67,11 +68,14 @@ import { CoreTimerComponent } from './timer/timer';
|
||||||
CoreNavBarButtonsComponent,
|
CoreNavBarButtonsComponent,
|
||||||
CoreDynamicComponent,
|
CoreDynamicComponent,
|
||||||
CoreSendMessageFormComponent,
|
CoreSendMessageFormComponent,
|
||||||
CoreTimerComponent
|
CoreTimerComponent,
|
||||||
|
CoreRecaptchaComponent,
|
||||||
|
CoreRecaptchaModalComponent
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
CoreContextMenuPopoverComponent,
|
CoreContextMenuPopoverComponent,
|
||||||
CoreCoursePickerMenuPopoverComponent
|
CoreCoursePickerMenuPopoverComponent,
|
||||||
|
CoreRecaptchaModalComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
IonicModule,
|
IonicModule,
|
||||||
|
@ -101,7 +105,8 @@ import { CoreTimerComponent } from './timer/timer';
|
||||||
CoreNavBarButtonsComponent,
|
CoreNavBarButtonsComponent,
|
||||||
CoreDynamicComponent,
|
CoreDynamicComponent,
|
||||||
CoreSendMessageFormComponent,
|
CoreSendMessageFormComponent,
|
||||||
CoreTimerComponent
|
CoreTimerComponent,
|
||||||
|
CoreRecaptchaComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CoreComponentsModule {}
|
export class CoreComponentsModule {}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<div [class.core-loading-container]="loading">
|
<div [class.core-loading-container]="loading">
|
||||||
<iframe #iframe [hidden]="loading" class="core-iframe" [ngStyle]="{'width': iframeWidth, 'height': iframeHeight}" [src]="safeUrl"></iframe>
|
<iframe #iframe [hidden]="loading" class="core-iframe" [ngStyle]="{'width': iframeWidth, 'height': iframeHeight}" [src]="safeUrl"></iframe>
|
||||||
<ion-spinner *ngIf="loading"></ion-spinner>
|
<span class="core-loading-spinner">
|
||||||
|
<ion-spinner *ngIf="loading"></ion-spinner>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
|
@ -5,4 +5,25 @@ core-iframe {
|
||||||
iframe {
|
iframe {
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.core-loading-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: table;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
clear: both;
|
||||||
|
|
||||||
|
.core-loading-spinner {
|
||||||
|
display: table-cell;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, Input, OnInit, ViewChild, ElementRef } from '@angular/core';
|
import { Component, Input, Output, OnInit, ViewChild, ElementRef, EventEmitter } from '@angular/core';
|
||||||
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
||||||
import { Platform } from 'ionic-angular';
|
import { Platform } from 'ionic-angular';
|
||||||
import { CoreFileProvider } from '@providers/file';
|
import { CoreFileProvider } from '@providers/file';
|
||||||
|
@ -35,6 +35,7 @@ export class CoreIframeComponent implements OnInit {
|
||||||
@Input() src: string;
|
@Input() src: string;
|
||||||
@Input() iframeWidth: string;
|
@Input() iframeWidth: string;
|
||||||
@Input() iframeHeight: string;
|
@Input() iframeHeight: string;
|
||||||
|
@Output() loaded?: EventEmitter<HTMLIFrameElement> = new EventEmitter<HTMLIFrameElement>();
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
safeUrl: SafeResourceUrl;
|
safeUrl: SafeResourceUrl;
|
||||||
|
|
||||||
|
@ -46,6 +47,7 @@ export class CoreIframeComponent implements OnInit {
|
||||||
private textUtils: CoreTextUtilsProvider, private utils: CoreUtilsProvider, private domUtils: CoreDomUtilsProvider,
|
private textUtils: CoreTextUtilsProvider, private utils: CoreUtilsProvider, private domUtils: CoreDomUtilsProvider,
|
||||||
private sitesProvider: CoreSitesProvider, private platform: Platform, private sanitizer: DomSanitizer) {
|
private sitesProvider: CoreSitesProvider, private platform: Platform, private sanitizer: DomSanitizer) {
|
||||||
this.logger = logger.getInstance('CoreIframe');
|
this.logger = logger.getInstance('CoreIframe');
|
||||||
|
this.loaded = new EventEmitter<HTMLIFrameElement>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,6 +68,7 @@ export class CoreIframeComponent implements OnInit {
|
||||||
if (this.loading) {
|
if (this.loading) {
|
||||||
iframe.addEventListener('load', () => {
|
iframe.addEventListener('load', () => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
this.loaded.emit(iframe); // Notify iframe was loaded.
|
||||||
});
|
});
|
||||||
|
|
||||||
iframe.addEventListener('error', () => {
|
iframe.addEventListener('error', () => {
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!-- ReCAPTCHA V2 -->
|
||||||
|
<div *ngIf="publicKey">
|
||||||
|
<!-- A button to open the recaptcha modal. -->
|
||||||
|
<!-- Use anchor instead of button to prevent marking form as submitted. -->
|
||||||
|
<button ion-button block *ngIf="!model[modelValueName]" (click)="answerRecaptcha()" type="button">{{ 'core.answer' | translate }}</button>
|
||||||
|
<p *ngIf="model[modelValueName]" class="text-success">{{ 'core.answered' | translate }}</p>
|
||||||
|
<p *ngIf="expired" class="text-danger">{{ 'core.login.recaptchaexpired' | translate }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error that happens sometimes with reCaptcha V1, probably due to deprecation warnings. -->
|
||||||
|
<div class="core-warning-card" icon-start *ngIf="!challengehash && challengeimage" >
|
||||||
|
<ion-icon name="warning"></ion-icon>
|
||||||
|
{{ 'core.errorloadingcontent' | translate }}
|
||||||
|
</div>
|
|
@ -0,0 +1,127 @@
|
||||||
|
// (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, Input } from '@angular/core';
|
||||||
|
import { ModalController, ViewController, NavParams } from 'ionic-angular';
|
||||||
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreLangProvider } from '@providers/lang';
|
||||||
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directive to display a reCaptcha.
|
||||||
|
*
|
||||||
|
* Accepts the following attributes:
|
||||||
|
* @param {any} model The model where to store the recaptcha response.
|
||||||
|
* @param {string} publicKey The site public key.
|
||||||
|
* @param {string} [modelValueName] Name of the model property where to store the response. Defaults to 'recaptcharesponse'.
|
||||||
|
* @param {string} [siteUrl] The site URL. If not defined, current site.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-recaptcha',
|
||||||
|
templateUrl: 'recaptcha.html'
|
||||||
|
})
|
||||||
|
export class CoreRecaptchaComponent {
|
||||||
|
expired = false;
|
||||||
|
|
||||||
|
protected lang: string;
|
||||||
|
|
||||||
|
@Input() model: any;
|
||||||
|
@Input() publicKey: string;
|
||||||
|
@Input() modelValueName = 'recaptcharesponse';
|
||||||
|
@Input() siteUrl?: string;
|
||||||
|
|
||||||
|
constructor(private sitesProvider: CoreSitesProvider, langProvider: CoreLangProvider,
|
||||||
|
private textUtils: CoreTextUtilsProvider, private modalCtrl: ModalController) {
|
||||||
|
|
||||||
|
// Get the current language of the app.
|
||||||
|
langProvider.getCurrentLanguage().then((lang) => {
|
||||||
|
this.lang = lang;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.siteUrl = this.siteUrl || this.sitesProvider.getCurrentSite().getURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the recaptcha modal.
|
||||||
|
*/
|
||||||
|
answerRecaptcha(): void {
|
||||||
|
// Set the iframe src. We use an iframe because reCaptcha V2 doesn't work with file:// protocol.
|
||||||
|
const src = this.textUtils.concatenatePaths(this.siteUrl, 'webservice/recaptcha.php?lang=' + this.lang);
|
||||||
|
|
||||||
|
// Modal to answer the recaptcha.
|
||||||
|
// This is because the size of the recaptcha is dynamic, so it could cause problems if it was displayed inline.
|
||||||
|
const modal = this.modalCtrl.create(CoreRecaptchaModalComponent, { src: src },
|
||||||
|
{ cssClass: 'core-modal-fullscreen'});
|
||||||
|
modal.onDidDismiss((data) => {
|
||||||
|
this.expired = data.expired;
|
||||||
|
this.model[this.modelValueName] = data.value;
|
||||||
|
});
|
||||||
|
modal.present();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'core-recaptcha-modal',
|
||||||
|
templateUrl: 'recaptchamodal.html'
|
||||||
|
})
|
||||||
|
export class CoreRecaptchaModalComponent {
|
||||||
|
|
||||||
|
expired = false;
|
||||||
|
value = '';
|
||||||
|
src: string;
|
||||||
|
|
||||||
|
constructor(protected viewCtrl: ViewController, params: NavParams) {
|
||||||
|
this.src = params.get('src');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close modal.
|
||||||
|
*/
|
||||||
|
closeModal(): void {
|
||||||
|
this.viewCtrl.dismiss({
|
||||||
|
expired: this.expired,
|
||||||
|
value: this.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The iframe with the recaptcha was loaded.
|
||||||
|
*
|
||||||
|
* @param {HTMLIFrameElement} iframe Iframe element.
|
||||||
|
*/
|
||||||
|
loaded(iframe: HTMLIFrameElement): void {
|
||||||
|
// Search the iframe content.
|
||||||
|
const contentWindow = iframe && iframe.contentWindow;
|
||||||
|
|
||||||
|
if (contentWindow) {
|
||||||
|
// Set the callbacks we're interested in.
|
||||||
|
contentWindow['recaptchacallback'] = (value): void => {
|
||||||
|
this.expired = false;
|
||||||
|
this.value = value;
|
||||||
|
this.closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
contentWindow['recaptchaexpiredcallback'] = (): void => {
|
||||||
|
// Verification expired. Check the checkbox again.
|
||||||
|
this.expired = true;
|
||||||
|
this.value = '';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar>
|
||||||
|
<ion-title>{{ 'core.login.security_question' | translate }}</ion-title>
|
||||||
|
|
||||||
|
<ion-buttons end>
|
||||||
|
<button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||||
|
<ion-icon name="close"></ion-icon>
|
||||||
|
</button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<core-iframe [src]="src" (loaded)="loaded($event)"></core-iframe>
|
||||||
|
</ion-content>
|
|
@ -56,7 +56,7 @@ core-tabs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-content.no-scroll {
|
:not(.has-refresher) > .scroll-content.no-scroll {
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,13 +54,12 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
||||||
this.courseProvider = injector.get(CoreCourseProvider);
|
this.courseProvider = injector.get(CoreCourseProvider);
|
||||||
this.appProvider = injector.get(CoreAppProvider);
|
this.appProvider = injector.get(CoreAppProvider);
|
||||||
this.eventsProvider = injector.get(CoreEventsProvider);
|
this.eventsProvider = injector.get(CoreEventsProvider);
|
||||||
this.modulePrefetchProvider = injector.get(CoreCourseModulePrefetchDelegate);
|
|
||||||
|
|
||||||
const network = injector.get(Network);
|
const network = injector.get(Network);
|
||||||
|
|
||||||
// Refresh online status when changes.
|
// Refresh online status when changes.
|
||||||
this.onlineObserver = network.onchange().subscribe((online) => {
|
this.onlineObserver = network.onchange().subscribe((online) => {
|
||||||
this.isOnline = this.appProvider.isOnline();
|
this.isOnline = online;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,14 +77,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
||||||
if (this.syncEventName) {
|
if (this.syncEventName) {
|
||||||
// Refresh data if this discussion is synchronized automatically.
|
// Refresh data if this discussion is synchronized automatically.
|
||||||
this.syncObserver = this.eventsProvider.on(this.syncEventName, (data) => {
|
this.syncObserver = this.eventsProvider.on(this.syncEventName, (data) => {
|
||||||
if (this.isRefreshSyncNeeded(data)) {
|
this.autoSyncEventReceived(data);
|
||||||
// Refresh the data.
|
|
||||||
this.loaded = false;
|
|
||||||
this.refreshIcon = 'spinner';
|
|
||||||
this.syncIcon = 'spinner';
|
|
||||||
|
|
||||||
this.refreshContent(false);
|
|
||||||
}
|
|
||||||
}, this.siteId);
|
}, this.siteId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,12 +92,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
||||||
*/
|
*/
|
||||||
doRefresh(refresher?: any, done?: () => void, showErrors: boolean = false): Promise<any> {
|
doRefresh(refresher?: any, done?: () => void, showErrors: boolean = false): Promise<any> {
|
||||||
if (this.loaded) {
|
if (this.loaded) {
|
||||||
this.refreshIcon = 'spinner';
|
|
||||||
this.syncIcon = 'spinner';
|
|
||||||
|
|
||||||
return this.refreshContent(true, showErrors).finally(() => {
|
return this.refreshContent(true, showErrors).finally(() => {
|
||||||
this.refreshIcon = 'refresh';
|
|
||||||
this.syncIcon = 'sync';
|
|
||||||
refresher && refresher.complete();
|
refresher && refresher.complete();
|
||||||
done && done();
|
done && done();
|
||||||
});
|
});
|
||||||
|
@ -124,6 +111,20 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An autosync event has been received, check if refresh is needed and update the view.
|
||||||
|
*
|
||||||
|
* @param {any} syncEventData Data receiven on sync observer.
|
||||||
|
*/
|
||||||
|
protected autoSyncEventReceived(syncEventData: any): void {
|
||||||
|
if (this.isRefreshSyncNeeded(syncEventData)) {
|
||||||
|
this.loaded = false;
|
||||||
|
|
||||||
|
// Refresh the data.
|
||||||
|
this.refreshContent(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform the refresh content function.
|
* Perform the refresh content function.
|
||||||
*
|
*
|
||||||
|
@ -132,10 +133,16 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
||||||
* @return {Promise<any>} Resolved when done.
|
* @return {Promise<any>} Resolved when done.
|
||||||
*/
|
*/
|
||||||
protected refreshContent(sync: boolean = false, showErrors: boolean = false): Promise<any> {
|
protected refreshContent(sync: boolean = false, showErrors: boolean = false): Promise<any> {
|
||||||
|
this.refreshIcon = 'spinner';
|
||||||
|
this.syncIcon = 'spinner';
|
||||||
|
|
||||||
return this.invalidateContent().catch(() => {
|
return this.invalidateContent().catch(() => {
|
||||||
// Ignore errors.
|
// Ignore errors.
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
return this.loadContent(true, sync, showErrors);
|
return this.loadContent(true, sync, showErrors);
|
||||||
|
}).finally(() => {
|
||||||
|
this.refreshIcon = 'refresh';
|
||||||
|
this.syncIcon = 'sync';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -79,10 +79,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
|
||||||
*/
|
*/
|
||||||
doRefresh(refresher?: any, done?: () => void): Promise<any> {
|
doRefresh(refresher?: any, done?: () => void): Promise<any> {
|
||||||
if (this.loaded) {
|
if (this.loaded) {
|
||||||
this.refreshIcon = 'spinner';
|
|
||||||
|
|
||||||
return this.refreshContent().finally(() => {
|
return this.refreshContent().finally(() => {
|
||||||
this.refreshIcon = 'refresh';
|
|
||||||
refresher && refresher.complete();
|
refresher && refresher.complete();
|
||||||
done && done();
|
done && done();
|
||||||
});
|
});
|
||||||
|
@ -97,10 +94,14 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
|
||||||
* @return {Promise<any>} Resolved when done.
|
* @return {Promise<any>} Resolved when done.
|
||||||
*/
|
*/
|
||||||
protected refreshContent(): Promise<any> {
|
protected refreshContent(): Promise<any> {
|
||||||
|
this.refreshIcon = 'spinner';
|
||||||
|
|
||||||
return this.invalidateContent().catch(() => {
|
return this.invalidateContent().catch(() => {
|
||||||
// Ignore errors.
|
// Ignore errors.
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
return this.loadContent(true);
|
return this.loadContent(true);
|
||||||
|
}).finally(() => {
|
||||||
|
this.refreshIcon = 'refresh';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,8 @@
|
||||||
"profileinvaliddata": "Invalid value",
|
"profileinvaliddata": "Invalid value",
|
||||||
"potentialidps": "Log in using your account on:",
|
"potentialidps": "Log in using your account on:",
|
||||||
"recaptchachallengeimage": "reCAPTCHA challenge image",
|
"recaptchachallengeimage": "reCAPTCHA challenge image",
|
||||||
|
"recaptchaexpired": "Verification expired. Answer the security question again.",
|
||||||
|
"recaptchaincorrect": "The security question answer is incorrect.",
|
||||||
"reconnect": "Reconnect",
|
"reconnect": "Reconnect",
|
||||||
"reconnectdescription": "Your authentication token is invalid or has expired, you have to reconnect to the site.",
|
"reconnectdescription": "Your authentication token is invalid or has expired, you have to reconnect to the site.",
|
||||||
"reconnectssodescription": "Your authentication token is invalid or has expired, you have to reconnect to the site. You need to log in to the site in a browser window.",
|
"reconnectssodescription": "Your authentication token is invalid or has expired, you have to reconnect to the site. You need to log in to the site in a browser window.",
|
||||||
|
|
|
@ -82,19 +82,10 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- ReCAPTCHA -->
|
<!-- ReCAPTCHA -->
|
||||||
<ng-container *ngIf="settings.recaptchachallengehash && settings.recaptchachallengeimage">
|
<ng-container *ngIf="settings.recaptchapublickey">
|
||||||
<ion-item-divider text-wrap color="light">{{ 'core.login.security_question' | translate }}</ion-item-divider>
|
<ion-item-divider text-wrap color="light">{{ 'core.login.security_question' | translate }}</ion-item-divider>
|
||||||
<ion-item>
|
|
||||||
<img [src]="settings.recaptchachallengeimage" alt="{{ 'core.login.recaptchachallengeimage' | translate }}">
|
|
||||||
</ion-item>
|
|
||||||
<ion-item text-wrap>
|
<ion-item text-wrap>
|
||||||
<ion-label stacked core-mark-required="true">{{ 'core.login.enterthewordsabove' | translate }}</ion-label>
|
<core-recaptcha [publicKey]="settings.recaptchapublickey" [model]="captcha" [siteUrl]="siteUrl"></core-recaptcha>
|
||||||
<ion-input type="text" name="recaptcharesponse" placeholder="{{ 'core.login.enterthewordsabove' | translate }}" formControlName="recaptcharesponse" autocapitalize="none" autocorrect="off"></ion-input>
|
|
||||||
<core-input-errors item-content [control]="signupForm.controls.recaptcharesponse"></core-input-errors>
|
|
||||||
</ion-item>
|
|
||||||
<ion-item padding>
|
|
||||||
<!-- Use anchor instead of button to prevent marking form as submitted. -->
|
|
||||||
<a ion-button block (click)="requestCaptcha()">{{ 'core.login.getanothercaptcha' | translate }}</a>
|
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -113,7 +104,7 @@
|
||||||
|
|
||||||
<!-- Submit button. -->
|
<!-- Submit button. -->
|
||||||
<ion-item padding>
|
<ion-item padding>
|
||||||
<button ion-button block color="primary">{{ 'core.login.createaccount' | translate }}</button>
|
<button ion-button block color="primary" type="submit">{{ 'core.login.createaccount' | translate }}</button>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</form>
|
</form>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
|
|
@ -44,6 +44,9 @@ export class CoreLoginEmailSignupPage {
|
||||||
countriesKeys: any[];
|
countriesKeys: any[];
|
||||||
categories: any[];
|
categories: any[];
|
||||||
settingsLoaded = false;
|
settingsLoaded = false;
|
||||||
|
captcha = {
|
||||||
|
recaptcharesponse: ''
|
||||||
|
};
|
||||||
|
|
||||||
// Validation errors.
|
// Validation errors.
|
||||||
usernameErrors: any;
|
usernameErrors: any;
|
||||||
|
@ -98,10 +101,6 @@ export class CoreLoginEmailSignupPage {
|
||||||
this.signupForm.addControl(this.settings.namefields[i], this.fb.control('', Validators.required));
|
this.signupForm.addControl(this.settings.namefields[i], this.fb.control('', Validators.required));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.settings.recaptchachallengehash && this.settings.recaptchachallengeimage) {
|
|
||||||
this.signupForm.addControl('recaptcharesponse', this.fb.control('', Validators.required));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.settings.sitepolicy) {
|
if (this.settings.sitepolicy) {
|
||||||
this.signupForm.addControl('policyagreed', this.fb.control(false, Validators.requiredTrue));
|
this.signupForm.addControl('policyagreed', this.fb.control(false, Validators.requiredTrue));
|
||||||
}
|
}
|
||||||
|
@ -133,8 +132,8 @@ export class CoreLoginEmailSignupPage {
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.categories = this.loginHelper.formatProfileFieldsForSignup(settings.profilefields);
|
this.categories = this.loginHelper.formatProfileFieldsForSignup(settings.profilefields);
|
||||||
|
|
||||||
if (this.signupForm && this.signupForm.controls['recaptcharesponse']) {
|
if (this.settings.recaptchapublickey) {
|
||||||
this.signupForm.controls['recaptcharesponse'].reset(); // Reset captcha.
|
this.captcha.recaptcharesponse = ''; // Reset captcha.
|
||||||
}
|
}
|
||||||
|
|
||||||
this.namefieldsErrors = {};
|
this.namefieldsErrors = {};
|
||||||
|
@ -183,27 +182,11 @@ export class CoreLoginEmailSignupPage {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Request another captcha.
|
|
||||||
*
|
|
||||||
* @param {boolean} ignoreError Whether to ignore errors.
|
|
||||||
*/
|
|
||||||
requestCaptcha(ignoreError?: boolean): void {
|
|
||||||
const modal = this.domUtils.showModalLoading();
|
|
||||||
this.getSignupSettings().catch((err) => {
|
|
||||||
if (!ignoreError && err) {
|
|
||||||
this.domUtils.showErrorModal(err);
|
|
||||||
}
|
|
||||||
}).finally(() => {
|
|
||||||
modal.dismiss();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create account.
|
* Create account.
|
||||||
*/
|
*/
|
||||||
create(): void {
|
create(): void {
|
||||||
if (!this.signupForm.valid) {
|
if (!this.signupForm.valid || (this.settings.recaptchapublickey && !this.captcha.recaptcharesponse)) {
|
||||||
// Form not valid. Scroll to the first element with errors.
|
// Form not valid. Scroll to the first element with errors.
|
||||||
if (!this.domUtils.scrollToInputError(this.content)) {
|
if (!this.domUtils.scrollToInputError(this.content)) {
|
||||||
// Input not found, show an error modal.
|
// Input not found, show an error modal.
|
||||||
|
@ -226,9 +209,9 @@ export class CoreLoginEmailSignupPage {
|
||||||
params.redirect = this.loginHelper.prepareForSSOLogin(this.siteUrl, service, this.siteConfig.launchurl);
|
params.redirect = this.loginHelper.prepareForSSOLogin(this.siteUrl, service, this.siteConfig.launchurl);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.settings.recaptchachallengehash && this.settings.recaptchachallengeimage) {
|
// Get the recaptcha response (if needed).
|
||||||
params.recaptchachallengehash = this.settings.recaptchachallengehash;
|
if (this.settings.recaptchapublickey && this.captcha.recaptcharesponse) {
|
||||||
params.recaptcharesponse = this.signupForm.value.recaptcharesponse;
|
params.recaptcharesponse = this.captcha.recaptcharesponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the data for the custom profile fields.
|
// Get the data for the custom profile fields.
|
||||||
|
@ -243,17 +226,20 @@ export class CoreLoginEmailSignupPage {
|
||||||
this.domUtils.showAlert(this.translate.instant('core.success'), message);
|
this.domUtils.showAlert(this.translate.instant('core.success'), message);
|
||||||
this.navCtrl.pop();
|
this.navCtrl.pop();
|
||||||
} else {
|
} else {
|
||||||
this.domUtils.showErrorModalFirstWarning(result.warnings, 'core.login.usernotaddederror', true);
|
if (result.warnings && result.warnings.length) {
|
||||||
|
let error = result.warnings[0].message;
|
||||||
|
if (error == 'incorrect-captcha-sol') {
|
||||||
|
error = this.translate.instant('mm.login.recaptchaincorrect');
|
||||||
|
}
|
||||||
|
|
||||||
// Error sending, request another capctha since the current one is probably invalid now.
|
this.domUtils.showErrorModal(error);
|
||||||
this.requestCaptcha(true);
|
} else {
|
||||||
|
this.domUtils.showErrorModal('core.login.usernotaddederror', true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
this.domUtils.showErrorModalDefault(error && error.error, 'core.login.usernotaddederror', true);
|
this.domUtils.showErrorModalDefault(error && error.error, 'core.login.usernotaddederror', true);
|
||||||
|
|
||||||
// Error sending, request another capctha since the current one is probably invalid now.
|
|
||||||
this.requestCaptcha(true);
|
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
modal.dismiss();
|
modal.dismiss();
|
||||||
});
|
});
|
||||||
|
|
|
@ -143,6 +143,10 @@ export class CoreUserProfileFieldDelegate extends CoreDelegate {
|
||||||
const result = [],
|
const result = [],
|
||||||
promises = [];
|
promises = [];
|
||||||
|
|
||||||
|
if (!fields) {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
fields.forEach((field) => {
|
fields.forEach((field) => {
|
||||||
promises.push(this.getDataForField(field, signup, registerAuth, formValues).then((data) => {
|
promises.push(this.getDataForField(field, signup, registerAuth, formValues).then((data) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { CoreLoggerProvider } from '@providers/logger';
|
||||||
import { CoreSite } from '@classes/site';
|
import { CoreSite } from '@classes/site';
|
||||||
import { CoreSitesProvider } from '@providers/sites';
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to provide user functionalities.
|
* Service to provide user functionalities.
|
||||||
|
@ -53,7 +54,8 @@ export class CoreUserProvider {
|
||||||
|
|
||||||
protected logger;
|
protected logger;
|
||||||
|
|
||||||
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider) {
|
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider,
|
||||||
|
private filepoolProvider: CoreFilepoolProvider) {
|
||||||
this.logger = logger.getInstance('CoreUserProvider');
|
this.logger = logger.getInstance('CoreUserProvider');
|
||||||
this.sitesProvider.createTablesFromSchema(this.tablesSchema);
|
this.sitesProvider.createTablesFromSchema(this.tablesSchema);
|
||||||
}
|
}
|
||||||
|
@ -366,6 +368,36 @@ export class CoreUserProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch user profiles and their images from a certain course. It prevents duplicates.
|
||||||
|
*
|
||||||
|
* @param {number[]} userIds List of user IDs.
|
||||||
|
* @param {number} [courseId] Course the users belong to.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Promise resolved when prefetched.
|
||||||
|
*/
|
||||||
|
prefetchProfiles(userIds: number[], courseId?: number, siteId?: string): Promise<any> {
|
||||||
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||||
|
|
||||||
|
const treated = {},
|
||||||
|
promises = [];
|
||||||
|
|
||||||
|
userIds.forEach((userId) => {
|
||||||
|
// Prevent repeats and errors.
|
||||||
|
if (!treated[userId]) {
|
||||||
|
treated[userId] = true;
|
||||||
|
|
||||||
|
promises.push(this.getProfile(userId, courseId).then((profile) => {
|
||||||
|
if (profile.profileimageurl) {
|
||||||
|
this.filepoolProvider.addToQueueByUrl(siteId, profile.profileimageurl);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store user basic information in local DB to be retrieved if the WS call fails.
|
* Store user basic information in local DB to be retrieved if the WS call fails.
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
// (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 { Directive, Input, OnDestroy, OnInit, ElementRef, OnChanges } from '@angular/core';
|
||||||
|
import { Chart } from 'chart.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component shows a chart using chart.js.
|
||||||
|
* Documentation can be found at http://www.chartjs.org/docs/.
|
||||||
|
* It only supports changes on these properties: data and labels.
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
* <canvas core-chart [data]="data" [labels]="labels" [type]="type" [legend]="legend"></canvas>
|
||||||
|
*/
|
||||||
|
@Directive({
|
||||||
|
selector: 'canvas[core-chart]'
|
||||||
|
})
|
||||||
|
export class CoreChartDirective implements OnDestroy, OnInit, OnChanges {
|
||||||
|
// The first 6 colors will be the app colors, the following will be randomly generated.
|
||||||
|
// It will use the same colors in the whole session.
|
||||||
|
protected static backgroundColors = [
|
||||||
|
'rgba(0,100,210, 0.6)',
|
||||||
|
'rgba(203,61,77, 0.6)',
|
||||||
|
'rgba(0,121,130, 0.6)',
|
||||||
|
'rgba(249,128,18, 0.6)',
|
||||||
|
'rgba(94,129,0, 0.6)',
|
||||||
|
'rgba(251,173,26, 0.6)'
|
||||||
|
];
|
||||||
|
|
||||||
|
@Input() data: any[]; // Chart data.
|
||||||
|
@Input() labels = []; // Labels of the data.
|
||||||
|
@Input() type: string; // Type of chart.
|
||||||
|
@Input() legend: any; // Legend options.
|
||||||
|
|
||||||
|
chart: any;
|
||||||
|
protected element: ElementRef;
|
||||||
|
|
||||||
|
constructor(element: ElementRef) {
|
||||||
|
this.element = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): any {
|
||||||
|
let legend = {};
|
||||||
|
if (typeof this.legend == 'undefined') {
|
||||||
|
legend = {
|
||||||
|
display: true,
|
||||||
|
position: 'bottom',
|
||||||
|
labels: {
|
||||||
|
generateLabels: (chart): any => {
|
||||||
|
const data = chart.data;
|
||||||
|
if (data.labels.length && data.labels.length) {
|
||||||
|
const datasets = data.datasets[0];
|
||||||
|
|
||||||
|
return data.labels.map((label, i): any => {
|
||||||
|
return {
|
||||||
|
text: label + ': ' + datasets.data[i],
|
||||||
|
fillStyle: datasets.backgroundColor[i]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
legend = Object.assign({}, this.legend);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.type == 'bar' && this.data.length >= 5) {
|
||||||
|
this.type = 'horizontalBar';
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = this.element.nativeElement.getContext('2d');
|
||||||
|
this.chart = new Chart(context, {
|
||||||
|
type: this.type,
|
||||||
|
data: {
|
||||||
|
labels: this.labels,
|
||||||
|
datasets: [{
|
||||||
|
data: this.data,
|
||||||
|
backgroundColor: this.getRandomColors(this.data.length)
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {legend: legend}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen to chart changes.
|
||||||
|
*/
|
||||||
|
ngOnChanges(): void {
|
||||||
|
if (this.chart) {
|
||||||
|
this.chart.data.datasets[0] = {
|
||||||
|
data: this.data,
|
||||||
|
backgroundColor: this.getRandomColors(this.data.length)
|
||||||
|
};
|
||||||
|
this.chart.data.labels = this.labels;
|
||||||
|
this.chart.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate random colors if needed.
|
||||||
|
*
|
||||||
|
* @param {number} n Number of colors needed.
|
||||||
|
* @return {any[]} Array with the number of background colors requested.
|
||||||
|
*/
|
||||||
|
protected getRandomColors(n: number): any[] {
|
||||||
|
while (CoreChartDirective.backgroundColors.length < n) {
|
||||||
|
const red = Math.floor(Math.random() * 255),
|
||||||
|
green = Math.floor(Math.random() * 255),
|
||||||
|
blue = Math.floor(Math.random() * 255);
|
||||||
|
CoreChartDirective.backgroundColors.push('rgba(' + red + ', ' + green + ', ' + blue + ', 0.6)');
|
||||||
|
}
|
||||||
|
|
||||||
|
return CoreChartDirective.backgroundColors.slice(0, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): any {
|
||||||
|
if (this.chart) {
|
||||||
|
this.chart.destroy();
|
||||||
|
this.chart = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import { CoreKeepKeyboardDirective } from './keep-keyboard';
|
||||||
import { CoreUserLinkDirective } from './user-link';
|
import { CoreUserLinkDirective } from './user-link';
|
||||||
import { CoreAutoRowsDirective } from './auto-rows';
|
import { CoreAutoRowsDirective } from './auto-rows';
|
||||||
import { CoreLongPressDirective } from './long-press';
|
import { CoreLongPressDirective } from './long-press';
|
||||||
|
import { CoreChartDirective } from './chart';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -33,7 +34,8 @@ import { CoreLongPressDirective } from './long-press';
|
||||||
CoreLinkDirective,
|
CoreLinkDirective,
|
||||||
CoreUserLinkDirective,
|
CoreUserLinkDirective,
|
||||||
CoreAutoRowsDirective,
|
CoreAutoRowsDirective,
|
||||||
CoreLongPressDirective
|
CoreLongPressDirective,
|
||||||
|
CoreChartDirective
|
||||||
],
|
],
|
||||||
imports: [],
|
imports: [],
|
||||||
exports: [
|
exports: [
|
||||||
|
@ -45,7 +47,8 @@ import { CoreLongPressDirective } from './long-press';
|
||||||
CoreLinkDirective,
|
CoreLinkDirective,
|
||||||
CoreUserLinkDirective,
|
CoreUserLinkDirective,
|
||||||
CoreAutoRowsDirective,
|
CoreAutoRowsDirective,
|
||||||
CoreLongPressDirective
|
CoreLongPressDirective,
|
||||||
|
CoreChartDirective
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CoreDirectivesModule {}
|
export class CoreDirectivesModule {}
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"allparticipants": "All participants",
|
"allparticipants": "All participants",
|
||||||
"android": "Android",
|
"android": "Android",
|
||||||
|
"answer": "Answer",
|
||||||
|
"answered": "Answered",
|
||||||
"areyousure": "Are you sure?",
|
"areyousure": "Are you sure?",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
|
|
@ -160,7 +160,7 @@ export class CoreLangProvider {
|
||||||
return language;
|
return language;
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// User hasn't defined a language. If default language is forced, use it.
|
// User hasn't defined a language. If default language is forced, use it.
|
||||||
if (!CoreConfigConstants.forcedefaultlanguage) {
|
if (CoreConfigConstants.default_lang && !CoreConfigConstants.forcedefaultlanguage) {
|
||||||
return CoreConfigConstants.default_lang;
|
return CoreConfigConstants.default_lang;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -170,9 +170,10 @@ export class CoreUtilsProvider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blocks leaving a view.
|
* Blocks leaving a view.
|
||||||
|
* @deprecated, use ionViewCanLeave instead.
|
||||||
*/
|
*/
|
||||||
blockLeaveView(): void {
|
blockLeaveView(): void {
|
||||||
// @todo
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -139,6 +139,7 @@ $item-ios-avatar-size: 54px;
|
||||||
$loading-ios-spinner-color: $core-color;
|
$loading-ios-spinner-color: $core-color;
|
||||||
$spinner-ios-ios-color: $core-color;
|
$spinner-ios-ios-color: $core-color;
|
||||||
$tabs-ios-tab-color-inactive: $tabs-tab-color-inactive;
|
$tabs-ios-tab-color-inactive: $tabs-tab-color-inactive;
|
||||||
|
$button-ios-outline-background-color: $white;
|
||||||
|
|
||||||
|
|
||||||
// App Material Design Variables
|
// App Material Design Variables
|
||||||
|
@ -152,6 +153,7 @@ $item-md-avatar-size: 54px;
|
||||||
$loading-md-spinner-color: $core-color;
|
$loading-md-spinner-color: $core-color;
|
||||||
$spinner-md-crescent-color: $core-color;
|
$spinner-md-crescent-color: $core-color;
|
||||||
$tabs-md-tab-color-inactive: $tabs-tab-color-inactive;
|
$tabs-md-tab-color-inactive: $tabs-tab-color-inactive;
|
||||||
|
$button-md-outline-background-color: $white;
|
||||||
|
|
||||||
|
|
||||||
// App Windows Variables
|
// App Windows Variables
|
||||||
|
@ -164,6 +166,7 @@ $item-wp-avatar-size: 54px;
|
||||||
$loading-wp-spinner-color: $core-color;
|
$loading-wp-spinner-color: $core-color;
|
||||||
$spinner-wp-circles-color: $core-color;
|
$spinner-wp-circles-color: $core-color;
|
||||||
$tabs-wp-tab-color-inactive: $tabs-tab-color-inactive;
|
$tabs-wp-tab-color-inactive: $tabs-tab-color-inactive;
|
||||||
|
$button-wp-outline-background-color: $white;
|
||||||
|
|
||||||
|
|
||||||
// App Theme
|
// App Theme
|
||||||
|
|
Loading…
Reference in New Issue