MOBILE-2339 feedback: Implement Index page
parent
0da7b03e3d
commit
c586db40dd
|
@ -65,6 +65,7 @@
|
|||
"@types/cordova-plugin-network-information": "0.0.3",
|
||||
"@types/node": "^8.0.47",
|
||||
"@types/promise.prototype.finally": "^2.0.2",
|
||||
"chart.js": "^2.7.2",
|
||||
"electron-builder-squirrel-windows": "^19.3.0",
|
||||
"electron-windows-notifications": "^1.1.13",
|
||||
"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">
|
||||
<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}}. {{ item.name }}</h2>
|
||||
<p>{{ item.label }}</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,435 @@
|
|||
// (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;
|
||||
|
||||
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;
|
||||
|
||||
if (this.tab == 'analysis') {
|
||||
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): 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,57 @@
|
|||
// (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 { 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,
|
||||
AddonModFeedbackSyncCronHandler,
|
||||
AddonModFeedbackSyncProvider,
|
||||
AddonModFeedbackOfflineProvider
|
||||
]
|
||||
})
|
||||
export class AddonModFeedbackModule {
|
||||
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModFeedbackModuleHandler,
|
||||
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModFeedbackPrefetchHandler,
|
||||
contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModFeedbackLinkHandler,
|
||||
cronDelegate: CoreCronDelegate, syncHandler: AddonModFeedbackSyncCronHandler) {
|
||||
moduleDelegate.registerHandler(moduleHandler);
|
||||
prefetchDelegate.registerHandler(prefetchHandler);
|
||||
contentLinksDelegate.registerHandler(linkHandler);
|
||||
cronDelegate.register(syncHandler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"analysis": "Analysis",
|
||||
"anonymous": "Anonymous",
|
||||
"average": "Average",
|
||||
"completed_feedbacks": "Submitted answers",
|
||||
"complete_the_form": "Answer the questions...",
|
||||
"continue_the_form": "Continue the form",
|
||||
"feedbackclose": "Allow answers to",
|
||||
"feedbackopen": "Allow answers from",
|
||||
"mode": "Mode",
|
||||
"non_anonymous": "User's name will be logged and shown with answers",
|
||||
"overview": "Overview",
|
||||
"page_after_submit": "Completion message",
|
||||
"preview": "Preview",
|
||||
"questions": "Questions",
|
||||
"show_nonrespondents": "Show non-respondents",
|
||||
"this_feedback_is_already_submitted": "You've already completed this activity."
|
||||
}
|
|
@ -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,548 @@
|
|||
// (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 { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||
|
||||
/**
|
||||
* Service that provides some features for feedbacks.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModFeedbackProvider {
|
||||
static COMPONENT = 'mmaModFeedback';
|
||||
static FORM_SUBMITTED = 'addon_mod_feedback_form_submitted';
|
||||
|
||||
protected ROOT_CACHE_KEY = this.ROOT_CACHE_KEY + '';
|
||||
protected logger;
|
||||
|
||||
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider,
|
||||
private filepoolProvider: CoreFilepoolProvider) {
|
||||
this.logger = logger.getInstance('AddonModFeedbackProvider');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analysis information for a given feedback.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @param {number} [groupId] Group ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the feedback is retrieved.
|
||||
*/
|
||||
getAnalysis(feedbackId: number, groupId?: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const params = {
|
||||
feedbackid: feedbackId
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getAnalysisDataCacheKey(feedbackId, groupId)
|
||||
};
|
||||
|
||||
if (groupId) {
|
||||
params['groupid'] = groupId;
|
||||
}
|
||||
|
||||
return site.read('mod_feedback_get_analysis', params, preSets);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for feedback analysis data WS calls.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @param {number} [groupId=0] Group ID.
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getAnalysisDataCacheKey(feedbackId: number, groupId: number = 0): string {
|
||||
return this.getAnalysisDataPrefixCacheKey(feedbackId) + groupId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get prefix cache key for feedback analysis data WS calls.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getAnalysisDataPrefixCacheKey(feedbackId: number): string {
|
||||
return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':analysis:';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get prefix cache key for feedback completion data WS calls.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getCompletedDataCacheKey(feedbackId: number): string {
|
||||
return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':completed:';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the temporary completion timemodified for the current user.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getCurrentCompletedTimeModified(feedbackId: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const params = {
|
||||
feedbackid: feedbackId
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getCurrentCompletedTimeModifiedDataCacheKey(feedbackId)
|
||||
};
|
||||
|
||||
return site.read('mod_feedback_get_current_completed_tmp', params, preSets).then((response) => {
|
||||
if (response && typeof response.feedback != 'undefined' && typeof response.feedback.timemodified != 'undefined') {
|
||||
return response.feedback.timemodified;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}).catch(() => {
|
||||
// Ignore errors.
|
||||
return 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get prefix cache key for feedback current completed temp data WS calls.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getCurrentCompletedTimeModifiedDataCacheKey(feedbackId: number): string {
|
||||
return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':completedtime:';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the temporary completion record for the current user.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @param {boolean} [offline=false] True if it should return cached data. Has priority over ignoreCache.
|
||||
* @param {boolean} [ignoreCache=false] True if it should ignore cached data (it always fail in offline or server down).
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getCurrentValues(feedbackId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const params = {
|
||||
feedbackid: feedbackId
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getCurrentValuesDataCacheKey(feedbackId)
|
||||
};
|
||||
|
||||
if (offline) {
|
||||
preSets['omitExpires'] = true;
|
||||
} else if (ignoreCache) {
|
||||
preSets['getFromCache'] = false;
|
||||
preSets['emergencyCache'] = false;
|
||||
}
|
||||
|
||||
return site.read('mod_feedback_get_unfinished_responses', params, preSets).then((response) => {
|
||||
if (response && typeof response.responses != 'undefined') {
|
||||
return response.responses;
|
||||
}
|
||||
|
||||
return Promise.reject(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for get current values feedback data WS calls.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getCurrentValuesDataCacheKey(feedbackId: number): string {
|
||||
return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':currentvalues';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access information for a given feedback.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @param {boolean} [offline=false] True if it should return cached data. Has priority over ignoreCache.
|
||||
* @param {boolean} [ignoreCache=false] True if it should ignore cached data (it always fail in offline or server down).
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the feedback is retrieved.
|
||||
*/
|
||||
getFeedbackAccessInformation(feedbackId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string):
|
||||
Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const params = {
|
||||
feedbackid: feedbackId
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getFeedbackAccessInformationDataCacheKey(feedbackId)
|
||||
};
|
||||
|
||||
if (offline) {
|
||||
preSets['omitExpires'] = true;
|
||||
} else if (ignoreCache) {
|
||||
preSets['getFromCache'] = false;
|
||||
preSets['emergencyCache'] = false;
|
||||
}
|
||||
|
||||
return site.read('mod_feedback_get_feedback_access_information', params, preSets);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for feedback access information data WS calls.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getFeedbackAccessInformationDataCacheKey(feedbackId: number): string {
|
||||
return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':access';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for feedback data WS calls.
|
||||
*
|
||||
* @param {number} courseId Course ID.
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getFeedbackCacheKey(courseId: number): string {
|
||||
return this.ROOT_CACHE_KEY + 'feedback:' + courseId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get prefix cache key for all feedback activity data WS calls.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getFeedbackDataPrefixCacheKey(feedbackId: number): string {
|
||||
return this.ROOT_CACHE_KEY + feedbackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a feedback with key=value. If more than one is found, only the first will be returned.
|
||||
*
|
||||
* @param {number} courseId Course ID.
|
||||
* @param {string} key Name of the property to check.
|
||||
* @param {any} value Value to search.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false.
|
||||
* @return {Promise<any>} Promise resolved when the feedback is retrieved.
|
||||
*/
|
||||
protected getFeedbackDataByKey(courseId: number, key: string, value: any, siteId?: string, forceCache?: boolean): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const params = {
|
||||
courseids: [courseId]
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getFeedbackCacheKey(courseId)
|
||||
};
|
||||
|
||||
if (forceCache) {
|
||||
preSets['omitExpires'] = true;
|
||||
}
|
||||
|
||||
return site.read('mod_feedback_get_feedbacks_by_courses', params, preSets).then((response) => {
|
||||
if (response && response.feedbacks) {
|
||||
const currentFeedback = response.feedbacks.find((feedback) => {
|
||||
return feedback[key] == value;
|
||||
});
|
||||
if (currentFeedback) {
|
||||
return currentFeedback;
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a feedback by course module ID.
|
||||
*
|
||||
* @param {number} courseId Course ID.
|
||||
* @param {number} cmId Course module ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false.
|
||||
* @return {Promise<any>} Promise resolved when the feedback is retrieved.
|
||||
*/
|
||||
getFeedback(courseId: number, cmId: number, siteId?: string, forceCache?: boolean): Promise<any> {
|
||||
return this.getFeedbackDataByKey(courseId, 'coursemodule', cmId, siteId, forceCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a feedback by ID.
|
||||
*
|
||||
* @param {number} courseId Course ID.
|
||||
* @param {number} id Feedback ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @param {boolean} [forceCache] True to always get the value from cache, false otherwise. Default false.
|
||||
* @return {Promise<any>} Promise resolved when the feedback is retrieved.
|
||||
*/
|
||||
getFeedbackById(courseId: number, id: number, siteId?: string, forceCache?: boolean): Promise<any> {
|
||||
return this.getFeedbackDataByKey(courseId, 'id', id, siteId, forceCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the resume page information.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @param {boolean} [offline=false] True if it should return cached data. Has priority over ignoreCache.
|
||||
* @param {boolean} [ignoreCache=false] True if it should ignore cached data (it always fail in offline or server down).
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getResumePage(feedbackId: number, offline: boolean = false, ignoreCache: boolean = false, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const params = {
|
||||
feedbackid: feedbackId
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getResumePageDataCacheKey(feedbackId)
|
||||
};
|
||||
|
||||
if (offline) {
|
||||
preSets['omitExpires'] = true;
|
||||
} else if (ignoreCache) {
|
||||
preSets['getFromCache'] = false;
|
||||
preSets['emergencyCache'] = false;
|
||||
}
|
||||
|
||||
return site.read('mod_feedback_launch_feedback', params, preSets).then((response) => {
|
||||
if (response && typeof response.gopage != 'undefined') {
|
||||
// WS will return -1 for last page but the user need to start again.
|
||||
return response.gopage > 0 ? response.gopage : 0;
|
||||
}
|
||||
|
||||
return Promise.reject(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get prefix cache key for resume feedback page data WS calls.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getResumePageDataCacheKey(feedbackId: number): string {
|
||||
return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':launch';
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates feedback data except files and module info.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateAllFeedbackData(feedbackId: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.invalidateWsCacheForKeyStartingWith(this.getFeedbackDataPrefixCacheKey(feedbackId));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates feedback analysis data.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateAnalysisData(feedbackId: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.invalidateWsCacheForKeyStartingWith(this.getAnalysisDataPrefixCacheKey(feedbackId));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the prefetched content.
|
||||
* To invalidate files, use AddonFeedbackProvider#invalidateFiles.
|
||||
*
|
||||
* @param {number} moduleId The module ID.
|
||||
* @param {number} courseId Course ID of the module.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.getFeedback(courseId, moduleId, siteId).then((feedback) => {
|
||||
const ps = [];
|
||||
|
||||
// Do not invalidate module data before getting module info, we need it!
|
||||
ps.push(this.invalidateFeedbackData(courseId, siteId));
|
||||
ps.push(this.invalidateAllFeedbackData(feedback.id, siteId));
|
||||
|
||||
return Promise.all(ps);
|
||||
}));
|
||||
|
||||
promises.push(this.invalidateFiles(moduleId, siteId));
|
||||
|
||||
return this.utils.allPromises(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates temporary completion record data.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateCurrentValuesData(feedbackId: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.invalidateWsCacheForKey(this.getCurrentValuesDataCacheKey(feedbackId));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates feedback access information data.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateFeedbackAccessInformationData(feedbackId: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.invalidateWsCacheForKey(this.getFeedbackAccessInformationDataCacheKey(feedbackId));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates feedback data.
|
||||
*
|
||||
* @param {number} courseId Course ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateFeedbackData(courseId: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.invalidateWsCacheForKey(this.getFeedbackCacheKey(courseId));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the prefetched files.
|
||||
*
|
||||
* @param {number} moduleId The module ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the files are invalidated.
|
||||
*/
|
||||
invalidateFiles(moduleId: number, siteId?: string): Promise<any> {
|
||||
return this.filepoolProvider.invalidateFilesByComponent(siteId, AddonModFeedbackProvider.COMPONENT, moduleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates launch feedback data.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateResumePageData(feedbackId: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.invalidateWsCacheForKey(this.getResumePageDataCacheKey(feedbackId));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if feedback has been completed
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the info is retrieved.
|
||||
*/
|
||||
isCompleted(feedbackId: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const params = {
|
||||
feedbackid: feedbackId
|
||||
},
|
||||
preSets = {
|
||||
cacheKey: this.getCompletedDataCacheKey(feedbackId)
|
||||
};
|
||||
|
||||
return this.utils.promiseWorks(site.read('mod_feedback_get_last_completed', params, preSets));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether or not the plugin is enabled in a certain site. Plugin is enabled if the feedback WS are available.
|
||||
*
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<boolean>} Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise.
|
||||
* @since 3.3
|
||||
*/
|
||||
isPluginEnabled(siteId?: string): Promise<boolean> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.wsAvailable('mod_feedback_get_feedbacks_by_courses') &&
|
||||
site.wsAvailable('mod_feedback_get_feedback_access_information');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Report the feedback as being viewed.
|
||||
*
|
||||
* @param {number} id Module ID.
|
||||
* @param {boolean} [formViewed=false] True if form was viewed.
|
||||
* @return {Promise<any>} Promise resolved when the WS call is successful.
|
||||
*/
|
||||
logView(id: number, formViewed: boolean = false): Promise<any> {
|
||||
const params = {
|
||||
feedbackid: id,
|
||||
moduleviewed: formViewed ? 1 : 0
|
||||
};
|
||||
|
||||
return this.sitesProvider.getCurrentSite().write('mod_feedback_view_feedback', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a jump between pages.
|
||||
*
|
||||
* @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 {boolean} goPrevious Whether we want to jump to previous page.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the info is retrieved.
|
||||
*/
|
||||
processPageOnline(feedbackId: number, page: number, responses: any, goPrevious: boolean, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const params = {
|
||||
feedbackid: feedbackId,
|
||||
page: page,
|
||||
responses: this.utils.objectToArrayOfObjects(responses, 'name', 'value'),
|
||||
goprevious: goPrevious ? 1 : 0
|
||||
};
|
||||
|
||||
return site.write('mod_feedback_process_page', params).catch((error) => {
|
||||
return this.utils.createFakeWSError(error);
|
||||
}).then((response) => {
|
||||
// Invalidate and update current values because they will change.
|
||||
return this.invalidateCurrentValuesData(feedbackId, site.getId()).then(() => {
|
||||
return this.getCurrentValues(feedbackId, false, false, site.getId());
|
||||
}).catch(() => {
|
||||
// Ignore errors.
|
||||
}).then(() => {
|
||||
return response;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
// (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';
|
||||
|
||||
/**
|
||||
* Service that provides helper functions for feedbacks.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModFeedbackHelperProvider {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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,71 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { 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 = '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,163 @@
|
|||
// (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 = 'mma_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});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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().insertOrUpdateRecord(this.FEEDBACK_TABLE, entry, {feedbackid: feedbackId, page: page});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,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,254 @@
|
|||
// (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,
|
||||
private translate: TranslateService, private utils: CoreUtilsProvider, protected textUtils: CoreTextUtilsProvider,
|
||||
courseProvider: CoreCourseProvider, syncProvider: CoreSyncProvider) {
|
||||
super('AddonModFeedbackSyncProvider', sitesProvider, loggerProvider, appProvider, syncProvider, textUtils);
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ynchronize 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.
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -79,6 +79,7 @@ import { AddonFilesModule } from '@addon/files/files.module';
|
|||
import { AddonModBookModule } from '@addon/mod/book/book.module';
|
||||
import { AddonModLabelModule } from '@addon/mod/label/label.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 { AddonModPageModule } from '@addon/mod/page/page.module';
|
||||
import { AddonModQuizModule } from '@addon/mod/quiz/quiz.module';
|
||||
|
@ -173,6 +174,7 @@ export const CORE_PROVIDERS: any[] = [
|
|||
AddonModBookModule,
|
||||
AddonModLabelModule,
|
||||
AddonModResourceModule,
|
||||
AddonModFeedbackModule,
|
||||
AddonModFolderModule,
|
||||
AddonModPageModule,
|
||||
AddonModQuizModule,
|
||||
|
|
|
@ -591,6 +591,11 @@ textarea {
|
|||
}
|
||||
}
|
||||
|
||||
canvas[core-chart] {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.core-circle:before {
|
||||
content: ' \25CF';
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ core-tabs {
|
|||
}
|
||||
}
|
||||
|
||||
.scroll-content.no-scroll {
|
||||
:not(.has-refresher) > .scroll-content.no-scroll {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -54,7 +54,6 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
|||
this.courseProvider = injector.get(CoreCourseProvider);
|
||||
this.appProvider = injector.get(CoreAppProvider);
|
||||
this.eventsProvider = injector.get(CoreEventsProvider);
|
||||
this.modulePrefetchProvider = injector.get(CoreCourseModulePrefetchDelegate);
|
||||
|
||||
const network = injector.get(Network);
|
||||
|
||||
|
@ -78,14 +77,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
|||
if (this.syncEventName) {
|
||||
// Refresh data if this discussion is synchronized automatically.
|
||||
this.syncObserver = this.eventsProvider.on(this.syncEventName, (data) => {
|
||||
if (this.isRefreshSyncNeeded(data)) {
|
||||
// Refresh the data.
|
||||
this.loaded = false;
|
||||
this.refreshIcon = 'spinner';
|
||||
this.syncIcon = 'spinner';
|
||||
|
||||
this.refreshContent(false);
|
||||
}
|
||||
this.autoSyncEventReceived(data);
|
||||
}, this.siteId);
|
||||
}
|
||||
}
|
||||
|
@ -100,12 +92,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
|||
*/
|
||||
doRefresh(refresher?: any, done?: () => void, showErrors: boolean = false): Promise<any> {
|
||||
if (this.loaded) {
|
||||
this.refreshIcon = 'spinner';
|
||||
this.syncIcon = 'spinner';
|
||||
|
||||
return this.refreshContent(true, showErrors).finally(() => {
|
||||
this.refreshIcon = 'refresh';
|
||||
this.syncIcon = 'sync';
|
||||
refresher && refresher.complete();
|
||||
done && done();
|
||||
});
|
||||
|
@ -124,6 +111,20 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
|||
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.
|
||||
*
|
||||
|
@ -132,10 +133,16 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
|||
* @return {Promise<any>} Resolved when done.
|
||||
*/
|
||||
protected refreshContent(sync: boolean = false, showErrors: boolean = false): Promise<any> {
|
||||
this.refreshIcon = 'spinner';
|
||||
this.syncIcon = 'spinner';
|
||||
|
||||
return this.invalidateContent().catch(() => {
|
||||
// Ignore errors.
|
||||
}).then(() => {
|
||||
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> {
|
||||
if (this.loaded) {
|
||||
this.refreshIcon = 'spinner';
|
||||
|
||||
return this.refreshContent().finally(() => {
|
||||
this.refreshIcon = 'refresh';
|
||||
refresher && refresher.complete();
|
||||
done && done();
|
||||
});
|
||||
|
@ -97,10 +94,14 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
|
|||
* @return {Promise<any>} Resolved when done.
|
||||
*/
|
||||
protected refreshContent(): Promise<any> {
|
||||
this.refreshIcon = 'spinner';
|
||||
|
||||
return this.invalidateContent().catch(() => {
|
||||
// Ignore errors.
|
||||
}).then(() => {
|
||||
return this.loadContent(true);
|
||||
}).finally(() => {
|
||||
this.refreshIcon = 'refresh';
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import { CoreLoggerProvider } from '@providers/logger';
|
|||
import { CoreSite } from '@classes/site';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||
|
||||
/**
|
||||
* Service to provide user functionalities.
|
||||
|
@ -53,7 +54,8 @@ export class CoreUserProvider {
|
|||
|
||||
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.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.
|
||||
*
|
||||
|
|
|
@ -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 does not support changes on any input.
|
||||
*
|
||||
* 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 { CoreAutoRowsDirective } from './auto-rows';
|
||||
import { CoreLongPressDirective } from './long-press';
|
||||
import { CoreChartDirective } from './chart';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -33,7 +34,8 @@ import { CoreLongPressDirective } from './long-press';
|
|||
CoreLinkDirective,
|
||||
CoreUserLinkDirective,
|
||||
CoreAutoRowsDirective,
|
||||
CoreLongPressDirective
|
||||
CoreLongPressDirective,
|
||||
CoreChartDirective
|
||||
],
|
||||
imports: [],
|
||||
exports: [
|
||||
|
@ -45,7 +47,8 @@ import { CoreLongPressDirective } from './long-press';
|
|||
CoreLinkDirective,
|
||||
CoreUserLinkDirective,
|
||||
CoreAutoRowsDirective,
|
||||
CoreLongPressDirective
|
||||
CoreLongPressDirective,
|
||||
CoreChartDirective
|
||||
]
|
||||
})
|
||||
export class CoreDirectivesModule {}
|
||||
|
|
|
@ -160,7 +160,7 @@ export class CoreLangProvider {
|
|||
return language;
|
||||
}).catch(() => {
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -139,6 +139,7 @@ $item-ios-avatar-size: 54px;
|
|||
$loading-ios-spinner-color: $core-color;
|
||||
$spinner-ios-ios-color: $core-color;
|
||||
$tabs-ios-tab-color-inactive: $tabs-tab-color-inactive;
|
||||
$button-ios-outline-background-color: $white;
|
||||
|
||||
|
||||
// App Material Design Variables
|
||||
|
@ -152,6 +153,7 @@ $item-md-avatar-size: 54px;
|
|||
$loading-md-spinner-color: $core-color;
|
||||
$spinner-md-crescent-color: $core-color;
|
||||
$tabs-md-tab-color-inactive: $tabs-tab-color-inactive;
|
||||
$button-md-outline-background-color: $white;
|
||||
|
||||
|
||||
// App Windows Variables
|
||||
|
@ -164,6 +166,7 @@ $item-wp-avatar-size: 54px;
|
|||
$loading-wp-spinner-color: $core-color;
|
||||
$spinner-wp-circles-color: $core-color;
|
||||
$tabs-wp-tab-color-inactive: $tabs-tab-color-inactive;
|
||||
$button-wp-outline-background-color: $white;
|
||||
|
||||
|
||||
// App Theme
|
||||
|
|
Loading…
Reference in New Issue