MOBILE-2768 policy: Support accepting all types of policies

main
Dani Palou 2024-02-15 15:32:40 +01:00
parent c7262f715e
commit 85e6446f4b
14 changed files with 628 additions and 61 deletions

View File

@ -2282,12 +2282,19 @@
"core.phone": "moodle",
"core.pictureof": "moodle",
"core.play": "local_moodlemobileapp",
"core.policy.agreepolicies": "tool_policy",
"core.policy.backtotop": "tool_policy",
"core.policy.consentpagetitle": "tool_policy",
"core.policy.havereadandagreepolicy": "local_moodlemobileapp",
"core.policy.idontagree": "tool_policy",
"core.policy.mustagreetocontinue": "tool_policy",
"core.policy.policyacceptmandatory": "local_moodlemobileapp",
"core.policy.policyagree": "moodle",
"core.policy.policyagreement": "moodle",
"core.policy.policyagreementclick": "moodle",
"core.policy.refertofullpolicytext": "tool_policy",
"core.policy.sitepolicynotagreederror": "local_moodlemobileapp",
"core.policy.steppolicies": "tool_policy",
"core.previous": "moodle",
"core.proceed": "moodle",
"core.publicprofile": "moodle",

View File

@ -671,7 +671,7 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite {
this.triggerSiteEvent(CoreEvents.SITE_POLICY_NOT_AGREED, {});
error.message = Translate.instant('core.policy.sitepolicynotagreederror');
throw new CoreWSError(error);
throw new CoreSilentError(error);
} else if (error.errorcode === 'dmlwriteexception' && CoreTextUtils.hasUnicodeData(data)) {
if (!this.cleanUnicode) {
// Try again cleaning unicode.

View File

@ -2,7 +2,10 @@
<ng-container *ngFor="let error of errorKeys">
<div *ngIf="control.hasError(error)" class="core-input-error">
<ng-container *ngIf="error !== 'pattern'">
<span *ngIf="errorMessages && errorMessages[error]">{{ errorMessages[error] | translate }}</span>
<span *ngIf="errorMessages && errorMessages[error]">
<ion-icon *ngIf="error === 'required'" name="fas-circle-exclamation" aria-hidden="true" />
{{ errorMessages[error] | translate }}
</span>
<span *ngIf="(!errorMessages || !errorMessages[error]) && error === 'max' && control.errors?.max">
{{ 'core.login.invalidvaluemax' | translate:{$a: control.errors!.max.max} }}
</span>

View File

@ -62,10 +62,6 @@ const appRoutes: Routes = [
CoreLoginHelper.passwordChangeForced(data.siteId);
});
CoreEvents.on(CoreEvents.SITE_POLICY_NOT_AGREED, (data) => {
CoreLoginHelper.sitePolicyNotAgreed(data.siteId);
});
await CoreLoginHelper.initialize();
},
},

View File

@ -0,0 +1,31 @@
// (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 { CoreSharedModule } from '@/core/shared.module';
import { CorePolicyViewPolicyModalComponent } from './policy-modal/policy-modal';
@NgModule({
declarations: [
CorePolicyViewPolicyModalComponent,
],
imports: [
CoreSharedModule,
],
exports: [
CorePolicyViewPolicyModalComponent,
],
})
export class CorePolicyComponentsModule {}

View File

@ -0,0 +1,24 @@
<ion-header>
<ion-toolbar>
<ion-title *ngIf="policy">
<h1>{{ policy.name }}</h1>
</ion-title>
<ion-buttons slot="end">
<ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="fas-xmark" slot="icon-only" aria-hidden=true />
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item *ngIf="policy" class="ion-text-wrap">
<ion-label>
<core-format-text [text]="policy.summary" contextLevel="system" [contextInstanceId]="0" />
</ion-label>
</ion-item>
<ion-item *ngIf="policy" class="ion-text-wrap">
<ion-label>
<core-format-text [text]="policy.content" contextLevel="system" [contextInstanceId]="0" />
</ion-label>
</ion-item>
</ion-content>

View File

@ -0,0 +1,37 @@
// (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, Input } from '@angular/core';
import { CorePolicySitePolicy } from '@features/policy/services/policy';
import { ModalController } from '@singletons';
/**
* Modal to view a policy.
*/
@Component({
selector: 'core-policy-view-policy-modal',
templateUrl: 'policy-modal.html',
})
export class CorePolicyViewPolicyModalComponent {
@Input() policy?: CorePolicySitePolicy;
/**
* Close modal.
*/
closeModal(): void {
ModalController.dismiss();
}
}

View File

@ -1,8 +1,15 @@
{
"agreepolicies": "Please agree to the following policies",
"backtotop": "Back to top",
"consentpagetitle": "Consent",
"havereadandagreepolicy": "I have read and agree to the {{policyname}}",
"idontagree": "No thanks, I decline {{$a}}",
"mustagreetocontinue": "Before continuing you need to acknowledge all these policies.",
"policyacceptmandatory": "I understand and agree to the mandatory site policies",
"policyagree": "You must agree to this policy to continue using this site. Do you agree?",
"policyagreement": "Site policy agreement",
"policyagreementclick": "Link to site policy agreement",
"sitepolicynotagreederror": "Site policy not agreed."
"refertofullpolicytext": "Please refer to the full {{$a}} if you would like to review the text.",
"sitepolicynotagreederror": "Site policy not agreed.",
"steppolicies": "Policy {{$a.numpolicy}} out of {{$a.totalpolicies}}"
}

View File

