MOBILE-2339 feedback: Implement form page
parent
fca428843e
commit
d8d34a786a
|
@ -338,11 +338,11 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity
|
|||
*
|
||||
* @param {boolean} preview Preview or edit the form.
|
||||
*/
|
||||
gotoAnswerQuestions(preview: boolean): void {
|
||||
gotoAnswerQuestions(preview: boolean = false): void {
|
||||
const stateParams = {
|
||||
module: this.module,
|
||||
moduleid: this.module.id,
|
||||
courseid: this.courseId,
|
||||
moduleId: this.module.id,
|
||||
courseId: this.courseId,
|
||||
preview: preview
|
||||
};
|
||||
this.navCtrl.push('AddonModFeedbackFormPage', stateParams);
|
||||
|
|
|
@ -3,24 +3,32 @@
|
|||
"anonymous": "Anonymous",
|
||||
"anonymous_entries": "Anonymous entries ({{$a}})",
|
||||
"average": "Average",
|
||||
"captchaofflinewarning": "Feedback with CAPTCHA cannot be completed offline, or if not configured, or if the server is down.",
|
||||
"completed_feedbacks": "Submitted answers",
|
||||
"complete_the_form": "Answer the questions...",
|
||||
"continue_the_form": "Continue the form",
|
||||
"feedbackclose": "Allow answers to",
|
||||
"feedbackopen": "Allow answers from",
|
||||
"feedback_is_not_open": "The feedback is not open",
|
||||
"feedback_submitted_offline": "This feedback has been saved to be submitted later.",
|
||||
"mapcourses": "Map feedback to courses",
|
||||
"mode": "Mode",
|
||||
"next_page": "Next page",
|
||||
"non_anonymous": "User's name will be logged and shown with answers",
|
||||
"non_anonymous_entries": "Non anonymous entries ({{$a}})",
|
||||
"non_respondents_students": "Non respondents students ({{$a}})",
|
||||
"not_selected": "Not selected",
|
||||
"not_started": "Not started",
|
||||
"numberoutofrange": "Number out of range",
|
||||
"overview": "Overview",
|
||||
"page_after_submit": "Completion message",
|
||||
"preview": "Preview",
|
||||
"previous_page": "Previous page",
|
||||
"questions": "Questions",
|
||||
"responses": "Responses",
|
||||
"response_nr": "Response number",
|
||||
"save_entries": "Submit your answers",
|
||||
"show_entries": "Show responses",
|
||||
"show_nonrespondents": "Show non-respondents",
|
||||
"started": "Started",
|
||||
"this_feedback_is_already_submitted": "You've already completed this activity."
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title><core-format-text [text]=" title "></core-format-text></ion-title>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<core-loading [hideUntil]="feedbackLoaded">
|
||||
<ng-container *ngIf="items && items.length">
|
||||
<ion-list no-margin>
|
||||
<ion-item text-wrap>
|
||||
<h2>{{ 'addon.mod_feedback.mode' | translate }}</h2>
|
||||
<p *ngIf="access.isanonymous">{{ 'addon.mod_feedback.anonymous' | translate }}</p>
|
||||
<p *ngIf="!access.isanonymous">{{ 'addon.mod_feedback.non_anonymous' | translate }}</p>
|
||||
</ion-item>
|
||||
<ng-container *ngFor="let item of items">
|
||||
<ion-item-divider *ngIf="item.typ == 'pagebreak'" color="light"></ion-item-divider>
|
||||
<ion-item text-wrap *ngIf="item.typ != 'pagebreak'" [color]="item.dependitem > 0 ? 'light' : ''" [class.core-danger-item]="item.isEmpty || item.hasError">
|
||||
<ion-label *ngIf="item.name" [core-mark-required]="item.required" stacked>
|
||||
<span *ngIf="item.itemnumber">{{item.itemnumber}}. </span>{{ item.name }}
|
||||
</ion-label>
|
||||
<div item-content class="addon-mod_feedback-form-content" *ngIf="item.template">
|
||||
<ng-container [ngSwitch]="item.template">
|
||||
<ng-container *ngSwitchCase="'label'">
|
||||
<p><core-format-text [component]="component" [componentId]="componentId" [text]="item.presentation"></core-format-text></p>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'textfield'">
|
||||
<ion-input type="text" [(ngModel)]="item.value" autocorrect="off" name="{{item.typ}}_{{item.id}}" maxlength="{{item.maxlength}}" [required]="item.required"></ion-input>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'numeric'">
|
||||
<ion-input [required]="item.required" name="{{item.typ}}_{{item.id}}" type="number" [(ngModel)]="item.value"></ion-input>
|
||||
<p *ngIf="item.hasError" color="error">{{ 'addon.mod_feedback.numberoutofrange' |translate }} [{{item.rangefrom}}<span *ngIf="item.rangefrom && item.rangeto">, </span>{{item.rangeto}}]</p>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'textarea'">
|
||||
<ion-textarea [required]="item.required" name="{{item.typ}}_{{item.id}}" [attr.aria-multiline]="true" [(ngModel)]="item.value"></ion-textarea>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'multichoice-r'">
|
||||
<ion-list radio-group [(ngModel)]="item.value" [required]="item.required" name="{{item.typ}}_{{item.id}}">
|
||||
<ion-item *ngFor="let option of item.choices">
|
||||
<ion-label>{{option.label}}</ion-label>
|
||||
<ion-radio [value]="option.value"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ng-container>
|
||||
<ion-list *ngSwitchCase="'multichoice-c'">
|
||||
<ion-item *ngFor="let option of item.choices">
|
||||
<ion-label>{{option.label}}</ion-label>
|
||||
<ion-checkbox [required]="item.required" name="{{item.typ}}_{{item.id}}" [(ngModel)]="option.checked" value="option.value"></ion-checkbox>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
<ng-container *ngSwitchCase="'multichoice-d'">
|
||||
<ion-select [required]="item.required" name="{{item.typ}}_{{item.id}}" [(ngModel)]="item.value">
|
||||
<ion-option *ngFor="let option of item.choices" [value]="option.value">{{option.label}}</ion-option>
|
||||
</ion-select>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'captcha'">
|
||||
<core-recaptcha *ngIf="!preview && !offline" [publicKey]="item.captcha.recaptchapublickey" [model]="item" modelValueName="value"></core-recaptcha>
|
||||
<div *ngIf="!preview && (!item.captcha || offline)" class="core-warning-card" icon-start>
|
||||
<ion-icon name="warning"></ion-icon>
|
||||
{{ 'addon.mod_feedback.captchaofflinewarning' | translate }}
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<ion-grid>
|
||||
<ion-row align-items-center>
|
||||
<ion-col *ngIf="hasPrevPage">
|
||||
<button ion-button block outline icon-start (click)="gotoPage(true)">
|
||||
<ion-icon name="arrow-back"></ion-icon>
|
||||
{{ 'addon.mod_feedback.previous_page' | translate }}
|
||||
</button>
|
||||
</ion-col>
|
||||
<ion-col *ngIf="hasNextPage">
|
||||
<button ion-button block icon-end (click)="gotoPage(false)">
|
||||
{{ 'addon.mod_feedback.next_page' | translate }}
|
||||
<ion-icon name="arrow-forward"></ion-icon>
|
||||
</button>
|
||||
</ion-col>
|
||||
<ion-col *ngIf="!hasNextPage">
|
||||
<button ion-button block (click)="gotoPage(false)">
|
||||
{{ 'addon.mod_feedback.save_entries' | translate }}
|
||||
</button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-list>
|
||||
</ng-container>
|
||||
|
||||
<div class="core-success-card" icon-start *ngIf="completed">
|
||||
<ion-icon name="checkmark"></ion-icon>
|
||||
<p *ngIf="!completionPageContents && !completedOffline">{{ 'addon.mod_feedback.this_feedback_is_already_submitted' | translate }}</p>
|
||||
<p *ngIf="!completionPageContents && completedOffline">{{ 'addon.mod_feedback.feedback_submitted_offline' | translate }}</p>
|
||||
<p *ngIf="completionPageContents"><core-format-text [component]="component" componentId="componentId" [text]="completionPageContents"></core-format-text></p>
|
||||
</div>
|
||||
|
||||
<ion-grid *ngIf="completed">
|
||||
<ion-row align-items-center>
|
||||
<ion-col *ngIf="access.canviewanalysis">
|
||||
<button ion-button block outline icon-start (click)="showAnalysis()">
|
||||
<ion-icon name="stats"></ion-icon>
|
||||
{{ 'addon.mod_feedback.completed_feedbacks' | translate }}
|
||||
</button>
|
||||
</ion-col>
|
||||
<ion-col *ngIf="hasNextPage">
|
||||
<button ion-button block icon-end (click)="continue()">
|
||||
{{ 'core.continue' | translate }}
|
||||
<ion-icon name="arrow-forward"></ion-icon>
|
||||
</button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,37 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { IonicPageModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CorePipesModule } from '@pipes/pipes.module';
|
||||
import { AddonModFeedbackComponentsModule } from '../../components/components.module';
|
||||
import { AddonModFeedbackFormPage } from './form';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModFeedbackFormPage,
|
||||
],
|
||||
imports: [
|
||||
CoreDirectivesModule,
|
||||
CoreComponentsModule,
|
||||
CorePipesModule,
|
||||
AddonModFeedbackComponentsModule,
|
||||
IonicPageModule.forChild(AddonModFeedbackFormPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class AddonModFeedbackFormPageModule {}
|
|
@ -0,0 +1,15 @@
|
|||
page-addon-mod-feedback-form {
|
||||
.addon-mod_feedback-form-content {
|
||||
align-self: self-start;
|
||||
width: 100%;
|
||||
}
|
||||
.item-md .addon-mod_feedback-form-content {
|
||||
@include margin($item-md-padding-media-top, ($item-md-padding-end / 2), $item-md-padding-media-bottom, 0);
|
||||
}
|
||||
.item-ios .addon-mod_feedback-form-content {
|
||||
@include margin($item-ios-padding-media-top, $item-ios-padding-start, $item-ios-padding-media-bottom, 0);
|
||||
}
|
||||
.item-wp .addon-mod_feedback-form-content {
|
||||
@include margin($item-wp-padding-media-top, ($item-wp-padding-end / 2), $item-wp-padding-media-bottom, 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,341 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnDestroy, Optional } from '@angular/core';
|
||||
import { IonicPage, NavParams, NavController, Content } from 'ionic-angular';
|
||||
import { Network } from '@ionic-native/network';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { AddonModFeedbackProvider } from '../../providers/feedback';
|
||||
import { AddonModFeedbackHelperProvider } from '../../providers/helper';
|
||||
import { AddonModFeedbackSyncProvider } from '../../providers/sync';
|
||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
import { CoreLoginHelperProvider } from '@core/login/providers/helper';
|
||||
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
|
||||
/**
|
||||
* Page that displays feedback form.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-mod-feedback-form' })
|
||||
@Component({
|
||||
selector: 'page-addon-mod-feedback-form',
|
||||
templateUrl: 'form.html',
|
||||
})
|
||||
export class AddonModFeedbackFormPage implements OnDestroy {
|
||||
|
||||
protected module: any;
|
||||
protected currentPage: number;
|
||||
protected submitted: any;
|
||||
protected feedback;
|
||||
protected siteAfterSubmit;
|
||||
protected onlineObserver;
|
||||
protected originalData;
|
||||
protected currentSite;
|
||||
protected forceLeave = false;
|
||||
|
||||
title: string;
|
||||
preview = false;
|
||||
courseId: number;
|
||||
componentId: number;
|
||||
completionPageContents: string;
|
||||
component = AddonModFeedbackProvider.COMPONENT;
|
||||
offline = false;
|
||||
feedbackLoaded = false;
|
||||
access: any;
|
||||
items = [];
|
||||
hasPrevPage = false;
|
||||
hasNextPage = false;
|
||||
completed = false;
|
||||
completedOffline = false;
|
||||
|
||||
constructor(navParams: NavParams, protected feedbackProvider: AddonModFeedbackProvider, protected appProvider: CoreAppProvider,
|
||||
protected utils: CoreUtilsProvider, protected domUtils: CoreDomUtilsProvider, protected navCtrl: NavController,
|
||||
protected feedbackHelper: AddonModFeedbackHelperProvider, protected courseProvider: CoreCourseProvider,
|
||||
protected eventsProvider: CoreEventsProvider, protected feedbackSync: AddonModFeedbackSyncProvider, network: Network,
|
||||
protected translate: TranslateService, protected loginHelper: CoreLoginHelperProvider,
|
||||
protected linkHelper: CoreContentLinksHelperProvider, sitesProvider: CoreSitesProvider,
|
||||
@Optional() private content: Content) {
|
||||
|
||||
this.module = navParams.get('module');
|
||||
this.courseId = navParams.get('courseId');
|
||||
this.currentPage = navParams.get('page');
|
||||
this.title = navParams.get('title');
|
||||
this.preview = !!navParams.get('preview');
|
||||
this.componentId = navParams.get('moduleId') || this.module.id;
|
||||
|
||||
this.currentSite = sitesProvider.getCurrentSite();
|
||||
|
||||
// Refresh online status when changes.
|
||||
this.onlineObserver = network.onchange().subscribe((online) => {
|
||||
this.offline = !online;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* View loaded.
|
||||
*/
|
||||
ionViewDidLoad(): void {
|
||||
this.fetchData().then(() => {
|
||||
this.feedbackProvider.logView(this.feedback.id, true).then(() => {
|
||||
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* View entered.
|
||||
*/
|
||||
ionViewDidEnter(): void {
|
||||
this.forceLeave = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can leave the page or not.
|
||||
*
|
||||
* @return {boolean | Promise<void>} Resolved if we can leave it, rejected if not.
|
||||
*/
|
||||
ionViewCanLeave(): boolean | Promise<void> {
|
||||
if (this.forceLeave) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.preview) {
|
||||
const responses = this.feedbackHelper.getPageItemsResponses(this.items);
|
||||
|
||||
if (this.items && !this.completed && this.originalData) {
|
||||
// Form submitted. Check if there is any change.
|
||||
if (!this.utils.basicLeftCompare(responses, this.originalData, 3)) {
|
||||
return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the data required for the view.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchData(): Promise<any> {
|
||||
this.offline = !this.appProvider.isOnline();
|
||||
|
||||
return this.feedbackProvider.getFeedback(this.courseId, this.module.id).then((feedbackData) => {
|
||||
this.feedback = feedbackData;
|
||||
|
||||
this.title = this.feedback.name || this.title;
|
||||
|
||||
return this.fetchAccessData();
|
||||
}).then((accessData) => {
|
||||
if (!this.preview && accessData.cansubmit && !accessData.isempty) {
|
||||
return typeof this.currentPage == 'undefined' ?
|
||||
this.feedbackProvider.getResumePage(this.feedback.id, this.offline, true) :
|
||||
Promise.resolve(this.currentPage);
|
||||
} else {
|
||||
this.preview = true;
|
||||
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
}).catch((error) => {
|
||||
if (!this.offline && !this.utils.isWebServiceError(error)) {
|
||||
// If it fails, go offline.
|
||||
this.offline = true;
|
||||
|
||||
return this.feedbackProvider.getResumePage(this.feedback.id, true);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}).then((page) => {
|
||||
return this.fetchFeedbackPageData(page || 0);
|
||||
}).catch((message) => {
|
||||
this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
|
||||
this.forceLeave = true;
|
||||
this.navCtrl.pop();
|
||||
|
||||
return Promise.reject(null);
|
||||
}).finally(() => {
|
||||
this.feedbackLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch access information.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchAccessData(): Promise<any> {
|
||||
return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, this.offline, true).catch((error) => {
|
||||
if (!this.offline && !this.utils.isWebServiceError(error)) {
|
||||
// If it fails, go offline.
|
||||
this.offline = true;
|
||||
|
||||
return this.feedbackProvider.getFeedbackAccessInformation(this.feedback.id, true);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}).then((accessData) => {
|
||||
this.access = accessData;
|
||||
|
||||
return accessData;
|
||||
});
|
||||
}
|
||||
|
||||
protected fetchFeedbackPageData(page: number = 0): Promise<void> {
|
||||
let promise;
|
||||
this.items = [];
|
||||
|
||||
if (this.preview) {
|
||||
promise = this.feedbackProvider.getItems(this.feedback.id);
|
||||
} else {
|
||||
this.currentPage = page;
|
||||
|
||||
promise = this.feedbackProvider.getPageItemsWithValues(this.feedback.id, page, this.offline, true).catch((error) => {
|
||||
if (!this.offline && !this.utils.isWebServiceError(error)) {
|
||||
// If it fails, go offline.
|
||||
this.offline = true;
|
||||
|
||||
return this.feedbackProvider.getPageItemsWithValues(this.feedback.id, page, true);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}).then((response) => {
|
||||
this.hasPrevPage = !!response.hasprevpage;
|
||||
this.hasNextPage = !!response.hasnextpage;
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
return promise.then((response) => {
|
||||
this.items = response.items.map((itemData) => {
|
||||
return this.feedbackHelper.getItemForm(itemData, this.preview);
|
||||
}).filter((itemData) => {
|
||||
// Filter items with errors.
|
||||
return itemData;
|
||||
});
|
||||
|
||||
if (!this.preview) {
|
||||
const itemsCopy = this.utils.clone(this.items); // Copy the array to avoid modifications.
|
||||
this.originalData = this.feedbackHelper.getPageItemsResponses(itemsCopy);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to allow page navigation through the questions form.
|
||||
*
|
||||
* @param {boolean} goPrevious If true it will go back to the previous page, if false, it will go forward.
|
||||
* @return {Promise<void>} Resolved when done.
|
||||
*/
|
||||
gotoPage(goPrevious: boolean): Promise<void> {
|
||||
this.content && this.content.scrollToTop();
|
||||
this.feedbackLoaded = false;
|
||||
|
||||
const responses = this.feedbackHelper.getPageItemsResponses(this.items),
|
||||
formHasErrors = this.items.some((item) => {
|
||||
return item.isEmpty || item.hasError;
|
||||
});
|
||||
|
||||
// Sync other pages first.
|
||||
return this.feedbackSync.syncFeedback(this.feedback.id).catch(() => {
|
||||
// Ignore errors.
|
||||
}).then(() => {
|
||||
return this.feedbackProvider.processPage(this.feedback.id, this.currentPage, responses, goPrevious, formHasErrors,
|
||||
this.courseId).then((response) => {
|
||||
const jumpTo = parseInt(response.jumpto, 10);
|
||||
|
||||
if (response.completed) {
|
||||
// Form is completed, show completion message and buttons.
|
||||
this.items = [];
|
||||
this.completed = true;
|
||||
this.completedOffline = !!response.offline;
|
||||
this.completionPageContents = response.completionpagecontents;
|
||||
this.siteAfterSubmit = response.siteaftersubmit;
|
||||
this.submitted = true;
|
||||
|
||||
// Invalidate access information so user will see home page updated (continue form or completion messages).
|
||||
const promises = [];
|
||||
promises.push(this.feedbackProvider.invalidateFeedbackAccessInformationData(this.feedback.id));
|
||||
promises.push(this.feedbackProvider.invalidateResumePageData(this.feedback.id));
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return this.fetchAccessData();
|
||||
});
|
||||
} else if (isNaN(jumpTo) || jumpTo == this.currentPage) {
|
||||
// Errors on questions, stay in page.
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
this.submitted = true;
|
||||
// Invalidate access information so user will see home page updated (continue form).
|
||||
this.feedbackProvider.invalidateResumePageData(this.feedback.id);
|
||||
|
||||
// Fetch the new page.
|
||||
return this.fetchFeedbackPageData(jumpTo);
|
||||
}
|
||||
});
|
||||
}).catch((message) => {
|
||||
this.domUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
|
||||
|
||||
return Promise.reject(null);
|
||||
}).finally(() => {
|
||||
this.feedbackLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to link implemented features.
|
||||
*/
|
||||
showAnalysis(): void {
|
||||
this.submitted = 'analysis';
|
||||
this.feedbackHelper.openFeature('analysis', this.navCtrl, this.module, this.courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to go to the page after submit.
|
||||
*/
|
||||
continue(): void {
|
||||
if (this.siteAfterSubmit) {
|
||||
const modal = this.domUtils.showModalLoading();
|
||||
this.linkHelper.handleLink(this.siteAfterSubmit).then((treated) => {
|
||||
if (!treated) {
|
||||
return this.currentSite.openInBrowserWithAutoLoginIfSameSite(this.siteAfterSubmit);
|
||||
}
|
||||
}).finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
} else {
|
||||
// Use redirect to make the course the new history root (to avoid "loops" in history).
|
||||
this.loginHelper.redirect('CoreCourseSectionPage', {
|
||||
course: { id: this.courseId }
|
||||
}, this.currentSite.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (this.submitted) {
|
||||
const tab = this.submitted == 'analysis' ? 'analysis' : 'overview';
|
||||
// If form has been submitted, the info has been already invalidated but we should update index view.
|
||||
this.eventsProvider.trigger(AddonModFeedbackProvider.FORM_SUBMITTED, {feedbackId: this.feedback.id, tab: tab});
|
||||
}
|
||||
this.onlineObserver && this.onlineObserver.unsubscribe();
|
||||
}
|
||||
}
|
|
@ -17,6 +17,8 @@ import { CoreLoggerProvider } from '@providers/logger';
|
|||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { AddonModFeedbackOfflineProvider } from './offline';
|
||||
|
||||
/**
|
||||
* Service that provides some features for feedbacks.
|
||||
|
@ -35,10 +37,255 @@ export class AddonModFeedbackProvider {
|
|||
protected logger;
|
||||
|
||||
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider,
|
||||
private filepoolProvider: CoreFilepoolProvider) {
|
||||
private filepoolProvider: CoreFilepoolProvider, private feedbackOffline: AddonModFeedbackOfflineProvider,
|
||||
private appProvider: CoreAppProvider) {
|
||||
this.logger = logger.getInstance('AddonModFeedbackProvider');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check dependency of a question item.
|
||||
*
|
||||
* @param {any[]} items All question items to check dependency.
|
||||
* @param {any} item Item to check.
|
||||
* @return {boolean} Return true if dependency is acomplished and it can be shown. False, otherwise.
|
||||
*/
|
||||
protected checkDependencyItem(items: any[], item: any): boolean {
|
||||
const depend = items.find((itemFind) => {
|
||||
return itemFind.id == item.dependitem;
|
||||
});
|
||||
|
||||
// Item not found, looks like dependent item has been removed or is in the same or following pages.
|
||||
if (!depend) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (depend.typ) {
|
||||
case 'label':
|
||||
return false;
|
||||
case 'multichoice':
|
||||
case 'multichoicerated':
|
||||
return this.compareDependItemMultichoice(depend, item.dependvalue);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return item.dependvalue == depend.rawValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check dependency item of type Multichoice.
|
||||
*
|
||||
* @param {any} item Item to check.
|
||||
* @param {string} dependValue Value to compare.
|
||||
* @return {boolean} eturn true if dependency is acomplished and it can be shown. False, otherwise.
|
||||
*/
|
||||
protected compareDependItemMultichoice(item: any, dependValue: string): boolean {
|
||||
let values, choices;
|
||||
const parts = item.presentation.split(AddonModFeedbackProvider.MULTICHOICE_TYPE_SEP) || [],
|
||||
subtype = parts.length > 0 && parts[0] ? parts[0] : 'r';
|
||||
|
||||
choices = parts[1] || '';
|
||||
choices = choices.split(AddonModFeedbackProvider.MULTICHOICE_ADJUST_SEP)[0] || '';
|
||||
choices = choices.split(AddonModFeedbackProvider.LINE_SEP) || [];
|
||||
|
||||
if (subtype === 'c') {
|
||||
if (typeof item.rawValue == 'undefined') {
|
||||
values = [''];
|
||||
} else {
|
||||
item.rawValue = '' + item.rawValue;
|
||||
values = item.rawValue.split(AddonModFeedbackProvider.LINE_SEP);
|
||||
}
|
||||
} else {
|
||||
values = [item.rawValue];
|
||||
}
|
||||
|
||||
for (let index = 0; index < choices.length; index++) {
|
||||
for (const x in values) {
|
||||
if (values[x] == index + 1) {
|
||||
let value = choices[index];
|
||||
|
||||
if (item.typ == 'multichoicerated') {
|
||||
value = value.split(AddonModFeedbackProvider.MULTICHOICERATED_VALUE_SEP)[1] || '';
|
||||
}
|
||||
|
||||
if (value.trim() == dependValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// We can finish checking if only searching on one value and we found it.
|
||||
if (values.length == 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill values of item questions.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @param {any[]} items Item to fill the value.
|
||||
* @param {boolean} offline True if it should return cached data. Has priority over ignoreCache.
|
||||
* @param {boolean} ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||
* @param {string} siteId Site ID.
|
||||
* @return {Promise<any>} Resolved with values when done.
|
||||
*/
|
||||
protected fillValues(feedbackId: number, items: any[], offline: boolean, ignoreCache: boolean, siteId: string): Promise<any> {
|
||||
return this.getCurrentValues(feedbackId, offline, ignoreCache, siteId).then((valuesArray) => {
|
||||
if (valuesArray.length == 0) {
|
||||
// Try sending empty values to get the last completed attempt values.
|
||||
return this.processPageOnline(feedbackId, 0, {}, undefined, siteId).then(() => {
|
||||
return this.getCurrentValues(feedbackId, offline, ignoreCache, siteId);
|
||||
}).catch(() => {
|
||||
// Ignore errors
|
||||
});
|
||||
}
|
||||
|
||||
return valuesArray;
|
||||
|
||||
}).then((valuesArray) => {
|
||||
const values = {};
|
||||
|
||||
valuesArray.forEach((value) => {
|
||||
values[value.item] = value.value;
|
||||
});
|
||||
|
||||
items.forEach((itemData) => {
|
||||
if (itemData.hasvalue && typeof values[itemData.id] != 'undefined') {
|
||||
itemData.rawValue = values[itemData.id];
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
// Ignore errors.
|
||||
}).then(() => {
|
||||
// Merge with offline data.
|
||||
return this.feedbackOffline.getFeedbackResponses(feedbackId, siteId).then((offlineValuesArray) => {
|
||||
const offlineValues = {};
|
||||
|
||||
// Merge all values into one array.
|
||||
offlineValuesArray = offlineValuesArray.reduce((a, b) => {
|
||||
const responses = this.utils.objectToArrayOfObjects(b.responses, 'id', 'value');
|
||||
|
||||
return a.concat(responses);
|
||||
}, []).map((a) => {
|
||||
const parts = a.id.split('_');
|
||||
a.typ = parts[0];
|
||||
a.item = parseInt(parts[1], 0);
|
||||
|
||||
return a;
|
||||
});
|
||||
|
||||
offlineValuesArray.forEach((value) => {
|
||||
if (typeof offlineValues[value.item] == 'undefined') {
|
||||
offlineValues[value.item] = [];
|
||||
}
|
||||
offlineValues[value.item].push(value.value);
|
||||
});
|
||||
|
||||
items.forEach((itemData) => {
|
||||
if (itemData.hasvalue && typeof offlineValues[itemData.id] != 'undefined') {
|
||||
// Treat multichoice checkboxes.
|
||||
if (itemData.typ == 'multichoice' &&
|
||||
itemData.presentation.split(AddonModFeedbackProvider.MULTICHOICE_TYPE_SEP)[0] == 'c') {
|
||||
|
||||
offlineValues[itemData.id] = offlineValues[itemData.id].filter((value) => {
|
||||
return value > 0;
|
||||
});
|
||||
itemData.rawValue = offlineValues[itemData.id].join(AddonModFeedbackProvider.LINE_SEP);
|
||||
} else {
|
||||
itemData.rawValue = offlineValues[itemData.id][0];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
});
|
||||
}).catch(() => {
|
||||
// Ignore errors.
|
||||
return items;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the feedback non respondents users.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @param {number} groupId Group id, 0 means that the function will determine the user group.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @param {any} [previous] Only for recurrent use. Object with the previous fetched info.
|
||||
* @return {Promise<any>} Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getAllNonRespondents(feedbackId: number, groupId: number, siteId?: string, previous?: any): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
if (typeof previous == 'undefined') {
|
||||
previous = {
|
||||
page: 0,
|
||||
users: []
|
||||
};
|
||||
}
|
||||
|
||||
return this.getNonRespondents(feedbackId, groupId, previous.page, siteId).then((response) => {
|
||||
if (previous.users.length < response.total) {
|
||||
previous.users = previous.users.concat(response.users);
|
||||
}
|
||||
|
||||
if (previous.users.length < response.total) {
|
||||
// Can load more.
|
||||
previous.page++;
|
||||
|
||||
return this.getAllNonRespondents(feedbackId, groupId, siteId, previous);
|
||||
}
|
||||
previous.total = response.total;
|
||||
|
||||
return previous;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the feedback user responses.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @param {number} groupId Group id, 0 means that the function will determine the user group.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @param {any} [previous] Only for recurrent use. Object with the previous fetched info.
|
||||
* @return {Promise<any>} Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getAllResponsesAnalysis(feedbackId: number, groupId: number, siteId?: string, previous?: any): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
if (typeof previous == 'undefined') {
|
||||
previous = {
|
||||
page: 0,
|
||||
attempts: [],
|
||||
anonattempts: []
|
||||
};
|
||||
}
|
||||
|
||||
return this.getResponsesAnalysis(feedbackId, groupId, previous.page, siteId).then((responses) => {
|
||||
if (previous.anonattempts.length < responses.totalanonattempts) {
|
||||
previous.anonattempts = previous.anonattempts.concat(responses.anonattempts);
|
||||
}
|
||||
|
||||
if (previous.attempts.length < responses.totalattempts) {
|
||||
previous.attempts = previous.attempts.concat(responses.attempts);
|
||||
}
|
||||
|
||||
if (previous.anonattempts.length < responses.totalanonattempts || previous.attempts.length < responses.totalattempts) {
|
||||
// Can load more.
|
||||
previous.page++;
|
||||
|
||||
return this.getAllResponsesAnalysis(feedbackId, groupId, siteId, previous);
|
||||
}
|
||||
|
||||
previous.totalattempts = responses.totalattempts;
|
||||
previous.totalanonattempts = responses.totalanonattempts;
|
||||
|
||||
return previous;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get analysis information for a given feedback.
|
||||
*
|
||||
|
@ -436,6 +683,118 @@ export class AddonModFeedbackProvider {
|
|||
return this.getFeedbackDataPrefixCacheKey(feedbackId) + ':nonrespondents:';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single feedback page items. This function is not cached, use AddonModFeedbackHelperProvider#getPageItems instead.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @param {number} page The page to get.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the info is retrieved.
|
||||
*/
|
||||
getPageItems(feedbackId: number, page: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const params = {
|
||||
feedbackid: feedbackId,
|
||||
page: page
|
||||
};
|
||||
|
||||
return site.write('mod_feedback_get_page_items', params);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single feedback page items. If offline or server down it will use getItems to calculate dependencies.
|
||||
*
|
||||
* @param {number} feedbackId Feedback ID.
|
||||
* @param {number} page The page to get.
|
||||
* @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 will 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.
|
||||
*/
|
||||
getPageItemsWithValues(feedbackId: number, page: number, offline: boolean = false, ignoreCache: boolean = false,
|
||||
siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
return this.getPageItems(feedbackId, page, siteId).then((response) => {
|
||||
return this.fillValues(feedbackId, response.items, offline, ignoreCache, siteId).then((items) => {
|
||||
response.items = items;
|
||||
|
||||
return response;
|
||||
});
|
||||
}).catch(() => {
|
||||
// If getPageItems fail we should calculate it using getItems.
|
||||
return this.getItems(feedbackId, siteId).then((response) => {
|
||||
return this.fillValues(feedbackId, response.items, offline, ignoreCache, siteId).then((items) => {
|
||||
// Separate items by pages.
|
||||
let currentPage = 0;
|
||||
const previousPageItems = [];
|
||||
|
||||
const pageItems = items.filter((item) => {
|
||||
// Greater page, discard all entries.
|
||||
if (currentPage > page) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.typ == 'pagebreak') {
|
||||
currentPage++;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save items on previous page to check dependencies and discard entry.
|
||||
if (currentPage < page) {
|
||||
previousPageItems.push(item);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter depending items.
|
||||
if (item && item.dependitem > 0 && previousPageItems.length > 0) {
|
||||
return this.checkDependencyItem(previousPageItems, item);
|
||||
}
|
||||
|
||||
// Filter items with errors.
|
||||
return item;
|
||||
});
|
||||
|
||||
// Check if there are more pages.
|
||||
response.hasprevpage = page > 0;
|
||||
response.hasnextpage = currentPage > page;
|
||||
response.items = pageItems;
|
||||
|
||||
return response;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get the page we can jump.
|
||||
*
|
||||
* @param {number} feedbackId [description]
|
||||
* @param {number} page [description]
|
||||
* @param {number} changePage [description]
|
||||
* @param {string} siteId [description]
|
||||
* @return {Promise<number | false>} [description]
|
||||
*/
|
||||
protected getPageJumpTo(feedbackId: number, page: number, changePage: number, siteId: string): Promise<number | false> {
|
||||
return this.getPageItemsWithValues(feedbackId, page, true, false, siteId).then((resp) => {
|
||||
// The page we are going has items.
|
||||
if (resp.items.length > 0) {
|
||||
return page;
|
||||
}
|
||||
|
||||
// Check we can jump futher.
|
||||
if ((changePage == 1 && resp.hasnextpage) || (changePage == -1 && resp.hasprevpage)) {
|
||||
return this.getPageJumpTo(feedbackId, page + changePage, changePage, siteId);
|
||||
}
|
||||
|
||||
// Completed or first page.
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the feedback user responses.
|
||||
*
|
||||
|
@ -723,6 +1082,93 @@ export class AddonModFeedbackProvider {
|
|||
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 {boolean} formHasErrors Whether the form we sent has required but empty fields (only used in offline).
|
||||
* @param {number} courseId Course ID the feedback belongs to.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the info is retrieved.
|
||||
*/
|
||||
processPage(feedbackId: number, page: number, responses: any, goPrevious: boolean, formHasErrors: boolean, courseId: number,
|
||||
siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
// Convenience function to store a message to be synchronized later.
|
||||
const storeOffline = (): Promise<any> => {
|
||||
return this.feedbackOffline.saveResponses(feedbackId, page, responses, courseId, siteId).then(() => {
|
||||
// Simulate process_page response.
|
||||
const response = {
|
||||
jumpto: page,
|
||||
completed: false,
|
||||
offline: true
|
||||
};
|
||||
let changePage = 0;
|
||||
|
||||
if (goPrevious) {
|
||||
if (page > 0) {
|
||||
changePage = -1;
|
||||
}
|
||||
} else if (!formHasErrors) {
|
||||
// We can only go next if it has no errors.
|
||||
changePage = 1;
|
||||
}
|
||||
|
||||
if (changePage === 0) {
|
||||
return response;
|
||||
}
|
||||
|
||||
return this.getPageItemsWithValues(feedbackId, page, true, false, siteId).then((resp) => {
|
||||
// Check completion.
|
||||
if (changePage == 1 && !resp.hasnextpage) {
|
||||
response.completed = true;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
return this.getPageJumpTo(feedbackId, page + changePage, changePage, siteId).then((loadPage) => {
|
||||
if (loadPage === false) {
|
||||
// Completed or first page.
|
||||
if (changePage == -1) {
|
||||
// First page.
|
||||
response.jumpto = 0;
|
||||
} else {
|
||||
// Completed.
|
||||
response.completed = true;
|
||||
}
|
||||
} else {
|
||||
response.jumpto = loadPage;
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (!this.appProvider.isOnline()) {
|
||||
// App is offline, store the action.
|
||||
return storeOffline();
|
||||
}
|
||||
|
||||
// If there's already a response to be sent to the server, discard it first.
|
||||
return this.feedbackOffline.deleteFeedbackPageResponses(feedbackId, page, siteId).then(() => {
|
||||
return this.processPageOnline(feedbackId, page, responses, goPrevious, siteId).catch((error) => {
|
||||
if (this.utils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means that responses cannot be submitted.
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Couldn't connect to server, store in offline.
|
||||
return storeOffline();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a jump between pages.
|
||||
*
|
||||
|
|
|
@ -94,6 +94,88 @@ export class AddonModFeedbackHelperProvider {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page items responses to be sent.
|
||||
*
|
||||
* @param {any[]} items Items where the values are.
|
||||
* @return {any} Responses object to be sent.
|
||||
*/
|
||||
getPageItemsResponses(items: any[]): any {
|
||||
const responses = {};
|
||||
|
||||
items.forEach((itemData) => {
|
||||
let answered = false;
|
||||
|
||||
itemData.hasError = false;
|
||||
|
||||
if (itemData.typ == 'captcha') {
|
||||
const value = itemData.value || '',
|
||||
name = itemData.typ + '_' + itemData.id;
|
||||
|
||||
answered = !!value;
|
||||
responses[name] = 1;
|
||||
responses['g-recaptcha-response'] = value;
|
||||
responses['recaptcha_element'] = 'dummyvalue';
|
||||
|
||||
if (itemData.required && !answered) {
|
||||
// Check if it has any value.
|
||||
itemData.isEmpty = true;
|
||||
} else {
|
||||
itemData.isEmpty = false;
|
||||
}
|
||||
} else if (itemData.hasvalue) {
|
||||
let name, value;
|
||||
const nameTemp = itemData.typ + '_' + itemData.id;
|
||||
|
||||
if (itemData.typ == 'multichoice' && itemData.subtype == 'c') {
|
||||
name = nameTemp + '[0]';
|
||||
responses[name] = 0;
|
||||
itemData.choices.forEach((choice, index) => {
|
||||
name = nameTemp + '[' + (index + 1) + ']';
|
||||
value = choice.checked ? choice.value : 0;
|
||||
if (!answered && value) {
|
||||
answered = true;
|
||||
}
|
||||
responses[name] = value;
|
||||
});
|
||||
} else {
|
||||
if (itemData.typ == 'multichoice') {
|
||||
name = nameTemp + '[0]';
|
||||
} else {
|
||||
name = nameTemp;
|
||||
}
|
||||
|
||||
if (itemData.typ == 'multichoice' || itemData.typ == 'multichoicerated') {
|
||||
value = itemData.value || 0;
|
||||
} else if (itemData.typ == 'numeric') {
|
||||
value = itemData.value || itemData.value == 0 ? itemData.value : '';
|
||||
|
||||
if (value != '') {
|
||||
if ((itemData.rangefrom != '' && value < itemData.rangefrom) ||
|
||||
(itemData.rangeto != '' && value > itemData.rangeto)) {
|
||||
itemData.hasError = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
value = itemData.value || itemData.value == 0 ? itemData.value : '';
|
||||
}
|
||||
|
||||
answered = !!value;
|
||||
responses[name] = value;
|
||||
}
|
||||
|
||||
if (itemData.required && !answered) {
|
||||
// Check if it has any value.
|
||||
itemData.isEmpty = true;
|
||||
} else {
|
||||
itemData.isEmpty = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return responses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the feedback user responses with extra info.
|
||||
*
|
||||
|
@ -269,8 +351,6 @@ export class AddonModFeedbackHelperProvider {
|
|||
parts = item.presentation.split(AddonModFeedbackProvider.MULTICHOICE_ADJUST_SEP) || [];
|
||||
item.presentation = parts.length > 0 ? parts[0] : '';
|
||||
// Horizontal are not supported right now. item.horizontal = parts.length > 1 && !!parts[1];
|
||||
} else {
|
||||
item.class = 'item-select';
|
||||
}
|
||||
|
||||
item.choices = item.presentation.split(AddonModFeedbackProvider.LINE_SEP) || [];
|
||||
|
@ -320,9 +400,6 @@ export class AddonModFeedbackHelperProvider {
|
|||
const data = this.textUtils.parseJSON(item.otherdata);
|
||||
if (data && data.length > 3) {
|
||||
item.captcha = {
|
||||
challengehash: data[0],
|
||||
imageurl: data[1],
|
||||
jsurl: data[2],
|
||||
recaptchapublickey: data[3]
|
||||
};
|
||||
}
|
||||
|
|
|
@ -157,7 +157,7 @@ export class AddonModFeedbackOfflineProvider {
|
|||
timemodified: this.timeUtils.timestamp()
|
||||
};
|
||||
|
||||
return site.getDb().insertOrUpdateRecord(this.FEEDBACK_TABLE, entry, {feedbackid: feedbackId, page: page});
|
||||
return site.getDb().insertRecord(this.FEEDBACK_TABLE, entry);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseModulePrefetchHan
|
|||
* 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> {
|
||||
downloadOrPrefetch(module: any, courseId: number, prefetch?: boolean, dirPath?: string): Promise<any> {
|
||||
const promises = [],
|
||||
siteId = this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
|
@ -119,7 +119,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseModulePrefetchHan
|
|||
}));
|
||||
|
||||
return Promise.all(promises);
|
||||
}*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of downloadable files.
|
||||
|
@ -129,7 +129,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseModulePrefetchHan
|
|||
* @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[]> {
|
||||
getFiles(module: any, courseId: number, single?: boolean): Promise<any[]> {
|
||||
let files = [];
|
||||
|
||||
return this.feedbackProvider.getFeedback(courseId, module.id).then((feedback) => {
|
||||
|
@ -149,7 +149,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseModulePrefetchHan
|
|||
// Any error, return the list we have.
|
||||
return files;
|
||||
});
|
||||
}*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns feedback intro files.
|
||||
|
|
|
@ -567,8 +567,17 @@ textarea {
|
|||
height: auto;
|
||||
}
|
||||
|
||||
// Message cards
|
||||
canvas[core-chart] {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.core-circle:before {
|
||||
content: ' \25CF';
|
||||
}
|
||||
|
||||
@each $color-name, $color-base, $color-contrast in get-colors($colors) {
|
||||
// Message cards.
|
||||
.core-#{$color-name}-card {
|
||||
@extend ion-card;
|
||||
border-bottom: 3px solid $color-base;
|
||||
|
@ -589,21 +598,18 @@ textarea {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
canvas[core-chart] {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.core-#{$color-name}-item {
|
||||
border-bottom: 3px solid $color-base !important;
|
||||
ion-icon {
|
||||
color: $color-base;
|
||||
}
|
||||
}
|
||||
|
||||
.core-circle:before {
|
||||
content: ' \25CF';
|
||||
}
|
||||
|
||||
@each $color-name, $color-base, $color-contrast in get-colors($colors) {
|
||||
.core-#{$color-name}-circle {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.core-#{$color-name}-circle:before {
|
||||
@extend .core-circle:before;
|
||||
color: $color-base;
|
||||
|
|
|
@ -59,7 +59,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
|
|||
|
||||
// Refresh online status when changes.
|
||||
this.onlineObserver = network.onchange().subscribe((online) => {
|
||||
this.isOnline = this.appProvider.isOnline();
|
||||
this.isOnline = online;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -170,9 +170,10 @@ export class CoreUtilsProvider {
|
|||
|
||||
/**
|
||||
* Blocks leaving a view.
|
||||
* @deprecated, use ionViewCanLeave instead.
|
||||
*/
|
||||
blockLeaveView(): void {
|
||||
// @todo
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue