MOBILE-2768 policy: Support viewing agreements
parent
85e6446f4b
commit
b33b8b07fc
|
@ -94,6 +94,7 @@ jobs:
|
|||
"@core_grades"
|
||||
"@core_login"
|
||||
"@core_mainmenu"
|
||||
"@core_policy"
|
||||
"@core_reminders"
|
||||
"@core_reportbuilder"
|
||||
"@core_search"
|
||||
|
|
|
@ -1553,6 +1553,7 @@
|
|||
"core.connectionlost": "local_moodlemobileapp",
|
||||
"core.considereddigitalminor": "moodle",
|
||||
"core.contactsupport": "local_moodlemobileapp",
|
||||
"core.contactverb": "local_moodlemobileapp",
|
||||
"core.content": "moodle",
|
||||
"core.contenteditingsynced": "local_moodlemobileapp",
|
||||
"core.contentlinks.chooseaccount": "local_moodlemobileapp",
|
||||
|
@ -2282,19 +2283,42 @@
|
|||
"core.phone": "moodle",
|
||||
"core.pictureof": "moodle",
|
||||
"core.play": "local_moodlemobileapp",
|
||||
"core.policy.acceptancenote": "tool_policy",
|
||||
"core.policy.acceptancestatusaccepted": "tool_policy",
|
||||
"core.policy.acceptancestatusacceptedbehalf": "tool_policy",
|
||||
"core.policy.acceptancestatusdeclined": "tool_policy",
|
||||
"core.policy.acceptancestatusdeclinedbehalf": "tool_policy",
|
||||
"core.policy.acceptancestatuspending": "tool_policy",
|
||||
"core.policy.agreepolicies": "tool_policy",
|
||||
"core.policy.backtotop": "tool_policy",
|
||||
"core.policy.consentpagetitle": "tool_policy",
|
||||
"core.policy.contactdpo": "tool_policy",
|
||||
"core.policy.havereadandagreepolicy": "local_moodlemobileapp",
|
||||
"core.policy.idontagree": "tool_policy",
|
||||
"core.policy.mustagreetocontinue": "tool_policy",
|
||||
"core.policy.nopoliciesyet": "local_moodlemobileapp",
|
||||
"core.policy.policiesagreements": "tool_policy",
|
||||
"core.policy.policyacceptmandatory": "local_moodlemobileapp",
|
||||
"core.policy.policyagree": "moodle",
|
||||
"core.policy.policyagreement": "moodle",
|
||||
"core.policy.policyagreementclick": "moodle",
|
||||
"core.policy.policydocname": "tool_policy",
|
||||
"core.policy.policydocoptionalyes": "tool_policy",
|
||||
"core.policy.policydocrevision": "tool_policy",
|
||||
"core.policy.refertofullpolicytext": "tool_policy",
|
||||
"core.policy.response": "tool_policy",
|
||||
"core.policy.responseby": "tool_policy",
|
||||
"core.policy.responseon": "tool_policy",
|
||||
"core.policy.sitepolicynotagreederror": "local_moodlemobileapp",
|
||||
"core.policy.status1": "tool_policy",
|
||||
"core.policy.steppolicies": "tool_policy",
|
||||
"core.policy.useracceptanceactionaccept": "tool_policy",
|
||||
"core.policy.useracceptanceactionacceptone": "tool_policy",
|
||||
"core.policy.useracceptanceactiondecline": "tool_policy",
|
||||
"core.policy.useracceptanceactiondeclineone": "tool_policy",
|
||||
"core.policy.useracceptanceactionrevoke": "tool_policy",
|
||||
"core.policy.useracceptanceactionrevokeone": "tool_policy",
|
||||
"core.policy.viewpolicy": "local_moodlemobileapp",
|
||||
"core.previous": "moodle",
|
||||
"core.proceed": "moodle",
|
||||
"core.publicprofile": "moodle",
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="17" viewBox="0 0 20 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.5459 4.25281C4.5459 3.83859 4.21011 3.50281 3.7959 3.50281C3.38168 3.50281 3.0459 3.83859 3.0459 4.25281C3.0459 4.57259 3.04612 5.06957 3.04641 5.7082C3.04674 6.42035 3.04714 7.30873 3.0474 8.32386C3.04779 9.8405 4.2759 11.0715 5.79313 11.074L14.3978 11.0882L12.3432 13.1431C12.0503 13.436 12.0503 13.9109 12.3432 14.2038C12.6361 14.4966 13.111 14.4966 13.4039 14.2037L16.7351 10.8721C17.0279 10.5792 17.0279 10.1043 16.735 9.81143L13.4034 6.47991C13.1105 6.18703 12.6356 6.18704 12.3427 6.47993C12.0498 6.77283 12.0498 7.24771 12.3427 7.54059L14.3904 9.58821L5.7956 9.57403C5.10627 9.57289 4.54758 9.0134 4.5474 8.32348C4.54714 7.30775 4.54674 6.41971 4.54641 5.70791C4.54612 5.06975 4.5459 4.57254 4.5459 4.25281Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 847 B |
|
@ -15,3 +15,4 @@
|
|||
// Routing.
|
||||
export const POLICY_PAGE_NAME = 'policy';
|
||||
export const SITE_POLICY_PAGE_NAME = 'sitepolicy';
|
||||
export const ACCEPTANCES_PAGE_NAME = 'acceptances';
|
||||
|
|
|
@ -1,15 +1,38 @@
|
|||
{
|
||||
"acceptancenote": "Remarks",
|
||||
"acceptancestatusaccepted": "Accepted",
|
||||
"acceptancestatusacceptedbehalf": "Accepted on user's behalf",
|
||||
"acceptancestatusdeclined": "Declined",
|
||||
"acceptancestatusdeclinedbehalf": "Declined on user's behalf",
|
||||
"acceptancestatuspending": "Pending",
|
||||
"agreepolicies": "Please agree to the following policies",
|
||||
"backtotop": "Back to top",
|
||||
"consentpagetitle": "Consent",
|
||||
"contactdpo": "For any questions about the policies please contact the privacy officer.",
|
||||
"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.",
|
||||
"nopoliciesyet": "No policies and agreements yet.",
|
||||
"policiesagreements": "Policies and agreements",
|
||||
"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",
|
||||
"policydocname": "Name",
|
||||
"policydocoptionalyes": "Optional",
|
||||
"policydocrevision": "Version",
|
||||
"refertofullpolicytext": "Please refer to the full {{$a}} if you would like to review the text.",
|
||||
"response": "Response",
|
||||
"responseby": "Respondent",
|
||||
"responseon": "Date",
|
||||
"sitepolicynotagreederror": "Site policy not agreed.",
|
||||
"steppolicies": "Policy {{$a.numpolicy}} out of {{$a.totalpolicies}}"
|
||||
"steppolicies": "Policy {{$a.numpolicy}} out of {{$a.totalpolicies}}",
|
||||
"status1": "Active",
|
||||
"useracceptanceactionaccept": "Accept",
|
||||
"useracceptanceactionacceptone": "Accept {{$a}}",
|
||||
"useracceptanceactiondecline": "Decline",
|
||||
"useracceptanceactiondeclineone": "Decline {{$a}}",
|
||||
"useracceptanceactionrevoke": "Withdraw",
|
||||
"useracceptanceactionrevokeone": "Withdraw acceptance of {{$a}}",
|
||||
"viewpolicy": "View policy {{policyname}}."
|
||||
}
|
||||
|
|
|
@ -0,0 +1,309 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [text]="'core.back' | translate" />
|
||||
</ion-buttons>
|
||||
|
||||
<ion-title>
|
||||
<h1>{{ 'core.policy.policiesagreements' | translate }}</h1>
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!dataLoaded" (ionRefresh)="refreshAcceptances($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" />
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="dataLoaded">
|
||||
<div class="list-item-limited-width">
|
||||
<ion-card class="core-info-card">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-icon name="fas-circle-info" slot="start" aria-hidden="true" />
|
||||
<ion-label>
|
||||
<p>{{ 'core.policy.contactdpo' | translate }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<div class="core-card-buttons" *ngIf="canContactDPO">
|
||||
<ion-button fill="outline" (click)="openContactDPO($event)">{{ 'core.contactverb' | translate }}</ion-button>
|
||||
</div>
|
||||
</ion-card>
|
||||
</div>
|
||||
|
||||
<table *ngIf="isTablet && policies.length" class="core-table x-scrollable core-policy-tablet-container">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'core.policy.policydocname' | translate }}</th>
|
||||
<th>{{ 'core.policy.policydocrevision' | translate }}</th>
|
||||
<th>{{ 'core.policy.response' | translate }}</th>
|
||||
<th>{{ 'core.policy.responseon' | translate }}</th>
|
||||
<ng-container *ngIf="hasOnBehalf">
|
||||
<th>{{ 'core.policy.responseby' | translate }}</th>
|
||||
<th>{{ 'core.policy.acceptancenote' | translate }}</th>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="auto-striped">
|
||||
<ng-container *ngFor="let policy of policies">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="policyTabletTemplate; context: {policy: policy, isPreviousVersion: false, hidden: false}" />
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div *ngIf="!isTablet && policies.length" class="core-policy-mobile-container">
|
||||
<ng-container *ngFor="let policy of policies">
|
||||
<ng-container *ngTemplateOutlet="policyMobileTemplate; context: {policy: policy, isPreviousVersion: false}" />
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<core-empty-box *ngIf="!policies.length" icon="fas-file-shield" [message]="'core.policy.nopoliciesyet' | translate" />
|
||||
|
||||
</core-loading>
|
||||
</ion-content>
|
||||
|
||||
<!-- Template to render a policy in a tablet device. -->
|
||||
<ng-template #policyTabletTemplate let-policy="policy" let-isPreviousVersion="isPreviousVersion">
|
||||
<tr [class.core-policy-previous-version]="isPreviousVersion">
|
||||
<td class="core-policy-title">
|
||||
<ion-icon *ngIf="isPreviousVersion" name="fam-arrow-turn-down-right" aria-hidden="true" />
|
||||
<ion-icon *ngIf="!isPreviousVersion && policy.previousVersions.length" name="fas-chevron-right" flip-rtl
|
||||
(ariaButtonClick)="toggle($event, policy)" class="expandable-status-icon"
|
||||
[class.expandable-status-icon-expanded]="policy.expanded" [attr.aria-expanded]="policy.expanded"
|
||||
[attr.aria-label]="(policy.expanded ? 'core.collapse' : 'core.expand') | translate" />
|
||||
<ion-icon *ngIf="!isPreviousVersion && !policy.previousVersions.length" class="core-policy-icon-placeholder"
|
||||
aria-hidden="true" />
|
||||
|
||||
<a href="#" (click)="viewFullPolicy($event, policy)">{{ policy.name }}</a>
|
||||
</td>
|
||||
|
||||
<td class="core-policy-revision">
|
||||
<p>{{ policy.revision }}</p>
|
||||
<ion-badge color="success" *ngIf="policy.status === activeStatus">
|
||||
{{ 'core.policy.status1' | translate }}
|
||||
</ion-badge>
|
||||
<ion-badge color="info" *ngIf="policy.optional">
|
||||
{{ 'core.policy.policydocoptionalyes' | translate }}
|
||||
</ion-badge>
|
||||
</td>
|
||||
|
||||
<td class="core-policy-user-agreement">
|
||||
<p class="core-policy-user-agreement-info">
|
||||
<ng-container *ngIf="policy.hasAccepted">
|
||||
<ion-icon name="fas-check" color="success" aria-hidden="true" />
|
||||
|
||||
<span *ngIf="policy.onBehalf" class="core-policy-user-agreement-status">
|
||||
{{ 'core.policy.acceptancestatusacceptedbehalf' | translate }}
|
||||
</span>
|
||||
<span *ngIf="!policy.onBehalf" class="core-policy-user-agreement-status">
|
||||
{{ 'core.policy.acceptancestatusaccepted' | translate }}
|
||||
</span>
|
||||
|
||||
<span class="core-policy-user-agreement-actions" *ngIf="policy.canrevoke">
|
||||
<ion-button fill="none" (click)="setAcceptance($event, policy, false)"
|
||||
[attr.aria-label]="'core.policy.useracceptanceactionrevokeone' | translate:{$a: policy.name}">
|
||||
{{ 'core.policy.useracceptanceactionrevoke' | translate }}
|
||||
</ion-button>
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="policy.hasDeclined">
|
||||
<ion-icon name="fas-xmark" color="danger" aria-hidden="true" />
|
||||
|
||||
<span *ngIf="policy.onBehalf" class="core-policy-user-agreement-status">
|
||||
{{ 'core.policy.acceptancestatusdeclinedbehalf' | translate }}
|
||||
</span>
|
||||
<span *ngIf="!policy.onBehalf" class="core-policy-user-agreement-status">
|
||||
{{ 'core.policy.acceptancestatusdeclined' | translate }}
|
||||
</span>
|
||||
|
||||
<span class="core-policy-user-agreement-actions" *ngIf="policy.canaccept">
|
||||
<ion-button fill="none" (click)="setAcceptance($event, policy, true)"
|
||||
[attr.aria-label]="'core.policy.useracceptanceactionacceptone' | translate:{$a: policy.name}">
|
||||
{{ 'core.policy.useracceptanceactionaccept' | translate }}
|
||||
</ion-button>
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!policy.hasAccepted && !policy.hasDeclined">
|
||||
<ion-icon name="fas-clock" color="warning" aria-hidden="true" />
|
||||
|
||||
<span class="core-policy-user-agreement-status">{{ 'core.policy.acceptancestatuspending' | translate }}</span>
|
||||
|
||||
<span class="core-policy-user-agreement-actions" *ngIf="policy.canaccept">
|
||||
<ion-button fill="none" (click)="setAcceptance($event, policy, true)"
|
||||
[attr.aria-label]="'core.policy.useracceptanceactionacceptone' | translate:{$a: policy.name}">
|
||||
{{ 'core.policy.useracceptanceactionaccept' | translate }}
|
||||
</ion-button>
|
||||
<ion-button fill="none" (click)="setAcceptance($event, policy, false)"
|
||||
[attr.aria-label]="'core.policy.useracceptanceactiondeclineone' | translate:{$a: policy.name}">
|
||||
{{ 'core.policy.useracceptanceactiondecline' | translate }}
|
||||
</ion-button>
|
||||
</span>
|
||||
</ng-container>
|
||||
</p>
|
||||
</td>
|
||||
|
||||
<td class="core-policy-responseon">
|
||||
<p *ngIf="policy.acceptance">
|
||||
{{ policy.acceptance.timemodified * 1000 | coreFormatDate:'strftimedatetime' }}
|
||||
</p>
|
||||
<p *ngIf="!policy.acceptance">-</p>
|
||||
</td>
|
||||
|
||||
<ng-container *ngIf="hasOnBehalf">
|
||||
<td class="core-policy-responseby">
|
||||
<p *ngIf="policy.onBehalf">
|
||||
<a href="#" core-user-link [userId]="policy.acceptance.usermodified">{{ policy.acceptance.modfullname }}</a>
|
||||
</p>
|
||||
<p *ngIf="!policy.onBehalf">-</p>
|
||||
</td>
|
||||
|
||||
<td class="core-policy-acceptance-note">
|
||||
<p *ngIf="policy.acceptance?.note">
|
||||
<core-format-text [text]="policy.acceptance.note" contextLevel="system" [contextInstanceId]="0" />
|
||||
</p>
|
||||
<p *ngIf="!policy.acceptance?.note">-</p>
|
||||
</td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
<ng-container *ngIf="!isPreviousVersion && policy.previousVersions.length && policy.expanded">
|
||||
<ng-container *ngFor="let policy of policy.previousVersions">
|
||||
<ng-container *ngTemplateOutlet="policyTabletTemplate; context: {policy: policy, isPreviousVersion: true }" />
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<!-- Template to render a policy in a mobile device. -->
|
||||
<ng-template #policyMobileTemplate let-policy="policy" let-isPreviousVersion="isPreviousVersion">
|
||||
<ion-item class="ion-text-wrap core-policy-title">
|
||||
<div slot="start">
|
||||
<ion-icon *ngIf="isPreviousVersion" name="fam-arrow-turn-down-right" aria-hidden="true" />
|
||||
<ion-icon name="fas-chevron-right" flip-rtl (ariaButtonClick)="toggle($event, policy)" class="expandable-status-icon"
|
||||
[class.expandable-status-icon-expanded]="policy.expanded" [attr.aria-expanded]="policy.expanded"
|
||||
[attr.aria-label]="(policy.expanded ? 'core.collapse' : 'core.expand') | translate"
|
||||
[attr.aria-controls]="'core-policy-details-' + policy.versionid" />
|
||||
</div>
|
||||
<ion-label>
|
||||
<p *ngIf="isPreviousVersion">{{ policy.revision }}</p>
|
||||
<p *ngIf="!isPreviousVersion">{{ policy.name }}</p>
|
||||
</ion-label>
|
||||
<ion-button fill="clear" (click)="viewFullPolicy($event, policy)"
|
||||
[attr.aria-label]="'core.policy.viewpolicy' | translate:{policyname: policy.name}">
|
||||
<ion-icon slot="icon-only" name="fas-eye" aria-hidden="true" />
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<div [hidden]="!policy.expanded" id="core-policy-details-{{policy.versionid}}" class="core-policy-details">
|
||||
<ion-item class="ion-text-wrap core-policy-revision" lines="full">
|
||||
<ion-label>
|
||||
<ng-container *ngIf="isPreviousVersion">
|
||||
<p class="item-heading">{{ 'core.policy.policydocname' | translate }}</p>
|
||||
<p>{{ policy.name }}</p>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!isPreviousVersion">
|
||||
<p class="item-heading">{{ 'core.policy.policydocrevision' | translate }}</p>
|
||||
<p>{{ policy.revision }}</p>
|
||||
</ng-container>
|
||||
<ion-badge color="success" *ngIf="policy.status === activeStatus">
|
||||
{{ 'core.policy.status1' | translate }}
|
||||
</ion-badge>
|
||||
<ion-badge color="info" *ngIf="policy.optional">
|
||||
{{ 'core.policy.policydocoptionalyes' | translate }}
|
||||
</ion-badge>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap core-policy-user-agreement" lines="full"
|
||||
[class.core-policy-agreement-has-actions]="policy.hasActions">
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.policy.response' | translate }}</p>
|
||||
<p class="core-policy-user-agreement-info">
|
||||
<ng-container *ngIf="policy.hasAccepted">
|
||||
<ion-icon name="fas-check" color="success" aria-hidden="true" />
|
||||
|
||||
<span *ngIf="policy.onBehalf" class="core-policy-user-agreement-status">
|
||||
{{ 'core.policy.acceptancestatusacceptedbehalf' | translate }}
|
||||
</span>
|
||||
<span *ngIf="!policy.onBehalf" class="core-policy-user-agreement-status">
|
||||
{{ 'core.policy.acceptancestatusaccepted' | translate }}
|
||||
</span>
|
||||
|
||||
<span class="core-policy-user-agreement-actions" *ngIf="policy.canrevoke">
|
||||
<ion-button fill="none" (click)="setAcceptance($event, policy, false)"
|
||||
[attr.aria-label]="'core.policy.useracceptanceactionrevokeone' | translate:{$a: policy.name}">
|
||||
{{ 'core.policy.useracceptanceactionrevoke' | translate }}
|
||||
</ion-button>
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="policy.hasDeclined">
|
||||
<ion-icon name="fas-xmark" color="danger" aria-hidden="true" />
|
||||
|
||||
<span *ngIf="policy.onBehalf" class="core-policy-user-agreement-status">
|
||||
{{ 'core.policy.acceptancestatusdeclinedbehalf' | translate }}
|
||||
</span>
|
||||
<span *ngIf="!policy.onBehalf" class="core-policy-user-agreement-status">
|
||||
{{ 'core.policy.acceptancestatusdeclined' | translate }}
|
||||
</span>
|
||||
|
||||
<span class="core-policy-user-agreement-actions" *ngIf="policy.canaccept">
|
||||
<ion-button fill="none" (click)="setAcceptance($event, policy, true)"
|
||||
[attr.aria-label]="'core.policy.useracceptanceactionacceptone' | translate:{$a: policy.name}">
|
||||
{{ 'core.policy.useracceptanceactionaccept' | translate }}
|
||||
</ion-button>
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!policy.hasAccepted && !policy.hasDeclined">
|
||||
<ion-icon name="fas-clock" color="warning" aria-hidden="true" />
|
||||
|
||||
<span class="core-policy-user-agreement-status">{{ 'core.policy.acceptancestatuspending' | translate }}</span>
|
||||
|
||||
<span class="core-policy-user-agreement-actions" *ngIf="policy.canaccept">
|
||||
<ion-button fill="none" (click)="setAcceptance($event, policy, true)"
|
||||
[attr.aria-label]="'core.policy.useracceptanceactionacceptone' | translate:{$a: policy.name}">
|
||||
{{ 'core.policy.useracceptanceactionaccept' | translate }}
|
||||
</ion-button>
|
||||
<ion-button fill="none" (click)="setAcceptance($event, policy, false)"
|
||||
[attr.aria-label]="'core.policy.useracceptanceactiondeclineone' | translate:{$a: policy.name}">
|
||||
{{ 'core.policy.useracceptanceactiondecline' | translate }}
|
||||
</ion-button>
|
||||
</span>
|
||||
</ng-container>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap core-policy-responseon" lines="full">
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.policy.responseon' | translate }}</p>
|
||||
<p *ngIf="policy.acceptance">
|
||||
{{ policy.acceptance.timemodified * 1000 | coreFormatDate:'strftimedatetime' }}
|
||||
</p>
|
||||
<p *ngIf="!policy.acceptance">-</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngIf="policy.onBehalf">
|
||||
<ion-item button class="ion-text-wrap core-policy-responseby" core-user-link [userId]="policy.acceptance.usermodified"
|
||||
lines="full" detail="false">
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.policy.responseby' | translate }}</p>
|
||||
<p class="core-policy-responseby-name">{{ policy.acceptance.modfullname }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap core-policy-acceptance-note" lines="full">
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.policy.acceptancenote' | translate }}</p>
|
||||
<p *ngIf="policy.acceptance.note">
|
||||
<core-format-text [text]="policy.acceptance.note" contextLevel="system" [contextInstanceId]="0" />
|
||||
</p>
|
||||
<p *ngIf="!policy.acceptance.note">-</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="!isPreviousVersion && policy.previousVersions.length" class="core-policy-previous-versions">
|
||||
<ng-container *ngFor="let policy of policy.previousVersions">
|
||||
<ng-container *ngTemplateOutlet="policyMobileTemplate; context: {policy: policy, isPreviousVersion: true}" />
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
|
@ -0,0 +1,129 @@
|
|||
@use "theme/globals" as *;
|
||||
|
||||
:host {
|
||||
.core-policy-revision ion-badge {
|
||||
@include margin-horizontal(0px, 4px);
|
||||
}
|
||||
|
||||
.core-policy-responseby-name {
|
||||
color: var(--core-link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.core-policy-user-agreement-info {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
ion-icon {
|
||||
@include margin-horizontal(0px, 8px);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
ion-button {
|
||||
--background: transparent;
|
||||
color: var(--ion-color-primary);
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
text-transform: none;
|
||||
|
||||
&:first-child:not(:last-child) {
|
||||
@include margin-horizontal(null, 0px);
|
||||
}
|
||||
&:not(:first-child) {
|
||||
@include margin-horizontal(0px, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.core-policy-mobile-container {
|
||||
.core-policy-title div[slot="start"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0px;
|
||||
|
||||
ion-icon {
|
||||
font-size: 1.125rem;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.core-policy-details {
|
||||
.item:not(.core-policy-title) {
|
||||
--background: var(--gray-100);
|
||||
}
|
||||
|
||||
.core-policy-user-agreement.core-policy-agreement-has-actions {
|
||||
ion-label {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.core-policy-user-agreement-status {
|
||||
line-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.core-policy-user-agreement-info {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
|
||||
.core-policy-user-agreement-status {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.core-policy-tablet-container {
|
||||
th {
|
||||
&:first-child {
|
||||
@include padding-horizontal(24px, 0px);
|
||||
}
|
||||
}
|
||||
|
||||
th, td {
|
||||
min-width: 200px;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.core-policy-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
ion-icon {
|
||||
font-size: 1.125rem;
|
||||
padding: 13px;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.core-policy-icon-placeholder {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.core-policy-revision p {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.core-policy-mobile-container .core-policy-title .expandable-status-icon,
|
||||
.core-policy-tablet-container .core-policy-title ion-icon {
|
||||
min-height: auto;
|
||||
min-width: auto;
|
||||
font-size: 1.125rem;
|
||||
padding: 13px;
|
||||
}
|
||||
|
||||
.core-policy-title .expandable-status-icon {
|
||||
border-radius: 50%;
|
||||
&:hover {
|
||||
background: var(--secondary);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,284 @@
|
|||
// (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, OnDestroy, OnInit } from '@angular/core';
|
||||
|
||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
|
||||
import { Translate } from '@singletons';
|
||||
import { CorePolicy, CorePolicySitePolicy, CorePolicyStatus } from '@features/policy/services/policy';
|
||||
import { CorePolicyViewPolicyModalComponent } from '@features/policy/components/policy-modal/policy-modal';
|
||||
import { CoreTime } from '@singletons/time';
|
||||
import { CoreScreen } from '@services/screen';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { CORE_DATAPRIVACY_PAGE_NAME } from '@features/dataprivacy/constants';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreDataPrivacy } from '@features/dataprivacy/services/dataprivacy';
|
||||
|
||||
/**
|
||||
* Page to view user acceptances.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-policy-acceptances',
|
||||
templateUrl: 'acceptances.html',
|
||||
styleUrls: ['acceptances.scss'],
|
||||
})
|
||||
export class CorePolicyAcceptancesPage implements OnInit, OnDestroy {
|
||||
|
||||
dataLoaded = false;
|
||||
policies: ActiveSitePolicy[] = [];
|
||||
activeStatus = CorePolicyStatus.Active;
|
||||
isTablet = false;
|
||||
hasOnBehalf = false;
|
||||
canContactDPO = false;
|
||||
|
||||
protected logView: () => void;
|
||||
protected layoutSubscription?: Subscription;
|
||||
|
||||
constructor() {
|
||||
this.logView = CoreTime.once(() => {
|
||||
const currentUserId = CoreSites.getCurrentSiteUserId();
|
||||
|
||||
CoreAnalytics.logEvent({
|
||||
type: CoreAnalyticsEventType.VIEW_ITEM_LIST,
|
||||
ws: 'tool_policy_get_user_acceptances',
|
||||
name: Translate.instant('core.policy.policiesagreements'),
|
||||
data: { userid: currentUserId },
|
||||
url: `/admin/tool/policy/user.php?userid=${currentUserId}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.isTablet = CoreScreen.isTablet;
|
||||
this.layoutSubscription = CoreScreen.layoutObservable.subscribe(() => {
|
||||
this.isTablet = CoreScreen.isTablet;
|
||||
});
|
||||
|
||||
this.fetchCanContactDPO();
|
||||
this.fetchAcceptances().finally(() => {
|
||||
this.dataLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can contact DPO.
|
||||
*/
|
||||
protected async fetchCanContactDPO(): Promise<void> {
|
||||
this.canContactDPO = await CoreUtils.ignoreErrors(CoreDataPrivacy.isEnabled(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the policies and acceptances.
|
||||
*
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
protected async fetchAcceptances(): Promise<void> {
|
||||
try {
|
||||
const allPolicies = await CorePolicy.getUserAcceptances();
|
||||
|
||||
this.hasOnBehalf = false;
|
||||
|
||||
const policiesById = allPolicies.reduce((groupedPolicies, policy) => {
|
||||
const formattedPolicy = this.formatSitePolicy(policy);
|
||||
this.hasOnBehalf = this.hasOnBehalf || formattedPolicy.onBehalf;
|
||||
|
||||
groupedPolicies[policy.policyid] = groupedPolicies[policy.policyid] || [];
|
||||
groupedPolicies[policy.policyid].push(formattedPolicy);
|
||||
|
||||
return groupedPolicies;
|
||||
}, <Record<number, SitePolicy[]>> {});
|
||||
|
||||
this.policies = [];
|
||||
for (const policyId in policiesById) {
|
||||
const policyVersions = policiesById[policyId];
|
||||
|
||||
let activePolicy: ActiveSitePolicy | undefined =
|
||||
policyVersions.find((policy) => policy.status === CorePolicyStatus.Active);
|
||||
if (!activePolicy) {
|
||||
// No active policy, it shouldn't happen. Use the one with highest versionid.
|
||||
policyVersions.sort((a, b) => b.versionid - a.versionid);
|
||||
activePolicy = policyVersions[0];
|
||||
}
|
||||
|
||||
activePolicy.previousVersions = policyVersions.filter(policy => policy !== activePolicy);
|
||||
this.policies.push(activePolicy);
|
||||
}
|
||||
|
||||
this.logView();
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error getting policies.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a site policy, adding some calculated data.
|
||||
*
|
||||
* @param policy Policy to format.
|
||||
* @param expanded Whether the policy should be expanded or not.
|
||||
* @returns Formatted policy.
|
||||
*/
|
||||
protected formatSitePolicy(policy: CorePolicySitePolicy, expanded = false): SitePolicy {
|
||||
const hasAccepted = policy.acceptance?.status === 1;
|
||||
const hasDeclined = policy.acceptance?.status === 0;
|
||||
const onBehalf = !!policy.acceptance && policy.acceptance.usermodified !== CoreSites.getCurrentSiteUserId();
|
||||
|
||||
return {
|
||||
...policy,
|
||||
expanded,
|
||||
hasAccepted,
|
||||
hasDeclined,
|
||||
onBehalf,
|
||||
hasActions: hasDeclined || !hasAccepted || !!policy.optional,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the data.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
*/
|
||||
async refreshAcceptances(refresher?: HTMLIonRefresherElement): Promise<void> {
|
||||
await CoreUtils.ignoreErrors(CorePolicy.invalidateAcceptances());
|
||||
|
||||
await CoreUtils.ignoreErrors(this.fetchAcceptances());
|
||||
|
||||
refresher?.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toogle the visibility of a policy (expand/collapse).
|
||||
*
|
||||
* @param event Event.
|
||||
* @param policy Policy.
|
||||
*/
|
||||
toggle(event: Event, policy: SitePolicy): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
policy.expanded = !policy.expanded;
|
||||
}
|
||||
|
||||
/**
|
||||
* View the full policy.
|
||||
*
|
||||
* @param event Event.
|
||||
* @param policy Policy.
|
||||
*/
|
||||
viewFullPolicy(event: Event, policy: CorePolicySitePolicy): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
CoreDomUtils.openModal({
|
||||
component: CorePolicyViewPolicyModalComponent,
|
||||
componentProps: { policy },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the acceptance of a policy.
|
||||
*
|
||||
* @param event Event.
|
||||
* @param policy Policy
|
||||
* @param accept Whether to accept or not.
|
||||
*/
|
||||
async setAcceptance(event: Event, policy: SitePolicy, accept: boolean): Promise<void> {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
|
||||
|
||||
try {
|
||||
await CorePolicy.setUserAcceptances({ [policy.versionid]: accept ? 1 : 0 });
|
||||
|
||||
await this.updatePolicyAcceptance(policy, accept);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error changing policy status.');
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the acceptance data for a certain policy.
|
||||
*
|
||||
* @param policy Policy to update.
|
||||
* @param accepted Whether the policy has just been accepted or declined.
|
||||
*/
|
||||
protected async updatePolicyAcceptance(policy: SitePolicy, accepted: boolean): Promise<void> {
|
||||
try {
|
||||
const policies = await CorePolicy.getUserAcceptances({ readingStrategy: CoreSitesReadingStrategy.ONLY_NETWORK });
|
||||
|
||||
const newPolicy = policies.find((p) => p.versionid === policy.versionid);
|
||||
|
||||
if (!newPolicy) {
|
||||
throw new Error('Policy not found.');
|
||||
}
|
||||
|
||||
policy.acceptance = newPolicy.acceptance;
|
||||
} catch (error) {
|
||||
// Error updating the acceptance, calculate it in the app.
|
||||
policy.acceptance = {
|
||||
status: accepted ? 1 : 0,
|
||||
lang: policy.acceptance?.lang ?? 'en',
|
||||
timemodified: Date.now(),
|
||||
usermodified: CoreSites.getCurrentSiteUserId(),
|
||||
};
|
||||
}
|
||||
|
||||
Object.assign(policy, this.formatSitePolicy(policy, policy.expanded));
|
||||
}
|
||||
|
||||
/**
|
||||
* Open page to contact DPO.
|
||||
*
|
||||
* @param event Event.
|
||||
*/
|
||||
openContactDPO(event: Event): void {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
CoreNavigator.navigateToSitePath(CORE_DATAPRIVACY_PAGE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.layoutSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Site policy with some calculated data.
|
||||
*/
|
||||
type SitePolicy = CorePolicySitePolicy & {
|
||||
expanded: boolean;
|
||||
hasAccepted: boolean;
|
||||
hasDeclined: boolean;
|
||||
onBehalf: boolean;
|
||||
hasActions: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Active site policy with some calculated data.
|
||||
*/
|
||||
type ActiveSitePolicy = SitePolicy & {
|
||||
previousVersions?: SitePolicy[];
|
||||
};
|
|
@ -73,7 +73,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.item ::ng-deep ion-label p span {
|
||||
.item ::ng-deep ion-label .core-site-policy-full-policy-link {
|
||||
color: var(--core-link-color);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,14 +17,19 @@ 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 { ACCEPTANCES_PAGE_NAME, SITE_POLICY_PAGE_NAME } from './constants';
|
||||
import { CorePolicyComponentsModule } from './components/components.module';
|
||||
import { CorePolicyAcceptancesPage } from './pages/acceptances/acceptances';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: SITE_POLICY_PAGE_NAME,
|
||||
component: CorePolicySitePolicyPage,
|
||||
},
|
||||
{
|
||||
path: ACCEPTANCES_PAGE_NAME,
|
||||
component: CorePolicyAcceptancesPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -35,6 +40,7 @@ const routes: Routes = [
|
|||
],
|
||||
declarations: [
|
||||
CorePolicySitePolicyPage,
|
||||
CorePolicyAcceptancesPage,
|
||||
],
|
||||
})
|
||||
export class CorePolicyLazyModule {}
|
||||
|
|
|
@ -18,6 +18,11 @@ import { Routes } from '@angular/router';
|
|||
import { AppRoutingModule } from '@/app/app-routing.module';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { POLICY_PAGE_NAME } from './constants';
|
||||
import { CoreUserDelegate } from '@features/user/services/user-delegate';
|
||||
import { CorePolicyUserHandler } from './services/handlers/user';
|
||||
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||
import { CorePolicyAcceptancesLinkHandler } from './services/handlers/acceptances-link';
|
||||
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
@ -29,12 +34,16 @@ const routes: Routes = [
|
|||
@NgModule({
|
||||
imports: [
|
||||
AppRoutingModule.forChild(routes),
|
||||
CoreMainMenuTabRoutingModule.forChild(routes),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
useValue: async () => {
|
||||
CoreUserDelegate.registerHandler(CorePolicyUserHandler.instance);
|
||||
CoreContentLinksDelegate.registerHandler(CorePolicyAcceptancesLinkHandler.instance);
|
||||
|
||||
CoreEvents.on(CoreEvents.SITE_POLICY_NOT_AGREED, async (data) => {
|
||||
const { CorePolicy } = await import('@features/policy/services/policy');
|
||||
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
// (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 { Injectable } from '@angular/core';
|
||||
|
||||
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
|
||||
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CorePolicy } from '../policy';
|
||||
import { ACCEPTANCES_PAGE_NAME, POLICY_PAGE_NAME } from '@features/policy/constants';
|
||||
import { CoreSites } from '@services/sites';
|
||||
|
||||
/**
|
||||
* Handler to treat links to policy acceptances page.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CorePolicyAcceptancesLinkHandlerService extends CoreContentLinksHandlerBase {
|
||||
|
||||
name = 'CorePolicyAcceptancesLinkHandler';
|
||||
pattern = /\/admin\/tool\/policy\/user\.php/;
|
||||
featureName = 'CoreUserDelegate_CorePolicy';
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getActions(): CoreContentLinksAction[] {
|
||||
return [{
|
||||
action: async (siteId: string): Promise<void> => {
|
||||
await CoreNavigator.navigateToSitePath(`/${POLICY_PAGE_NAME}/${ACCEPTANCES_PAGE_NAME}`, { siteId });
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(siteId: string, url: string, params: Record<string, string>): Promise<boolean> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
const userId = Number(params.userid);
|
||||
|
||||
if (userId && userId !== site.getUserId()) {
|
||||
// Only viewing your own policies is supported.
|
||||
return false;
|
||||
}
|
||||
|
||||
return CorePolicy.isManageAcceptancesAvailable(siteId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const CorePolicyAcceptancesLinkHandler = makeSingleton(CorePolicyAcceptancesLinkHandlerService);
|
|
@ -0,0 +1,85 @@
|
|||
// (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 { Injectable } from '@angular/core';
|
||||
import {
|
||||
CoreUserDelegateContext,
|
||||
CoreUserProfileHandlerType,
|
||||
CoreUserProfileHandler,
|
||||
CoreUserProfileHandlerData,
|
||||
} from '@features/user/services/user-delegate';
|
||||
import { CorePolicy } from '../policy';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreUserProfile } from '@features/user/services/user';
|
||||
import { ACCEPTANCES_PAGE_NAME, POLICY_PAGE_NAME } from '@features/policy/constants';
|
||||
|
||||
/**
|
||||
* Handler to inject an option into user menu.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CorePolicyUserHandlerService implements CoreUserProfileHandler {
|
||||
|
||||
type = CoreUserProfileHandlerType.LIST_ACCOUNT_ITEM;
|
||||
name = 'CorePolicy';
|
||||
priority = 50;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabled(): Promise<boolean> {
|
||||
const wsAvailable = await CorePolicy.isManageAcceptancesAvailable();
|
||||
if (!wsAvailable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const policyHandler = await CoreSites.getCurrentSite()?.getConfig('sitepolicyhandler');
|
||||
|
||||
return policyHandler === 'tool_policy';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabledForContext(context: CoreUserDelegateContext): Promise<boolean> {
|
||||
return context === CoreUserDelegateContext.USER_MENU;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabledForUser(user: CoreUserProfile): Promise<boolean> {
|
||||
return user.id == CoreSites.getCurrentSiteUserId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getDisplayData(): CoreUserProfileHandlerData {
|
||||
return {
|
||||
icon: 'fas-file-shield',
|
||||
title: 'core.policy.policiesagreements',
|
||||
class: 'core-policy-user-handler',
|
||||
action: (event): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
CoreNavigator.navigateToSitePath(`/${POLICY_PAGE_NAME}/${ACCEPTANCES_PAGE_NAME}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const CorePolicyUserHandler = makeSingleton(CorePolicyUserHandlerService);
|
|
@ -127,14 +127,15 @@ export class CorePolicyService {
|
|||
* @param options Options
|
||||
* @returns List of policies with their acceptances.
|
||||
*/
|
||||
async getUserAcceptances(options: CoreSitesCommonWSOptions = {}): Promise<CorePolicySitePolicy[]> {
|
||||
async getUserAcceptances(options: CorePolicyGetAcceptancesOptions = {}): Promise<CorePolicySitePolicy[]> {
|
||||
const site = await CoreSites.getSite(options.siteId);
|
||||
|
||||
const userId = options.userId || site.getUserId();
|
||||
const data: CorePolicyGetUserAcceptancesWSParams = {
|
||||
userid: site.getUserId(),
|
||||
userid: userId,
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getUserAcceptancesCacheKey(site.getUserId()),
|
||||
cacheKey: this.getUserAcceptancesCacheKey(userId),
|
||||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
...CoreSites.getReadingStrategyPreSets(options.readingStrategy),
|
||||
};
|
||||
|
@ -179,6 +180,18 @@ export class CorePolicyService {
|
|||
CoreNavigator.navigate(routePath, { params: { siteId }, reset: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate acceptances WS call.
|
||||
*
|
||||
* @param options Options.
|
||||
* @returns Promise resolved when data is invalidated.
|
||||
*/
|
||||
async invalidateAcceptances(options: {userId?: number; siteId?: string} = {}): Promise<void> {
|
||||
const site = await CoreSites.getSite(options.siteId);
|
||||
|
||||
await site.invalidateWsCacheForKey(this.getUserAcceptancesCacheKey(options.userId || site.getUserId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a site allows getting and setting acceptances.
|
||||
*
|
||||
|
@ -221,6 +234,13 @@ export class CorePolicyService {
|
|||
|
||||
export const CorePolicy = makeSingleton(CorePolicyService);
|
||||
|
||||
/**
|
||||
* Options for get policy acceptances.
|
||||
*/
|
||||
type CorePolicyGetAcceptancesOptions = CoreSitesCommonWSOptions & {
|
||||
userId?: number; // User ID. If not defined, current user.
|
||||
};
|
||||
|
||||
/**
|
||||
* Result of WS core_user_agree_site_policy.
|
||||
*/
|
||||
|
@ -260,6 +280,9 @@ export type CorePolicySitePolicy = {
|
|||
content?: string; // The policy content.
|
||||
contentformat: number; // Content format (1 = HTML, 0 = MOODLE, 2 = PLAIN, or 4 = MARKDOWN).
|
||||
acceptance?: CorePolicySitePolicyAcceptance; // Acceptance status for the given user.
|
||||
canaccept: boolean; // Whether the policy can be accepted.
|
||||
candecline: boolean; // Whether the policy can be declined.
|
||||
canrevoke: boolean; // Whether the policy can be revoked.
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -300,3 +323,12 @@ 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.
|
||||
}
|
||||
|
||||
/**
|
||||
* Status of a policy.
|
||||
*/
|
||||
export enum CorePolicyStatus {
|
||||
Draft = 0,
|
||||
Active = 1,
|
||||
Archived = 2,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
@core_policy @app @javascript @lms_from4.4
|
||||
Feature: Test accepting pending policies on signup
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student | User | One | one@example.com |
|
||||
|
||||
Scenario: Accept policy using default handler
|
||||
Given the following config values are set as admin:
|
||||
| sitepolicyhandler | |
|
||||
| sitepolicy | https://moodle.org/invalidfile.pdf |
|
||||
When I launch the app
|
||||
And I set the field "Your site" to "$WWWROOT" in the app
|
||||
And I press "Connect to your site" in the app
|
||||
And I set the following fields to these values in the app:
|
||||
| Username | student |
|
||||
| Password | student |
|
||||
And I press "Log in" near "Lost password?" in the app
|
||||
Then I should find "You must agree to this policy to continue using this site" in the app
|
||||
But I should not be able to press "Continue" in the app
|
||||
And I should not be able to press "User account" in the app
|
||||
|
||||
When I press "Link to site policy agreement" in the app
|
||||
And I press "OK" in the app
|
||||
Then the app should have opened a browser tab with url "moodle.org"
|
||||
|
||||
When I close the browser tab opened by the app
|
||||
And I press "I have read and agree to the Site policy agreement" in the app
|
||||
And I press "Continue" in the app
|
||||
Then I should be able to press "User account" in the app
|
||||
|
||||
When I press "User account" in the app
|
||||
Then I should not find "Policies and agreements" in the app
|
||||
|
||||
Scenario: Accept policy using tool_policy
|
||||
Given the following config values are set as admin:
|
||||
| sitepolicyhandler | tool_policy |
|
||||
And the following policies exist:
|
||||
| name | agreementstyle | optional | revision | content | summary |
|
||||
| Mandatory policy own page | 1 | 0 | mo v1 | Content mand own page | Summ mand own page |
|
||||
| Optional policy own page | 1 | 1 | oo v1 | Content opt own page | Summ opt own page |
|
||||
| Mandatory policy consent page | 0 | 0 | mc v1 | Content mand consent page | Summ mand consent page |
|
||||
| Optional policy consent page | 0 | 1 | oc v1 | Content opt consent page | Summ opt consent page |
|
||||
When I launch the app
|
||||
And I set the field "Your site" to "$WWWROOT" in the app
|
||||
And I press "Connect to your site" in the app
|
||||
And I set the following fields to these values in the app:
|
||||
| Username | student |
|
||||
| Password | student |
|
||||
And I press "Log in" near "Lost password?" in the app
|
||||
Then I should find "Mandatory policy own page" in the app
|
||||
And I should find "Summ mand own page" in the app
|
||||
And I should find "Content mand own page" in the app
|
||||
But I should not be able to press "Continue" in the app
|
||||
And I should not be able to press "User account" in the app
|
||||
|
||||
When I press "I have read and agree to the Mandatory policy own page" in the app
|
||||
And I press "I have read and agree to the Mandatory policy own page" in the app
|
||||
Then I should find "Before continuing you need to acknowledge all these policies" in the app
|
||||
But I should not be able to press "Continue" in the app
|
||||
|
||||
When I press "I have read and agree to the Mandatory policy own page" in the app
|
||||
And I press "Continue" in the app
|
||||
Then I should find "Optional policy own page" in the app
|
||||
And I should find "Summ opt own page" in the app
|
||||
And I should find "Content opt own page" in the app
|
||||
But I should not be able to press "Continue" in the app
|
||||
|
||||
When I press "No thanks, I decline Optional policy own page" in the app
|
||||
And I press "Continue" in the app
|
||||
Then I should find "Policy 1 out of 2" in the app
|
||||
And I should find "Mandatory policy consent page" in the app
|
||||
And I should find "Summ mand consent page" in the app
|
||||
And I should find "Content mand consent page" in the app
|
||||
But I should not find "I have read and agree" in the app
|
||||
|
||||
When I press "Next" in the app
|
||||
Then I should find "Policy 2 out of 2" in the app
|
||||
And I should find "Optional policy consent page" in the app
|
||||
And I should find "Summ opt consent page" in the app
|
||||
And I should find "Content opt consent page" in the app
|
||||
But I should not find "No thanks, I decline" in the app
|
||||
|
||||
When I press "Next" in the app
|
||||
Then I should find "Please agree to the following policies" in the app
|
||||
And I should find "Mandatory policy consent page" in the app
|
||||
And I should find "Summ mand consent page" in the app
|
||||
And I should find "Optional policy consent page" in the app
|
||||
And I should find "Summ opt consent page" in the app
|
||||
But I should not find "Content mand consent page" in the app
|
||||
And I should not find "Content opt consent page" in the app
|
||||
|
||||
When I press "Please refer to the full Mandatory policy consent page" in the app
|
||||
Then I should find "Content mand consent page" in the app
|
||||
But I should not find "Content opt consent page" in the app
|
||||
|
||||
When I press "Close" in the app
|
||||
And I press "Please refer to the full Optional policy consent page" in the app
|
||||
Then I should find "Content opt consent page" in the app
|
||||
But I should not find "Content mand consent page" in the app
|
||||
|
||||
When I press "Close" in the app
|
||||
And I press "Continue" in the app
|
||||
Then I should find "Before continuing you need to acknowledge all these policies" in the app
|
||||
|
||||
When I press "I have read and agree to the Mandatory policy consent page" in the app
|
||||
And I press "Continue" in the app
|
||||
Then I should find "Before continuing you need to acknowledge all these policies" in the app
|
||||
|
||||
When I press "I have read and agree to the Optional policy consent page" in the app
|
||||
And I press "Continue" in the app
|
||||
Then I should be able to press "User account" in the app
|
||||
|
||||
# TODO: Add a new version for a policy and check that the user is prompted to accept it.
|
||||
# This is currently not possible with the current step to create policies.
|
||||
|
||||
# View policies and agreements. Do it in this Scenario because there is no generator to set acceptances.
|
||||
When I press "User account" in the app
|
||||
And I press "Policies and agreements" in the app
|
||||
Then I should find "Mandatory policy own page" in the app
|
||||
And I should find "Optional policy own page" in the app
|
||||
And I should find "Mandatory policy consent page" in the app
|
||||
And I should find "Optional policy consent page" in the app
|
||||
But I should not find "Revision" in the app
|
||||
And I should not find "Summ mand own page" in the app
|
||||
And I should not find "Content mand own page" in the app
|
||||
|
||||
When I press "View policy Mandatory policy own page" in the app
|
||||
Then I should find "Summ mand own page" in the app
|
||||
And I should find "Content mand own page" in the app
|
||||
But I should not find "Summ opt own page" in the app
|
||||
|
||||
When I press "Close" in the app
|
||||
And I press "Expand" within "Mandatory policy own page" "ion-item" in the app
|
||||
Then I should find "mo v1" in the app
|
||||
And I should find "Active" in the app
|
||||
And I should find "Accepted" in the app
|
||||
But I should not be able to press "Withdraw" in the app
|
||||
|
||||
When I press "Collapse" within "Mandatory policy own page" "ion-item" in the app
|
||||
And I press "Expand" within "Optional policy own page" "ion-item" in the app
|
||||
Then I should find "oo v1" in the app
|
||||
And I should find "Active" in the app
|
||||
And I should find "Declined" in the app
|
||||
|
||||
When I press "Accept" in the app
|
||||
Then I should find "Accepted" in the app
|
||||
But I should not find "Declined" in the app
|
||||
|
||||
When I press "Withdraw" in the app
|
||||
Then I should find "Declined" in the app
|
||||
But I should not find "Accepted" in the app
|
||||
|
||||
# Test tablet view now.
|
||||
When I press the back button in the app
|
||||
And I change viewport size to "1200x640" in the app
|
||||
And I press "User account" in the app
|
||||
And I press "Policies and agreements" in the app
|
||||
Then I should find "Mandatory policy own page" in the app
|
||||
And I should find "Optional policy own page" in the app
|
||||
And I should find "Mandatory policy consent page" in the app
|
||||
And I should find "Optional policy consent page" in the app
|
||||
And I should find "mo v1" within "Mandatory policy own page" "tr" in the app
|
||||
And I should find "Active" within "Mandatory policy own page" "tr" in the app
|
||||
And I should find "Accepted" within "Mandatory policy own page" "tr" in the app
|
||||
And I should find "Declined" within "Optional policy own page" "tr" in the app
|
||||
But I should not be able to press "Withdraw" within "Mandatory policy own page" "tr" in the app
|
||||
And I should not find "Summ mand own page" in the app
|
||||
And I should not find "Content mand own page" in the app
|
||||
|
||||
When I press "Mandatory policy own page" in the app
|
||||
Then I should find "Summ mand own page" in the app
|
||||
And I should find "Content mand own page" in the app
|
||||
But I should not find "Summ opt own page" in the app
|
||||
|
||||
When I press "Close" in the app
|
||||
And I press "Accept" within "Optional policy own page" "tr" in the app
|
||||
Then I should find "Accepted" within "Optional policy own page" "tr" in the app
|
|
@ -0,0 +1,28 @@
|
|||
@core_policy @app @javascript @lms_from4.4
|
||||
Feature: Test contact DPO from acceptances page
|
||||
|
||||
Background:
|
||||
Given the following config values are set as admin:
|
||||
| sitepolicyhandler | tool_policy |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student | User | One | one@example.com |
|
||||
|
||||
Scenario: Cannot contact DPO if not enabled
|
||||
When I entered the app as "student"
|
||||
And I press the user menu button in the app
|
||||
And I press "Policies and agreements" in the app
|
||||
Then I should find "For any questions about the policies please contact the privacy officer" in the app
|
||||
But I should not be able to press "Contact" in the app
|
||||
|
||||
Scenario: Can contact DPO if enabled
|
||||
Given the following config values are set as admin:
|
||||
| contactdataprotectionofficer | 1 | tool_dataprivacy |
|
||||
When I entered the app as "student"
|
||||
And I press the user menu button in the app
|
||||
And I press "Policies and agreements" in the app
|
||||
Then I should find "For any questions about the policies please contact the privacy officer" in the app
|
||||
|
||||
When I press "Contact" in the app
|
||||
Then I should find "Data privacy" in the app
|
||||
And I should be able to press "Contact the privacy officer" in the app
|
|
@ -55,6 +55,7 @@
|
|||
"connectionlost": "Connection to site lost",
|
||||
"considereddigitalminor": "You are too young to create an account on this site.",
|
||||
"contactsupport": "Contact support",
|
||||
"contactverb": "Contact",
|
||||
"content": "Content",
|
||||
"contenteditingsynced": "The content you are editing has been synced.",
|
||||
"continue": "Continue",
|
||||
|
|
|
@ -942,7 +942,7 @@ ion-card {
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
margin: 0 8px 8px 8px;
|
||||
margin: 0px 8px 8px 8px;
|
||||
|
||||
ion-button {
|
||||
&[fill="outline"] {
|
||||
|
@ -1633,9 +1633,10 @@ ion-item.item.divider {
|
|||
}
|
||||
|
||||
ion-item-divider.item,
|
||||
ion-item.item {
|
||||
ion-item.item,
|
||||
td {
|
||||
.expandable-status-icon {
|
||||
font-size: 18px;
|
||||
font-size: 1.125rem;
|
||||
@include core-transition(transform, 200ms);
|
||||
@include margin-horizontal(null, 16px);
|
||||
|
||||
|
|
Loading…
Reference in New Issue