@ -17,13 +17,22 @@
</ion-header>
<ion-content class="limited-width">
<core-loading [hideUntil]="policyLoaded">
<form *ngIf="policyForm" [formGroup]="policyForm" (ngSubmit)="submitAcceptances($event)">
<ion-item class="ion-text-wrap">
<form *ngIf="policiesForm" [formGroup]="policiesForm" (ngSubmit)="submitAcceptances($event)">
<ion-item class="core-site-policy-top-bar" *ngIf="stepData">
<ion-label>
<h2>{{ 'core.policy.policyagreement' | translate }}</h2>
<p>{{ 'core.policy.steppolicies' | translate:{ $a:stepData } }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="title">
<ion-label>
<h2>{{ title }}</h2>
<p *ngIf="subTitle">{{ subTitle }}</p>
</ion-label>
</ion-item>
<hr>
<!-- Accepting all policies at once, using a URL to view them. -->
<ng-container *ngIf="isPoliciesURL && sitePoliciesURL">
<ion-card class="core-info-card">
<ion-item class="ion-text-wrap">
@ -48,18 +57,104 @@
</div>
<ion-item class="ion-text-wrap">
<ion-checkbox name="agreepolicy" formControlName="agreepolicy">
<span *ngIf="isManageAcceptancesAvailable" [core-mark-required]="true">
<p *ngIf="isManageAcceptancesAvailable" [core-mark-required]="true">
{{ 'core.policy.havereadandagreepolicy' | translate:{ policyname:'core.policy.policyagreement' | translate } }}
</span>
<span *ngIf="!isManageAcceptancesAvailable" [core-mark-required]="true">
</p>
<p *ngIf="!isManageAcceptancesAvailable" [core-mark-required]="true">
{{ 'core.policy.policyacceptmandatory' | translate }}
</span>
</p>
</ion-checkbox>
</ion-item>
<ion-button type="submit" class="ion-text-wrap ion-margin-horizontal" expand="block" [disabled]="!policyForm.valid">
</ng-container>
<!-- Accepting policies one by one , either in same page or using a consent form at the end. -->
<ng-container *ngIf="!isPoliciesURL && (currentPolicy || showConsentForm)">
<ng-container *ngIf="currentPolicy">
<ion-item class="ion-text-wrap core-site-policy-summary">
<ion-label>
<core-format-text [text]="currentPolicy.summary" contextLevel="system" [contextInstanceId]="0"
(afterRender)="checkScroll()" />
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap core-site-policy-content">
<ion-label>
<core-format-text [text]="currentPolicy.content" contextLevel="system" [contextInstanceId]="0"
(afterRender)="checkScroll()" />
</ion-label>
</ion-item>
</ng-container>
<ng-container *ngIf="showConsentForm">
<ng-container *ngFor="let policy of pendingPolicies">
<ion-item class="ion-text-wrap">
<ion-label>
<h2>{{ policy.name }}</h2>
<div [collapsible-item]="64">
<core-format-text [text]="policy.summary" contextLevel="system" [contextInstanceId]="0"
(afterRender)="checkScroll()" />
</div>
</ion-label>
</ion-item>
<ion-item *ngIf="policy.referToFullPolicyText" class="ion-text-wrap" button detail="false"
(click)="viewFullPolicy(policy)">
<ion-label>
<p [innerHTML]="policy.referToFullPolicyText"></p>
</ion-label>
</ion-item>
<ng-container *ngTemplateOutlet="policyForm; context: {policy: policy}" />
</ng-container>
</ng-container>
<ion-item class="core-site-policy-bottom-bar" *ngIf="stepData || hasScroll">
<ion-label>
<p *ngIf="stepData">{{ 'core.policy.steppolicies' | translate:{ $a:stepData } }}</p>
</ion-label>
<ion-button *ngIf="hasScroll" class="core-site-policy-go-top-button" color="secondary" slot="end"
(click)="scrollTop($event)" [attr.aria-label]="'core.policy.backtotop' | translate">
<ion-icon name="fas-chevron-up" slot="icon-only" aria-hidden="true" />
</ion-button>
</ion-item>
<ng-container *ngIf="agreeInOwnPage">
<ng-container *ngTemplateOutlet="policyForm; context: {policy: currentPolicy}" />
</ng-container>
</ng-container>
<div class="core-site-policy-buttons ion-margin-horizontal">
<ion-button *ngIf="(isPoliciesURL && sitePoliciesURL) || showConsentForm || agreeInOwnPage" type="submit"
class="ion-text-wrap" [expand]="isTablet ? null : 'block'"
[disabled]="(isPoliciesURL || agreeInOwnPage) && !policiesForm.valid">
{{ 'core.continue' | translate }}
</ion-button>
</ng-container>
<ion-button *ngIf="!isPoliciesURL && currentPolicy && !agreeInOwnPage" class="ion-text-wrap"
[expand]="isTablet ? null : 'block'" (click)="nextPolicy($event)">
{{ 'core.next' | translate }}
</ion-button>
</div>
</form>
</core-loading>
</ion-content>
<ng-template #policyForm let-policy="policy">
<ion-item class="ion-text-wrap" *ngIf="!policy.optional && policiesForm">
<ion-checkbox [formControl]="policiesForm.controls['agreepolicy' + policy.versionid]">
<p [core-mark-required]="true">
{{ 'core.policy.havereadandagreepolicy' | translate:{ policyname:policy.name } }}
</p>
</ion-checkbox>
<core-input-errors [control]="policiesForm.controls['agreepolicy' + policy.versionid]" [errorMessages]="policiesErrors" />
</ion-item>
<ion-radio-group *ngIf="policy.optional && policiesForm" [formControl]="policiesForm.controls['agreepolicy' + policy.versionid]">
<ion-item class="ion-text-wrap">
<ion-radio [value]="1">
<p>{{ 'core.policy.havereadandagreepolicy' | translate:{ policyname:policy.name } }}</p>
</ion-radio>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-radio [value]="0">
<p>{{ 'core.policy.idontagree' | translate:{ $a:policy.name } }}</p>
</ion-radio>
<core-input-errors [control]="policiesForm.controls['agreepolicy' + policy.versionid]" [errorMessages]="policiesErrors" />
</ion-item>
</ion-radio-group>
</ng-template>

View File

@ -6,6 +6,10 @@
margin: 0 16px;
}
h2 {
font-size: 1.25rem;
}
form {
display: flex;
flex-direction: column;
@ -20,6 +24,17 @@
--inner-border-width: 0;
}
.core-site-policy-top-bar, .core-site-policy-bottom-bar {
font-size: 0.875rem;
}
.core-site-policy-top-bar ion-label {
margin-bottom: 0;
}
.core-site-policy-bottom-bar ion-label {
margin-top: 0;
}
.core-site-policy-link {
p {
text-decoration: underline;
@ -38,8 +53,29 @@
flex-grow: 1;
}
ion-button[type="submit"] {
margin-bottom: 12px;;
.core-site-policy-content ion-label {
margin-top: 0;
}
.core-site-policy-go-top-button {
--border-radius: 50%;
--box-shadow: 0 3px 5px -1px rgba(0, 0, 0, .2), 0 6px 10px 0 rgba(0, 0, 0, .14), 0 1px 18px 0 rgba(0, 0, 0, .12);
margin-bottom: 8px;
}
.core-site-policy-buttons {
text-align: center;
ion-button {
margin-left: 0;
margin-right: 0;
margin-bottom: 12px;
}
}
.item ::ng-deep ion-label p span {
color: var(--core-link-color);
}
}
}

