MOBILE-3639 choice: Implement index page

main
Dani Palou 2021-03-30 13:05:54 +02:00
parent 18757924bb
commit 53d808ad76
8 changed files with 818 additions and 1 deletions

View File

@ -0,0 +1,38 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { RouterModule, Routes } from '@angular/router';
import { CoreSharedModule } from '@/core/shared.module';
import { AddonModChoiceComponentsModule } from './components/components.module';
import { AddonModChoiceIndexPage } from './pages/index/index';
const routes: Routes = [
{
path: ':courseId/:cmId',
component: AddonModChoiceIndexPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CoreSharedModule,
AddonModChoiceComponentsModule,
],
declarations: [
AddonModChoiceIndexPage,
],
})
export class AddonModChoiceLazyModule {}

View File

@ -13,19 +13,22 @@
// limitations under the License.
import { APP_INITIALIZER, NgModule, Type } from '@angular/core';
import { Routes } from '@angular/router';
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
import { CoreCronDelegate } from '@services/cron';
import { CORE_SITE_SCHEMAS } from '@services/sites';
import { AddonModChoiceComponentsModule } from './components/components.module';
import { AddonModChoiceProvider } from './services/choice';
import { AddonModChoiceOfflineProvider } from './services/choice-offline';
import { AddonModChoiceSyncProvider } from './services/choice-sync';
import { OFFLINE_SITE_SCHEMA } from './services/database/choice';
import { AddonModChoiceIndexLinkHandler } from './services/handlers/index-link';
import { AddonModChoiceListLinkHandler } from './services/handlers/list-link';
import { AddonModChoiceModuleHandler } from './services/handlers/module';
import { AddonModChoiceModuleHandler, AddonModChoiceModuleHandlerService } from './services/handlers/module';
import { AddonModChoicePrefetchHandler } from './services/handlers/prefetch';
import { AddonModChoiceSyncCronHandler } from './services/handlers/sync-cron';
@ -35,8 +38,17 @@ export const ADDON_MOD_CHOICE_SERVICES: Type<unknown>[] = [
AddonModChoiceSyncProvider,
];
const routes: Routes = [
{
path: AddonModChoiceModuleHandlerService.PAGE_NAME,
loadChildren: () => import('./choice-lazy.module').then(m => m.AddonModChoiceLazyModule),
},
];
@NgModule({
imports: [
CoreMainMenuTabRoutingModule.forChild(routes),
AddonModChoiceComponentsModule,
],
providers: [
{

View File

@ -0,0 +1,34 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { CoreSharedModule } from '@/core/shared.module';
import { NgModule } from '@angular/core';
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
import { AddonModChoiceIndexComponent } from './index/index';
@NgModule({
declarations: [
AddonModChoiceIndexComponent,
],
imports: [
CoreSharedModule,
CoreCourseComponentsModule,
],
providers: [
],
exports: [
AddonModChoiceIndexComponent,
],
})
export class AddonModChoiceComponentsModule {}

View File

@ -0,0 +1,175 @@
<!-- Buttons to add to the header. -->
<core-navbar-buttons slot="end">
<core-context-menu>
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate"
[href]="externalUrl" iconAction="fas-external-link-alt">
</core-context-menu-item>
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate"
(action)="expandDescription()" iconAction="fas-arrow-right">
</core-context-menu-item>
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}"
iconAction="far-newspaper" (action)="gotoBlog()">
</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" (action)="doRefresh(null, $event, true)"
[content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon" [closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)"
[iconAction]="prefetchStatusIcon" [closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}"
iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false">
</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"
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-course-module-description>
<!-- Activity availability messages -->
<ion-card class="core-info-card" *ngIf="choiceNotOpenYet">
<ion-item>
<ion-icon name="fas-info-circle" slot="start"></ion-icon>
<ion-label>
<p *ngIf="options.length">{{ 'addon.mod_choice.previewonly' | translate:{$a: openTimeReadable} }}</p>
<p *ngIf="!options.length">{{ 'addon.mod_choice.notopenyet' | translate:{$a: openTimeReadable} }}</p>
</ion-label>
</ion-item>
</ion-card>
<ion-card class="core-info-card" *ngIf="choiceClosed">
<ion-item>
<ion-icon name="fas-info-circle" slot="start"></ion-icon>
<ion-label>
<p *ngIf="options.length">
{{ 'addon.mod_choice.yourselection' | translate }}
<core-format-text [text]="options[0].text" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="courseId">
</core-format-text>
</p>
<p>{{ 'addon.mod_choice.expired' | translate:{$a: closeTimeReadable} }}</p>
</ion-label>
</ion-item>
</ion-card>
<!-- Choice done in offline but not synchronized -->
<ion-card class="core-warning-card" *ngIf="hasOffline">
<ion-item>
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
<ion-label>{{ 'core.hasdatatosync' | translate:{$a: moduleName} }}</ion-label>
</ion-item>
</ion-card>
<!-- Inform what will happen with the choices. -->
<ion-card class="core-info-card" *ngIf="canEdit && publishInfo && options.length">
<ion-item>
<ion-icon name="fas-info-circle" slot="start"></ion-icon>
<ion-label>{{ publishInfo | translate }}</ion-label>
</ion-item>
</ion-card>
<!-- Choice options -->
<ion-card *ngIf="options.length && choice">
<ng-container *ngIf="choice.allowmultiple">
<ion-item class="ion-text-wrap" *ngFor="let option of options">
<ion-label>
<ng-container *ngTemplateOutlet="optionLabelTemplate; context: {option: option}"></ng-container>
</ion-label>
<ion-checkbox slot="end" [(ngModel)]="option.checked" [disabled]="option.disabled || !canEdit"></ion-checkbox>
</ion-item>
</ng-container>
<ion-radio-group *ngIf="!choice.allowmultiple" [(ngModel)]="selectedOption.id">
<ion-item class="ion-text-wrap" *ngFor="let option of options">
<ion-label>
<ng-container *ngTemplateOutlet="optionLabelTemplate; context: {option: option}"></ng-container>
</ion-label>
<ion-radio slot="end" [value]="option.id" [disabled]="option.disabled || !canEdit"></ion-radio>
</ion-item>
</ion-radio-group>
<ion-button *ngIf="canEdit" expand="block" (click)="save()" [disabled]="!canSave()" class="ion-margin">
{{ 'addon.mod_choice.savemychoice' | translate }}
</ion-button>
<ion-button *ngIf="canDelete" expand="block" color="light" (click)="delete()" class="ion-margin">
{{ 'addon.mod_choice.removemychoice' | translate }}
</ion-button>
</ion-card>
<!-- Choice results -->
<div *ngIf="canSeeResults && choice">
<ion-item-divider>
<ion-label>
<h2>{{ 'addon.mod_choice.responses' | translate }}</h2>
</ion-label>
</ion-item-divider>
<ion-grid class="ion-no-padding">
<ion-row>
<ion-col size="12" size-lg="5">
<ion-item class="ion-text-wrap core-warning-item" *ngIf="hasOffline">
<ion-icon slot="start" name="fas-exclamation-triangle" color="warning"></ion-icon>
<ion-label>{{ 'addon.mod_choice.resultsnotsynced' | translate }}</ion-label>
</ion-item>
<ion-item>
<ion-label>
<core-chart type="pie" [data]="data" [labels]="labels" height="300" contextLevel="module"
[contextInstanceId]="module.id" [courseId]="courseId">
</core-chart>
</ion-label>
</ion-item>
</ion-col>
<ion-col *ngIf="choice.publish && results" size="12" size-lg="7">
<ion-item-group *ngFor="let result of results">
<ion-item-divider class="ion-text-wrap">
<ion-label>
<h2>
<core-format-text [text]="result.text" contextLevel="module" [contextInstanceId]="module.id"
[courseId]="courseId">
</core-format-text>
</h2>
<p>
{{ 'addon.mod_choice.numberofuser' | translate }}: {{ result.numberofuser }}
({{ 'core.percentagenumber' | translate: {$a: result.percentageamountfixed} }})
</p>
<p *ngIf="choice.limitanswers && choice.showavailable">
{{ 'addon.mod_choice.limita' | translate:{$a: result.maxanswer} }}
</p>
</ion-label>
</ion-item-divider>
<ion-item *ngFor="let user of result.userresponses" core-user-link [courseId]="courseId"
[userId]="user.userid" [title]="user.fullname" class="ion-text-wrap">
<core-user-avatar [user]="user" slot="start" [courseId]="courseId"></core-user-avatar>
<ion-label><p>{{user.fullname}}</p></ion-label>
</ion-item>
</ion-item-group>
</ion-col>
</ion-row>
</ion-grid>
</div>
<ion-card class="core-info-card" *ngIf="!canSeeResults && !choiceNotOpenYet">
<ion-item>
<ion-icon name="fas-info-circle" slot="start"></ion-icon>
<ion-label><p>{{ 'addon.mod_choice.noresultsviewable' | translate }}</p></ion-label>
</ion-item>
</ion-card>
</core-loading>
<!-- Template to render a choice option label. -->
<ng-template #optionLabelTemplate let-option="option">
<p>
<core-format-text [text]="option.text" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
</core-format-text>
<span *ngIf="choice!.limitanswers && option.countanswers >= option.maxanswers">
{{ 'addon.mod_choice.full' | translate }}
</span>
</p>
<ng-container *ngIf="choice!.limitanswers && choice!.showavailable">
<p>{{ 'addon.mod_choice.responsesa' | translate:{$a: option.countanswers} }}</p>
<p>{{ 'addon.mod_choice.limita' | translate:{$a: option.maxanswers} }}</p>
</ng-container>
</ng-template>

View File

@ -0,0 +1,479 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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, Optional, OnInit } from '@angular/core';
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse } from '@features/course/services/course';
import { IonContent } from '@ionic/angular';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTimeUtils } from '@services/utils/time';
import { Translate } from '@singletons';
import { CoreEvents } from '@singletons/events';
import {
AddonModChoice,
AddonModChoiceChoice,
AddonModChoiceOption,
AddonModChoiceProvider,
AddonModChoiceResult,
} from '../../services/choice';
import { AddonModChoiceOffline } from '../../services/choice-offline';
import {
AddonModChoiceAutoSyncData,
AddonModChoiceSync,
AddonModChoiceSyncProvider,
AddonModChoiceSyncResult,
} from '../../services/choice-sync';
import { AddonModChoicePrefetchHandler } from '../../services/handlers/prefetch';
/**
* Component that displays a choice.
*/
@Component({
selector: 'addon-mod-choice-index',
templateUrl: 'addon-mod-choice-index.html',
})
export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit {
component = AddonModChoiceProvider.COMPONENT;
moduleName = 'choice';
choice?: AddonModChoiceChoice;
options: AddonModChoiceOption[] = [];
selectedOption: {id: number} = { id: -1 };
choiceNotOpenYet = false;
choiceClosed = false;
canEdit = false;
canDelete = false;
canSeeResults = false;
data: number[] = [];
labels: string[] = [];
results: AddonModChoiceResultFormatted[] = [];
publishInfo?: string; // Message explaining the user what will happen with his choices.
openTimeReadable?: string;
closeTimeReadable?: string;
protected userId?: number;
protected syncEventName = AddonModChoiceSyncProvider.AUTO_SYNCED;
protected hasAnsweredOnline = false;
protected now = Date.now();
constructor(
protected content?: IonContent,
@Optional() courseContentsPage?: CoreCourseContentsPage,
) {
super('AddonModChoiceIndexComponent', content, courseContentsPage);
}
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
super.ngOnInit();
this.userId = CoreSites.getCurrentSiteUserId();
await this.loadContent(false, true);
if (!this.choice) {
return;
}
try {
await AddonModChoice.logView(this.choice.id, this.choice.name);
await CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
} catch {
// Ignore errors.
}
}
/**
* @inheritdoc
*/
protected async invalidateContent(): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(AddonModChoice.invalidateChoiceData(this.courseId));
if (this.choice) {
promises.push(AddonModChoice.invalidateOptions(this.choice.id));
promises.push(AddonModChoice.invalidateResults(this.choice.id));
}
await Promise.all(promises);
}
/**
* @inheritdoc
*/
protected isRefreshSyncNeeded(syncEventData: AddonModChoiceAutoSyncData): boolean {
if (this.choice && syncEventData.choiceId == this.choice.id && syncEventData.userId == this.userId) {
this.content?.scrollToTop();
return true;
}
return false;
}
/**
* @inheritdoc
*/
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
this.now = Date.now();
try {
this.choice = await AddonModChoice.getChoice(this.courseId, this.module.id);
if (sync) {
// Try to synchronize the choice.
const updated = await this.syncActivity(showErrors);
if (updated) {
// Responses were sent, update the choice.
this.choice = await AddonModChoice.getChoice(this.courseId, this.module.id);
}
}
this.choice.timeopen = (this.choice.timeopen || 0) * 1000;
this.choice.timeclose = (this.choice.timeclose || 0) * 1000;
this.openTimeReadable = CoreTimeUtils.userDate(this.choice.timeopen);
this.closeTimeReadable = CoreTimeUtils.userDate(this.choice.timeclose);
this.description = this.choice.intro;
this.choiceNotOpenYet = !!this.choice.timeopen && this.choice.timeopen > this.now;
this.choiceClosed = !!this.choice.timeclose && this.choice.timeclose <= this.now;
this.dataRetrieved.emit(this.choice);
// Check if there are responses stored in offline.
this.hasOffline = await AddonModChoiceOffline.hasResponse(this.choice.id);
// We need fetchOptions to finish before calling fetchResults because it needs hasAnsweredOnline variable.
await this.fetchOptions(this.choice);
await this.fetchResults(this.choice);
} finally {
this.fillContextMenu(refresh);
}
}
/**
* Convenience function to get choice options.
*
* @param choice Choice data.
* @return Promise resolved when done.
*/
protected async fetchOptions(choice: AddonModChoiceChoice): Promise<void> {
let options = await AddonModChoice.getOptions(choice.id, { cmId: this.module.id });
// Check if the user has answered (synced) to allow show results.
this.hasAnsweredOnline = options.some((option) => option.checked);
if (this.hasOffline) {
options = await this.getOfflineResponses(choice, options);
}
const isOpen = this.isChoiceOpen(choice);
this.selectedOption = { id: -1 }; // Single choice model.
const hasAnswered = options.some((option) => {
if (!option.checked) {
return false;
}
if (!choice.allowmultiple) {
this.selectedOption.id = option.id;
}
return true;
});
this.canEdit = isOpen && (choice.allowupdate! || !hasAnswered);
this.canDelete = isOpen && choice.allowupdate! && hasAnswered;
this.options = options;
if (!this.canEdit) {
return;
}
// Calculate the publish info message.
switch (choice.showresults) {
case AddonModChoiceProvider.RESULTS_NOT:
this.publishInfo = 'addon.mod_choice.publishinfonever';
break;
case AddonModChoiceProvider.RESULTS_AFTER_ANSWER:
if (choice.publish == AddonModChoiceProvider.PUBLISH_ANONYMOUS) {
this.publishInfo = 'addon.mod_choice.publishinfoanonafter';
} else {
this.publishInfo = 'addon.mod_choice.publishinfofullafter';
}
break;
case AddonModChoiceProvider.RESULTS_AFTER_CLOSE:
if (choice.publish == AddonModChoiceProvider.PUBLISH_ANONYMOUS) {
this.publishInfo = 'addon.mod_choice.publishinfoanonclose';
} else {
this.publishInfo = 'addon.mod_choice.publishinfofullclose';
}
break;
default:
// No need to inform the user since it's obvious that the results are being published.
this.publishInfo = '';
}
}
/**
* Get offline responses.
*
* @param choice Choice.
* @param options Online options.
* @return Promise resolved with the options.
*/
protected async getOfflineResponses(
choice: AddonModChoiceChoice,
options: AddonModChoiceOption[],
): Promise<AddonModChoiceOption[]> {
const response = await AddonModChoiceOffline.getResponse(choice.id);
const optionsMap: {[id: number]: AddonModChoiceOption} = {};
options.forEach((option) => {
optionsMap[option.id] = option;
});
// Update options with the offline data.
if (response.deleting) {
// Uncheck selected options.
if (response.responses.length > 0) {
// Uncheck all options selected in responses.
response.responses.forEach((selected) => {
if (optionsMap[selected] && optionsMap[selected].checked) {
optionsMap[selected].checked = false;
optionsMap[selected].countanswers--;
}
});
} else {
// On empty responses, uncheck all selected.
Object.keys(optionsMap).forEach((key) => {
if (optionsMap[key].checked) {
optionsMap[key].checked = false;
optionsMap[key].countanswers--;
}
});
}
} else {
// Uncheck all options to check again the offlines'.
Object.keys(optionsMap).forEach((key) => {
if (optionsMap[key].checked) {
optionsMap[key].checked = false;
optionsMap[key].countanswers--;
}
});
// Then check selected ones.
response.responses.forEach((selected) => {
if (optionsMap[selected]) {
optionsMap[selected].checked = true;
optionsMap[selected].countanswers++;
}
});
}
// Convert it again to array.
return Object.keys(optionsMap).map((key) => optionsMap[key]);
}
/**
* Convenience function to get choice results.
*
* @param choice Choice.
* @return Resolved when done.
*/
protected async fetchResults(choice: AddonModChoiceChoice): Promise<void> {
if (this.choiceNotOpenYet) {
// Cannot see results yet.
this.canSeeResults = false;
return;
}
const results = await AddonModChoice.getResults(choice.id, { cmId: this.module.id });
let hasVotes = false;
this.data = [];
this.labels = [];
this.results = results.map((result: AddonModChoiceResultFormatted) => {
if (result.numberofuser > 0) {
hasVotes = true;
}
this.data.push(result.numberofuser);
this.labels.push(result.text);
return Object.assign(result, { percentageamountfixed: result.percentageamount.toFixed(1) });
});
this.canSeeResults = hasVotes || AddonModChoice.canStudentSeeResults(choice, this.hasAnsweredOnline);
}
/**
* Check if a choice is open.
*
* @param choice Choice data.
* @return True if choice is open, false otherwise.
*/
protected isChoiceOpen(choice: AddonModChoiceChoice): boolean {
return (!choice.timeopen || choice.timeopen <= this.now) && (!choice.timeclose || choice.timeclose > this.now);
}
/**
* Return true if the user has selected at least one option.
*
* @return True if the user has responded.
*/
canSave(): boolean {
if (!this.choice) {
return false;
}
if (this.choice.allowmultiple) {
return this.options.some((option) => option.checked);
} else {
return this.selectedOption.id !== -1;
}
}
/**
* Save options selected.
*/
async save(): Promise<void> {
const choice = this.choice!;
// Only show confirm if choice doesn't allow update.
if (!choice.allowupdate) {
await CoreDomUtils.showConfirm(Translate.instant('core.areyousure'));
}
const responses: number[] = [];
if (choice.allowmultiple) {
this.options.forEach((option) => {
if (option.checked) {
responses.push(option.id);
}
});
} else {
responses.push(this.selectedOption.id);
}
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
try {
const online = await AddonModChoice.submitResponse(choice.id, choice.name, this.courseId, responses);
this.content?.scrollToTop();
if (online) {
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: this.moduleName });
// Check completion since it could be configured to complete once the user answers the choice.
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
}
await this.dataUpdated(online);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_choice.cannotsubmit', true);
} finally {
modal.dismiss();
}
}
/**
* Delete options selected.
*/
async delete(): Promise<void> {
try {
await CoreDomUtils.showDeleteConfirm();
} catch {
// User cancelled.
return;
}
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
try {
await AddonModChoice.deleteResponses(this.choice!.id, this.choice!.name, this.courseId);
this.content?.scrollToTop();
// Refresh the data. Don't call dataUpdated because deleting an answer doesn't mark the choice as outdated.
await this.refreshContent(false);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_choice.cannotsubmit', true);
} finally {
modal.dismiss();
}
}
/**
* Function to call when some data has changed. It will refresh/prefetch data.
*
* @param online Whether the data was sent to server or stored in offline.
* @return Promise resolved when done.
*/
protected async dataUpdated(online: boolean): Promise<void> {
if (!online || !this.isPrefetched) {
// Not downloaded, just refresh the data.
return this.refreshContent(false);
}
try {
// The choice is downloaded, update the data.
await AddonModChoiceSync.prefetchAfterUpdate(AddonModChoicePrefetchHandler.instance, this.module, this.courseId);
// Update the view.
this.showLoadingAndFetch(false, false);
} catch {
// Prefetch failed, refresh the data.
return this.refreshContent(false);
}
}
/**
* Performs the sync of the activity.
*
* @return Promise resolved when done.
*/
protected sync(): Promise<AddonModChoiceSyncResult> {
return AddonModChoiceSync.syncChoice(this.choice!.id, this.userId);
}
/**
* Checks if sync has succeed from result sync data.
*
* @param result Data returned on the sync function.
* @return Whether it succeed or not.
*/
protected hasSyncSucceed(result: AddonModChoiceSyncResult): boolean {
return result.updated;
}
}
/**
* Choice result with some calculated data.
*/
export type AddonModChoiceResultFormatted = AddonModChoiceResult & {
percentageamountfixed: string; // Percentage of users answers with fixed decimals.
};

View File

@ -0,0 +1,28 @@
{
"cannotsubmit": "Sorry, there was a problem submitting your choice. Please try again.",
"choiceoptions": "Choice options",
"errorgetchoice": "Error getting choice data.",
"expired": "This activity closed on {{$a}}.",
"full": "(Full)",
"limita": "Limit: {{$a}}",
"modulenameplural": "Choices",
"noresultsviewable": "The results are not currently viewable.",
"notopenyet": "This activity is not available until {{$a}}.",
"numberofuser": "Number of responses",
"numberofuserinpercentage": "Percentage of responses",
"previewonly": "This is just a preview of the available options for this activity. You will not be able to submit your choice until {{$a}}.",
"publishinfoanonafter": "Anonymous results will be published after you answer.",
"publishinfoanonclose": "Anonymous results will be published after the activity is closed.",
"publishinfofullafter": "Full results, showing everyone's choices, will be published after you answer.",
"publishinfofullclose": "Full results, showing everyone's choices, will be published after the activity is closed.",
"publishinfonever": "The results of this activity will not be published after you answer.",
"removemychoice": "Remove my choice",
"responses": "Responses",
"responsesa": "Responses: {{$a}}",
"responsesresultgraphdescription": "{{number}}% of the users chose the option: {{text}}.",
"responsesresultgraphheader": "Graph display",
"resultsnotsynced": "Your last response must be synchronised before it is included in the results.",
"savemychoice": "Save my choice",
"userchoosethisoption": "Users who chose this option",
"yourselection": "Your selection"
}

View File

@ -0,0 +1,21 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
</core-format-text>
</ion-title>
<ion-buttons slot="end">
<!-- The buttons defined by the component will be added in here. -->
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<addon-mod-choice-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)"></addon-mod-choice-index>
</ion-content>

View File

@ -0,0 +1,30 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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 { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page';
import { AddonModChoiceIndexComponent } from '../../components/index/index';
/**
* Page that displays a choice.
*/
@Component({
selector: 'page-addon-mod-choice-index',
templateUrl: 'index.html',
})
export class AddonModChoiceIndexPage extends CoreCourseModuleMainActivityPage<AddonModChoiceIndexComponent> {
@ViewChild(AddonModChoiceIndexComponent) activityComponent?: AddonModChoiceIndexComponent;
}