MOBILE-2339 feedback: Implement Index page

main
Pau Ferrer Ocaña 2018-03-16 13:44:57 +01:00
parent 0da7b03e3d
commit c586db40dd
27 changed files with 2490 additions and 22 deletions

View File

@ -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",

View File

@ -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 {}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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."
}

View File

@ -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>

View File

@ -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 {}

View File

@ -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;
}
}

View File

@ -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;
});
});
});
}
}

View File

@ -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);
}
}

View File

@ -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');
}
}

View File

@ -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;
}
}

View File

@ -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});
});
}
}

View File

@ -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();
}
}

View File

@ -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.
}
}

View File

@ -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);
}
});
}
}

View File

@ -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,

View File

@ -591,6 +591,11 @@ textarea {
}
}
canvas[core-chart] {
max-width: 500px;
margin: 0 auto;
}
.core-circle:before {
content: ' \25CF';
}

View File

@ -56,7 +56,7 @@ core-tabs {
}
}
.scroll-content.no-scroll {
:not(.has-refresher) > .scroll-content.no-scroll {
overflow: hidden !important;
}

View File

@ -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';
});
}

View File

@ -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';
});
}

View File

@ -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.
*

View File

@ -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;
}
}
}

View File

@ -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 {}

View File

@ -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;
}

View File

@ -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