View File

@ -12,9 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { CoreSites } from '@services/sites';
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
@ -23,8 +23,14 @@ import { CoreNavigator } from '@services/navigator';
import { CoreEvents } from '@singletons/events';
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
import { Translate } from '@singletons';
import { CorePolicy } from '@features/policy/services/policy';
import { CorePolicy, CorePolicyAgreementStyle, CorePolicySitePolicy } from '@features/policy/services/policy';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { CoreUrlUtils } from '@services/utils/url';
import { IonContent } from '@ionic/angular';
import { CoreScreen } from '@services/screen';
import { Subscription } from 'rxjs';
import { CoreDom } from '@singletons/dom';
import { CorePolicyViewPolicyModalComponent } from '@features/policy/components/policy-modal/policy-modal';
/**
* Page to accept a site policy.
@ -34,18 +40,38 @@ import { FormControl, FormGroup, Validators } from '@angular/forms';
templateUrl: 'site-policy.html',
styleUrls: ['site-policy.scss'],
})
export class CorePolicySitePolicyPage implements OnInit {
export class CorePolicySitePolicyPage implements OnInit, OnDestroy {
@ViewChild(IonContent) content?: IonContent;
siteName?: string;
isManageAcceptancesAvailable = false;
policyLoaded?: boolean;
policiesForm?: FormGroup;
isPoliciesURL = false;
title = '';
subTitle?: string;
hasScroll = false;
isTablet = false;
// Variables for accepting policies using a URL.
sitePoliciesURL?: string;
showInline?: boolean;
policyLoaded?: boolean;
policyForm?: FormGroup;
// Variables for accepting policies one by one.
currentPolicy?: SitePolicy;
pendingPolicies?: SitePolicy[];
agreeInOwnPage = false;
numPolicy = 1;
showConsentForm = false;
stepData?: {numpolicy: number; totalpolicies: number};
policiesErrors = { required: Translate.instant('core.policy.mustagreetocontinue') };
protected siteId?: string;
protected currentSite!: CoreSite;
protected layoutSubscription?: Subscription;
constructor(protected elementRef: ElementRef, protected changeDetector: ChangeDetectorRef) {}
/**
* @inheritdoc
@ -73,26 +99,23 @@ export class CorePolicySitePolicyPage implements OnInit {
return;
}
this.isTablet = CoreScreen.isTablet;
this.layoutSubscription = CoreScreen.layoutObservable.subscribe(() => {
this.isTablet = CoreScreen.isTablet;
});
this.isManageAcceptancesAvailable = await CorePolicy.isManageAcceptancesAvailable(this.siteId);
this.isPoliciesURL = this.isManageAcceptancesAvailable ?
(await this.currentSite.getConfig('sitepolicyhandler')) !== 'tool_policy' :
true; // Site doesn't support managing acceptances, just display it as a URL.
if (this.isPoliciesURL) {
this.initFormForPoliciesURL();
await this.fetchSitePoliciesURL();
} else {
// TODO
}
CoreAnalytics.logEvent({
type: CoreAnalyticsEventType.VIEW_ITEM,
ws: 'auth_email_get_signup_settings',
name: Translate.instant('core.policy.policyagreement'),
data: { category: 'policy' },
url: '/user/policy.php',
});
this.initFormForPoliciesURL();
} else {
await this.fetchNextPoliciesToAccept();
}
}
/**
@ -101,6 +124,9 @@ export class CorePolicySitePolicyPage implements OnInit {
* @returns Promise resolved when done.
*/
protected async fetchSitePoliciesURL(): Promise<void> {
this.title = Translate.instant('core.policy.policyagreement');
this.subTitle = undefined;
try {
this.sitePoliciesURL = await CorePolicy.getSitePoliciesURL(this.siteId);
} catch (error) {
@ -122,13 +148,103 @@ export class CorePolicySitePolicyPage implements OnInit {
} finally {
this.policyLoaded = true;
}
CoreAnalytics.logEvent({
type: CoreAnalyticsEventType.VIEW_ITEM,
ws: 'auth_email_get_signup_settings',
name: Translate.instant('core.policy.policyagreement'),
data: { category: 'policy' },
url: '/user/policy.php',
});
}
/**
* Fetch the next site policies to accept.
*
* @returns Promise resolved when done.
*/
protected async fetchNextPoliciesToAccept(): Promise<void> {
try {
this.scrollTop();
const pendingPolicies = await CorePolicy.getNextPendingPolicies({
readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK,
siteId: this.siteId,
});
// Add some calculated data.
this.pendingPolicies = pendingPolicies.map((policy: SitePolicy) => {
policy.referToFullPolicyText = Translate.instant('core.policy.refertofullpolicytext', {
$a: `<span class="core-site-policy-full-policy-link">${policy.name}</span>`,
});
return policy;
});
const policy = this.pendingPolicies[0];
if (!policy) {
// No more policies to accept.
await this.finishAcceptingPolicies();
return;
}
this.initFormForPendingPolicies();
this.agreeInOwnPage = policy.agreementstyle === CorePolicyAgreementStyle.OwnPage;
this.showConsentForm = false;
this.numPolicy = 1;
this.setCurrentPolicy(policy);
this.policyLoaded = true;
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error getting site policy.');
this.cancel();
}
}
/**
* Log in analytics viewing a certain policy.
*/
protected logAnalyticsPolicyView(): void {
if (!this.currentPolicy) {
return;
}
const analyticsParams: Record<string, string | number> = {
versionid: this.currentPolicy.versionid,
};
if (!this.agreeInOwnPage) {
analyticsParams.numpolicy = this.numPolicy;
analyticsParams.totalpolicies = this.pendingPolicies?.length ?? this.numPolicy;
}
CoreAnalytics.logEvent({
type: CoreAnalyticsEventType.VIEW_ITEM,
ws: 'tool_policy_get_user_acceptances',
name: this.currentPolicy.name,
data: analyticsParams,
url: CoreUrlUtils.addParamsToUrl('/admin/tool/policy/view.php', analyticsParams),
});
}
/**
* Log in analytics viewing the consent form.
*/
protected logAnalyticsConsentFormView(): void {
CoreAnalytics.logEvent({
type: CoreAnalyticsEventType.VIEW_ITEM,
ws: 'tool_policy_get_user_acceptances',
name: Translate.instant('core.policy.consentpagetitle'),
data: {},
url: CoreUrlUtils.addParamsToUrl('/admin/tool/policy/index.php'),
});
}
/**
* Init the form to accept the policies using a URL.
*/
protected initFormForPoliciesURL(): void {
this.policyForm = new FormGroup({
this.policiesForm = new FormGroup({
agreepolicy: new FormControl(false, {
validators: Validators.requiredTrue,
nonNullable: true,
@ -136,6 +252,26 @@ export class CorePolicySitePolicyPage implements OnInit {
});
}
/**
* Init the form to accept the current pending policies.
*/
protected initFormForPendingPolicies(): void {
this.policiesForm = new FormGroup({});
this.pendingPolicies?.forEach(policy => {
if (policy.optional) {
this.policiesForm?.addControl('agreepolicy' + policy.versionid, new FormControl<number | undefined>(undefined, {
validators: Validators.required,
}));
} else {
this.policiesForm?.addControl('agreepolicy' + policy.versionid, new FormControl(false, {
validators: Validators.requiredTrue,
nonNullable: true,
}));
}
});
}
/**
* Cancel.
*
@ -147,6 +283,68 @@ export class CorePolicySitePolicyPage implements OnInit {
await CoreNavigator.navigate('/login/sites', { reset: true });
}
/**
* Load next policy.
*
* @param event Event.
*/
nextPolicy(event: Event): void {
event.preventDefault();
event.stopPropagation();
if (!this.pendingPolicies) {
return;
}
this.scrollTop();
if (this.numPolicy < this.pendingPolicies.length) {
this.numPolicy++;
this.setCurrentPolicy(this.pendingPolicies[this.numPolicy - 1]);
return;
}
// All policies seen, display the consent form.
this.currentPolicy = undefined;
this.stepData = undefined;
this.showConsentForm = true;
this.title = Translate.instant('core.policy.consentpagetitle');
this.subTitle = Translate.instant('core.policy.agreepolicies');
this.logAnalyticsConsentFormView();
}
/**
* Set current policy.
*/
protected setCurrentPolicy(policy?: CorePolicySitePolicy): void {
if (!policy) {
return;
}
this.hasScroll = false;
this.currentPolicy = policy;
this.title = policy.name || '';
this.subTitle = undefined;
this.stepData = !this.agreeInOwnPage ?
{ numpolicy: this.numPolicy, totalpolicies: this.pendingPolicies?.length ?? this.numPolicy } :
undefined;
this.logAnalyticsPolicyView();
}
/**
* Check if the content has scroll.
*/
protected async checkScroll(): Promise<void> {
await CoreUtils.wait(400);
const scrollElement = await this.content?.getScrollElement();
this.hasScroll = !!scrollElement && scrollElement.scrollHeight > scrollElement.clientHeight;
}
/**
* Submit the acceptances to one or several policies.
*
@ -157,27 +355,131 @@ export class CorePolicySitePolicyPage implements OnInit {
event.preventDefault();
event.stopPropagation();
if (!this.policyForm?.valid) {
if (!this.policiesForm?.valid) {
for (const name in this.policiesForm?.controls) {
this.policiesForm.controls[name].markAsDirty();
}
this.changeDetector.detectChanges();
// Scroll to the first element with errors.
const errorFound = await CoreDom.scrollToInputError(
this.elementRef.nativeElement,
);
if (!errorFound) {
// Input not found, show an error modal.
CoreDomUtils.showErrorModal('core.policy.mustagreetocontinue', true);
}
return;
}
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
try {
if (!this.isPoliciesURL) {
await this.acceptPendingPolicies();
return;
}
await CorePolicy.acceptMandatorySitePolicies(this.siteId);
// Success accepting, go to site initial page.
// Invalidate cache since some WS don't return error if site policy is not accepted.
await CoreUtils.ignoreErrors(this.currentSite.invalidateWsCache());
CoreEvents.trigger(CoreEvents.SITE_POLICY_AGREED, {}, this.siteId);
await CoreNavigator.navigateToSiteHome();
await this.finishAcceptingPolicies();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error accepting site policy.');
CoreDomUtils.showErrorModalDefault(error, 'Error accepting site policies.');
} finally {
modal.dismiss();
}
}
/**
* Accept current pending policies.
*/
protected async acceptPendingPolicies(): Promise<void> {
if (!this.pendingPolicies) {
return;
}
const acceptances: Record<number, number> = {};
this.pendingPolicies?.forEach(policy => {
const control = this.policiesForm?.controls['agreepolicy' + policy.versionid];
if (!control) {
return;
}
if (policy.optional) {
if (control.value === null || control.value === undefined) {
// Not answered, this code shouldn't be reached. Display error.
CoreDomUtils.showErrorModal('core.policy.mustagreetocontinue', true);
return;
}
acceptances[policy.versionid] = control.value;
} else {
if (!control.value) {
// Not answered, this code shouldn't be reached. Display error.
CoreDomUtils.showErrorModal('core.policy.mustagreetocontinue', true);
return;
}
acceptances[policy.versionid] = 1;
}
});
await CorePolicy.setUserAcceptances(acceptances, this.siteId);
await this.fetchNextPoliciesToAccept();
}
/**
* All mandatory policies have been accepted, go to site initial page.
*/
protected async finishAcceptingPolicies(): Promise<void> {
// Invalidate cache since some WS don't return error if site policy is not accepted.
await CoreUtils.ignoreErrors(this.currentSite.invalidateWsCache());
CoreEvents.trigger(CoreEvents.SITE_POLICY_AGREED, {}, this.siteId);
await CoreNavigator.navigateToSiteHome();
}
/**
* Scroll to top.
*
* @param event Event.
*/
scrollTop(event?: Event): void {
event?.preventDefault();
event?.stopPropagation();
this.content?.scrollToTop(400);
}
/**
* View the full policy.
*
* @param policy Policy.
*/
viewFullPolicy(policy: CorePolicySitePolicy): void {
CoreDomUtils.openModal({
component: CorePolicyViewPolicyModalComponent,
componentProps: { policy },
});
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.layoutSubscription?.unsubscribe();
}
}
type SitePolicy = CorePolicySitePolicy & {
referToFullPolicyText?: string;
};

View File

@ -18,6 +18,7 @@ import { RouterModule, Routes } from '@angular/router';
import { CoreSharedModule } from '@/core/shared.module';
import { CorePolicySitePolicyPage } from '@features/policy/pages/site-policy/site-policy';
import { SITE_POLICY_PAGE_NAME } from './constants';
import { CorePolicyComponentsModule } from './components/components.module';
const routes: Routes = [
{
@ -30,6 +31,7 @@ const routes: Routes = [
imports: [
CoreSharedModule,
RouterModule.forChild(routes),
CorePolicyComponentsModule,
],
declarations: [
CorePolicySitePolicyPage,

View File

@ -12,26 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { APP_INITIALIZER, NgModule, Type } from '@angular/core';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { Routes } from '@angular/router';
import { AppRoutingModule } from '@/app/app-routing.module';
import { CoreEvents } from '@singletons/events';
import { POLICY_PAGE_NAME } from './constants';
/**
* Get policy services.
*
* @returns Policy services.
*/
export async function getPolicyServices(): Promise<Type<unknown>[]> {
const { CorePolicyService } = await import('@features/policy/services/policy');
return [
CorePolicyService,
];
}
const routes: Routes = [
{
path: POLICY_PAGE_NAME,

View File

@ -89,6 +89,38 @@ export class CorePolicyService {
return sitePolicy;
}
/**
* Get the next policies to accept.
*
* @param options Options
* @returns Next pending policies
*/
async getNextPendingPolicies(options: CoreSitesCommonWSOptions = {}): Promise<CorePolicySitePolicy[]> {
const policies = await this.getUserAcceptances(options);
const pendingPolicies: CorePolicySitePolicy[] = [];
for (const i in policies) {
const policy = policies[i];
const hasAccepted = policy.acceptance?.status === 1;
const hasDeclined = policy.acceptance?.status === 0;
if (hasAccepted || (hasDeclined && policy.optional === 1)) {
// Policy already answered, ignore.
continue;
}
if (policy.agreementstyle === CorePolicyAgreementStyle.OwnPage) {
// Policy needs to be accepted on its own page, it's the next policy to accept.
return [policy];
}
pendingPolicies.push(policy);
}
return pendingPolicies;
}
/**
* Get user acceptances.
*
@ -177,7 +209,7 @@ export class CorePolicyService {
})),
};
const response = await site.write<CorePolicySetAcceptancesWSResponse>('tool_policy_get_user_acceptances', data);
const response = await site.write<CorePolicySetAcceptancesWSResponse>('tool_policy_set_acceptances_status', data);
if (response.warnings?.length) {
throw new CoreWSError(response.warnings[0]);
}
@ -260,3 +292,11 @@ type CorePolicySetAcceptancesWSResponse = {
policyagreed: number; // Whether the user has provided acceptance to all current site policies. 1 if yes, 0 if not.
warnings?: CoreWSExternalWarning[];
};
/**
* Agreement style.
*/
export enum CorePolicyAgreementStyle {
ConsentPage = 0, // Policy to be accepted together with others on the consent page.
OwnPage = 1, // Policy to be accepted on its own page before reaching the consent page.
}