MOBILE-2768 policy: Support accepting all types of policies
parent
c7262f715e
commit
85e6446f4b
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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 {}
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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}}"
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue