MOBILE-4329 dataprivacy: Implement data privacy pages
parent
84d83b0450
commit
791903a80d
|
@ -1677,6 +1677,40 @@
|
|||
"core.courses.totalcoursesearchresults": "local_moodlemobileapp",
|
||||
"core.currentdevice": "local_moodlemobileapp",
|
||||
"core.custom": "form",
|
||||
"core.dataprivacy.cancelrequest": "tool_dataprivacy",
|
||||
"core.dataprivacy.cancelrequestconfirmation": "tool_dataprivacy",
|
||||
"core.dataprivacy.contactdataprotectionofficer": "tool_dataprivacy",
|
||||
"core.dataprivacy.createnewdatarequest": "tool_dataprivacy",
|
||||
"core.dataprivacy.datarequests": "tool_dataprivacy",
|
||||
"core.dataprivacy.daterequested": "tool_dataprivacy",
|
||||
"core.dataprivacy.deletemyaccount": "tool_dataprivacy",
|
||||
"core.dataprivacy.message": "tool_dataprivacy",
|
||||
"core.dataprivacy.newrequest": "tool_dataprivacy",
|
||||
"core.dataprivacy.nodatarequests": "tool_dataprivacy",
|
||||
"core.dataprivacy.pluginname": "tool_dataprivacy",
|
||||
"core.dataprivacy.replyto": "tool_dataprivacy",
|
||||
"core.dataprivacy.requestactions": "tool_dataprivacy",
|
||||
"core.dataprivacy.requestby": "tool_dataprivacy",
|
||||
"core.dataprivacy.requestcomments": "tool_dataprivacy",
|
||||
"core.dataprivacy.requeststatus": "tool_dataprivacy",
|
||||
"core.dataprivacy.requestsubmitted": "tool_dataprivacy",
|
||||
"core.dataprivacy.requesttype": "tool_dataprivacy",
|
||||
"core.dataprivacy.requesttype_help": "tool_dataprivacy",
|
||||
"core.dataprivacy.requesttypedelete": "tool_dataprivacy",
|
||||
"core.dataprivacy.requesttypeexport": "tool_dataprivacy",
|
||||
"core.dataprivacy.requesttypeothers": "tool_dataprivacy",
|
||||
"core.dataprivacy.send": "tool_dataprivacy",
|
||||
"core.dataprivacy.statusapproved": "tool_dataprivacy",
|
||||
"core.dataprivacy.statusawaitingapproval": "tool_dataprivacy",
|
||||
"core.dataprivacy.statuscancelled": "tool_dataprivacy",
|
||||
"core.dataprivacy.statuscomplete": "tool_dataprivacy",
|
||||
"core.dataprivacy.statusdeleted": "tool_dataprivacy",
|
||||
"core.dataprivacy.statusexpired": "tool_dataprivacy",
|
||||
"core.dataprivacy.statuspending": "tool_dataprivacy",
|
||||
"core.dataprivacy.statuspreprocessing": "tool_dataprivacy",
|
||||
"core.dataprivacy.statusprocessing": "tool_dataprivacy",
|
||||
"core.dataprivacy.statusready": "tool_dataprivacy",
|
||||
"core.dataprivacy.statusrejected": "tool_dataprivacy",
|
||||
"core.datastoredoffline": "local_moodlemobileapp",
|
||||
"core.date": "moodle",
|
||||
"core.datecreated": "repository",
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
:host {
|
||||
--image-size: 120px;
|
||||
--icon-color: var(--text-color);
|
||||
--icon-color: var(--subdued-text-color);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
// (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 { CoreDataPrivacyContactDPOComponent } from './contactdpo/contactdpo';
|
||||
import { CoreDataPrivacyNewRequestComponent } from './newrequest/newrequest';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreDataPrivacyContactDPOComponent,
|
||||
CoreDataPrivacyNewRequestComponent,
|
||||
],
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
],
|
||||
exports: [
|
||||
CoreDataPrivacyContactDPOComponent,
|
||||
CoreDataPrivacyNewRequestComponent,
|
||||
],
|
||||
})
|
||||
export class CoreDataPrivacyComponentsModule {}
|
|
@ -0,0 +1,37 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
<h1>{{ 'core.dataprivacy.contactdataprotectionofficer' | translate }}</h1>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button fill="clear" (click)="close()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon slot="icon-only" name="fas-xmark" aria-hidden="true" />
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<form [formGroup]="form" name="contactDPO" (ngSubmit)="send($event)">
|
||||
<ion-item *ngIf="email">
|
||||
<ion-label>
|
||||
<p class="item-heading">
|
||||
{{ 'core.dataprivacy.replyto' | translate }}
|
||||
</p>
|
||||
<p>{{ email }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-textarea labelPlacement="floating" placeholder="{{ 'core.dataprivacy.message' | translate }}" rows="5"
|
||||
[(ngModel)]="message" name="text" [required]="true" formControlName="message">
|
||||
<div [core-mark-required]="true" slot="label">
|
||||
{{ 'core.dataprivacy.message' | translate }}
|
||||
</div>
|
||||
</ion-textarea>
|
||||
</ion-item>
|
||||
</form>
|
||||
</ion-content>
|
||||
<ion-footer slot="fixed" class="ion-padding">
|
||||
<ion-button expand="block" (click)="send($event)" [disabled]="!form.valid">
|
||||
{{ 'core.dataprivacy.send' | translate }}
|
||||
</ion-button>
|
||||
</ion-footer>
|
|
@ -0,0 +1,94 @@
|
|||
// (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, OnInit } from '@angular/core';
|
||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||
import { CoreDataPrivacy } from '@features/dataprivacy/services/dataprivacy';
|
||||
import { CoreUser } from '@features/user/services/user';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils, ToastDuration } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
|
||||
import { ModalController } from '@singletons';
|
||||
|
||||
/**
|
||||
* Component that displays the contact DPO page.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-data-privacy-contact-dpo',
|
||||
templateUrl: 'contactdpo.html',
|
||||
})
|
||||
export class CoreDataPrivacyContactDPOComponent implements OnInit {
|
||||
|
||||
message = '';
|
||||
email = '';
|
||||
|
||||
// Form variables.
|
||||
form: FormGroup;
|
||||
|
||||
constructor(
|
||||
protected fb: FormBuilder,
|
||||
) {
|
||||
this.form = new FormGroup({});
|
||||
|
||||
// Initialize form variables.
|
||||
this.form.addControl('message', this.fb.control('', Validators.required));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
const modal = await CoreDomUtils.showModalLoading();
|
||||
|
||||
// Get current user email.
|
||||
const userId = CoreSites.getCurrentSiteUserId();
|
||||
const user = await CoreUtils.ignoreErrors(CoreUser.getProfile(userId));
|
||||
|
||||
this.email = user?.email || '';
|
||||
|
||||
modal.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the message.
|
||||
*/
|
||||
async send(event: Event): Promise<void> {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const modal = await CoreDomUtils.showModalLoading();
|
||||
|
||||
try {
|
||||
// Send the message.
|
||||
const succeed = await CoreDataPrivacy.contactDPO(this.message);
|
||||
if (succeed) {
|
||||
CoreDomUtils.showToast('core.dataprivacy.requestsubmitted', true, ToastDuration.LONG);
|
||||
ModalController.dismiss(true);
|
||||
}
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error sending data privacy request');
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal.
|
||||
*/
|
||||
close(): void {
|
||||
ModalController.dismiss();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
<h1>{{ 'core.dataprivacy.createnewdatarequest' | translate }}</h1>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button fill="clear" (click)="close()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon slot="icon-only" name="fas-xmark" aria-hidden="true" />
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<form [formGroup]="form" name="newRequest" (ngSubmit)="send($event)">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<p>
|
||||
{{ 'core.dataprivacy.requesttype_help' | translate }}
|
||||
</p>
|
||||
<p class="item-heading" [core-mark-required]="true">
|
||||
{{ 'core.dataprivacy.requesttype' | translate }}
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-radio-group name="type" formControlName="type">
|
||||
<ion-radio [value]="1" *ngIf="accessInfo?.cancreatedatadownloadrequest">
|
||||
{{ 'core.dataprivacy.requesttypeexport' | translate }}
|
||||
</ion-radio>
|
||||
<ion-radio [value]="2" *ngIf="accessInfo?.cancreatedatadeletionrequest">
|
||||
{{ 'core.dataprivacy.requesttypedelete' | translate }}
|
||||
</ion-radio>
|
||||
</ion-radio-group>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-textarea labelPlacement="stacked" placeholder="{{ 'core.dataprivacy.requestcomments' | translate }}" rows="5"
|
||||
[(ngModel)]="message" name="text" formControlName="message">
|
||||
<div slot="label">
|
||||
{{ 'core.dataprivacy.requestcomments' | translate }}
|
||||
</div>
|
||||
</ion-textarea>
|
||||
</ion-item>
|
||||
|
||||
</form>
|
||||
</ion-content>
|
||||
<ion-footer slot="fixed" class="ion-padding">
|
||||
<ion-button expand="block" (click)="send($event)" [disabled]="!form.valid">
|
||||
{{ 'core.dataprivacy.send' | translate }}
|
||||
</ion-button>
|
||||
</ion-footer>
|
|
@ -0,0 +1,104 @@
|
|||
// (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, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import {
|
||||
CoreDataPrivacy,
|
||||
CoreDataPrivacyDataRequestType,
|
||||
CoreDataPrivacyGetAccessInformationWSResponse,
|
||||
} from '@features/dataprivacy/services/dataprivacy';
|
||||
import { CoreDomUtils, ToastDuration } from '@services/utils/dom';
|
||||
|
||||
import { ModalController } from '@singletons';
|
||||
|
||||
/**
|
||||
* Component that displays the new request page.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-data-privacy-new-request',
|
||||
templateUrl: 'newrequest.html',
|
||||
})
|
||||
export class CoreDataPrivacyNewRequestComponent implements OnInit {
|
||||
|
||||
@Input() accessInfo?: CoreDataPrivacyGetAccessInformationWSResponse;
|
||||
|
||||
message = '';
|
||||
|
||||
// Form variables.
|
||||
form: FormGroup;
|
||||
typeControl: FormControl<CoreDataPrivacyDataRequestType>;
|
||||
|
||||
constructor(
|
||||
protected fb: FormBuilder,
|
||||
) {
|
||||
this.form = new FormGroup({});
|
||||
|
||||
// Initialize form variables.
|
||||
this.typeControl = this.fb.control(
|
||||
CoreDataPrivacyDataRequestType.DATAREQUEST_TYPE_EXPORT,
|
||||
{ validators: Validators.required, nonNullable: true },
|
||||
);
|
||||
this.form.addControl('type', this.typeControl);
|
||||
this.form.addControl('message', this.fb.control(''));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
// It should not happen. If there's no access info, close the modal.
|
||||
if (!this.accessInfo) {
|
||||
ModalController.dismiss();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Just in case only deleting is allowed, change the default type.
|
||||
if (!this.accessInfo.cancreatedatadownloadrequest && this.accessInfo.cancreatedatadeletionrequest){
|
||||
this.typeControl.setValue(CoreDataPrivacyDataRequestType.DATAREQUEST_TYPE_DELETE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the request.
|
||||
*/
|
||||
async send(event: Event): Promise<void> {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const modal = await CoreDomUtils.showModalLoading();
|
||||
|
||||
try {
|
||||
// Send the message.
|
||||
const requestId = await CoreDataPrivacy.createDataRequest(this.typeControl.value, this.message);
|
||||
if (requestId) {
|
||||
CoreDomUtils.showToast('core.dataprivacy.requestsubmitted', true, ToastDuration.LONG);
|
||||
ModalController.dismiss(true);
|
||||
}
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error sending data privacy request');
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal.
|
||||
*/
|
||||
close(): void {
|
||||
ModalController.dismiss();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreDataPrivacyMainPage } from './pages/main/main';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
component: CoreDataPrivacyMainPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CoreSharedModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreDataPrivacyMainPage,
|
||||
],
|
||||
})
|
||||
export class CoreDataPrivacyLazyModule {}
|
|
@ -15,9 +15,23 @@
|
|||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||
import { CoreUserDelegate } from '@features/user/services/user-delegate';
|
||||
import { CoreDataPrivacyUserHandler } from './services/handlers/user';
|
||||
import { Routes } from '@angular/router';
|
||||
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||
import { CoreDataPrivacyComponentsModule } from './components/components.module';
|
||||
import { CORE_DATAPRIVACY_PAGE_NAME } from './constants';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: CORE_DATAPRIVACY_PAGE_NAME,
|
||||
loadChildren: () => import('./dataprivacy-lazy.module').then(m => m.CoreDataPrivacyLazyModule),
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CoreMainMenuTabRoutingModule.forChild(routes),
|
||||
CoreDataPrivacyComponentsModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
|
|
|
@ -1,3 +1,36 @@
|
|||
{
|
||||
"pluginname": "Data privacy"
|
||||
"contactdataprotectionofficer": "Contact the privacy officer",
|
||||
"cancelrequest": "Cancel request",
|
||||
"cancelrequestconfirmation": "Do you really want cancel this data request?",
|
||||
"createnewdatarequest": "Create a new data request",
|
||||
"datarequests": "Data requests",
|
||||
"daterequested": "Date requested",
|
||||
"deletemyaccount": "Delete my account",
|
||||
"message": "Message",
|
||||
"newrequest": "New request",
|
||||
"nodatarequests": "There are no data requests",
|
||||
"pluginname": "Data privacy",
|
||||
"replyto": "Reply to",
|
||||
"requestactions": "Actions",
|
||||
"requestby": "Requested by",
|
||||
"requestcomments": "Comments",
|
||||
"requeststatus": "Status",
|
||||
"requestsubmitted": "Your request has been submitted to the privacy officer",
|
||||
"requesttype_help": "Select the reason for contacting the privacy officer. Be aware that deletion of all personal data will result in you no longer being able to log in to the site.",
|
||||
"requesttype": "Type",
|
||||
"requesttypedelete": "Delete all of my personal data",
|
||||
"requesttypeexport": "Export all of my personal data",
|
||||
"requesttypeothers": "General enquiry",
|
||||
"send": "Send",
|
||||
"statusapproved": "Approved",
|
||||
"statusawaitingapproval": "Awaiting approval",
|
||||
"statuscancelled": "Cancelled",
|
||||
"statuscomplete": "Complete",
|
||||
"statusdeleted": "Deleted",
|
||||
"statusexpired": "Expired",
|
||||
"statuspending": "Pending",
|
||||
"statuspreprocessing": "Pre-processing",
|
||||
"statusprocessing": "Processing",
|
||||
"statusready": "Download ready",
|
||||
"statusrejected": "Rejected"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [text]="'core.back' | translate" />
|
||||
</ion-buttons>
|
||||
<ion-title>
|
||||
<h1>{{ 'core.dataprivacy.pluginname' | translate }}</h1>
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshContent($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}" />
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<ion-item-divider class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<h2 class="big">{{ 'core.dataprivacy.datarequests' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-list *ngIf="requests.length && !isTablet">
|
||||
<ion-card *ngFor=" let request of requests">
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<p class="item-heading">
|
||||
<ng-container *ngTemplateOutlet="type; context: {request: request}" />
|
||||
</p>
|
||||
<ion-row class="ion-justify-content-between ion-no-padding">
|
||||
<ion-col class="ion-no-padding">
|
||||
<p>{{request.timecreated * 1000 | coreFormatDate }}</p>
|
||||
</ion-col>
|
||||
<ion-col class="core-flex-no-grow ion-text-end ion-no-padding">
|
||||
<ng-container *ngTemplateOutlet="statusBadge; context: {request: request}" />
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button [detail]="false" *ngIf="request.requestedbyuser" core-user-link [userId]="request.requestedbyuser.id"
|
||||
[attr.aria-label]="request.requestedbyuser.fullname" class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.dataprivacy.requestby' | translate }}</p>
|
||||
<p>{{ request.requestedbyuser.fullname }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" [lines]="request.canCancel ? 'full' : 'none'">
|
||||
<ion-label>
|
||||
<p class="item-heading">{{ 'core.dataprivacy.message' | translate }}</p>
|
||||
<p><core-format-text [text]="request.messagehtml" /></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap ion-text-end" *ngIf="request.canCancel">
|
||||
<ion-label>
|
||||
<ion-button fill="outline" expand="block" (click)="cancelRequest(request.id)">
|
||||
{{ 'core.dataprivacy.cancelrequest' | translate }}
|
||||
</ion-button>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
</ion-list>
|
||||
|
||||
<table *ngIf="requests.length && isTablet" class="core-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'core.dataprivacy.requesttype' | translate }}</th>
|
||||
<th>{{ 'core.dataprivacy.daterequested' | translate }}</th>
|
||||
<th class="shrink">{{ 'core.dataprivacy.requestby' | translate }}</th>
|
||||
<th class="shrink">{{ 'core.dataprivacy.requeststatus' | translate }}</th>
|
||||
<th>{{ 'core.dataprivacy.message' | translate }}</th>
|
||||
<th class=" shrink">{{ 'core.dataprivacy.requestactions' | translate }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="auto-striped">
|
||||
<tr *ngFor=" let request of requests">
|
||||
<td>
|
||||
<p><ng-container *ngTemplateOutlet="type; context: {request: request}" /></p>
|
||||
</td>
|
||||
<td>
|
||||
<p>{{request.timecreated * 1000 | coreFormatDate }}</p>
|
||||
</td>
|
||||
<td>
|
||||
<p>{{ request.requestedbyuser.fullname }}</p>
|
||||
</td>
|
||||
<td>
|
||||
<ng-container *ngTemplateOutlet="statusBadge; context: {request: request}" />
|
||||
</td>
|
||||
<td>
|
||||
<p><core-format-text [text]="request.messagehtml" /></p>
|
||||
</td>
|
||||
<td>
|
||||
<ion-button fill="outline" size="small" (click)="cancelRequest(request.id)" *ngIf="request.canCancel">
|
||||
{{ 'core.dataprivacy.cancelrequest' | translate }}
|
||||
</ion-button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<core-empty-box *ngIf="!requests.length" icon="fas-bell-concierge" [message]="'core.dataprivacy.nodatarequests' | translate" />
|
||||
|
||||
<div collapsible-footer *ngIf="loaded" slot="fixed">
|
||||
<div class="list-item-limited-width adaptable-buttons-row">
|
||||
<ion-button class="ion-margin ion-text-wrap" expand="block" (click)="contactDPO()" *ngIf="accessInfo?.cancontactdpo"
|
||||
fill="outline">
|
||||
<ion-icon slot="start" name="fas-envelope" [attr.aria-hidden]="true" />
|
||||
{{ 'core.dataprivacy.contactdataprotectionofficer' | translate }}
|
||||
</ion-button>
|
||||
<ion-button class="ion-margin ion-text-wrap" expand="block" (click)="newRequest()"
|
||||
*ngIf="accessInfo?.cancreatedatadownloadrequest || accessInfo?.cancreatedatadeletionrequest">
|
||||
<ion-icon slot="start" name="fas-pen-to-square" [attr.aria-hidden]="true" />
|
||||
{{ 'core.dataprivacy.newrequest' | translate }}
|
||||
</ion-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</core-loading>
|
||||
</ion-content>
|
||||
|
||||
<ng-template #statusBadge let-request="request">
|
||||
@switch (request.status) {
|
||||
@case (0) {
|
||||
<ion-badge color="info">{{'core.dataprivacy.statuspending' | translate }}</ion-badge>
|
||||
} @case (1) {
|
||||
<ion-badge color="info">{{'core.dataprivacy.statuspreprocessing' | translate }}</ion-badge>
|
||||
} @case (2) {
|
||||
<ion-badge color="info">{{'core.dataprivacy.statusawaitingapproval' | translate }}</ion-badge>
|
||||
} @case (3) {
|
||||
<ion-badge color="info">{{'core.dataprivacy.statusapproved' | translate }}</ion-badge>
|
||||
} @case (4) {
|
||||
<ion-badge color="info">{{'core.dataprivacy.statusprocessing' | translate }}</ion-badge>
|
||||
} @case (5) {
|
||||
<ion-badge color="success">{{'core.dataprivacy.statuscomplete' | translate }}</ion-badge>
|
||||
} @case (6) {
|
||||
<ion-badge color="warning">{{'core.dataprivacy.statuscancelled' | translate }}</ion-badge>
|
||||
} @case (7) {
|
||||
<ion-badge color="danger">{{'core.dataprivacy.statusrejected' | translate }}</ion-badge>
|
||||
} @case (8) {
|
||||
<ion-badge color="success">{{'core.dataprivacy.statusready' | translate }}</ion-badge>
|
||||
} @case (9) {
|
||||
<ion-badge color="secondary">{{'core.dataprivacy.statusexpired' | translate }}</ion-badge>
|
||||
} @case (10) {
|
||||
<ion-badge color="success">{{'core.dataprivacy.statusdeleted' | translate }}</ion-badge>
|
||||
} @default {
|
||||
<ion-badge class="ion-text-wrap">{{request.statuslabel}}</ion-badge>
|
||||
}
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #type let-request="request">
|
||||
@switch (request.type) {
|
||||
@case (1) {
|
||||
{{ 'core.dataprivacy.requesttypeexport' | translate }}
|
||||
} @case (2) {
|
||||
{{ 'core.dataprivacy.requesttypedelete' | translate }}
|
||||
} @case (3) {
|
||||
{{ 'core.dataprivacy.requesttypeothers' | translate }}
|
||||
} @default {
|
||||
{{request.typename}}
|
||||
}
|
||||
}
|
||||
</ng-template>
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
table {
|
||||
th {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
th.shrink {
|
||||
width: 1%;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
// (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, OnInit } from '@angular/core';
|
||||
import { CoreDataPrivacyContactDPOComponent } from '@features/dataprivacy/components/contactdpo/contactdpo';
|
||||
import { CoreDataPrivacyNewRequestComponent } from '@features/dataprivacy/components/newrequest/newrequest';
|
||||
import {
|
||||
CoreDataPrivacy,
|
||||
CoreDataPrivacyGetAccessInformationWSResponse,
|
||||
CoreDataPrivacyRequest,
|
||||
} from '@features/dataprivacy/services/dataprivacy';
|
||||
import { CoreScreen } from '@services/screen';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Translate } from '@singletons';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Page to display the main data privacy page.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-data-privacy-main',
|
||||
templateUrl: 'main.html',
|
||||
styleUrl: 'main.scss',
|
||||
})
|
||||
export class CoreDataPrivacyMainPage implements OnInit {
|
||||
|
||||
accessInfo?: CoreDataPrivacyGetAccessInformationWSResponse;
|
||||
requests: CoreDataPrivacyRequestToDisplay[] = [];
|
||||
loaded = false;
|
||||
isTablet = false;
|
||||
layoutSubscription?: Subscription;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.fetchContent();
|
||||
|
||||
this.isTablet = CoreScreen.isTablet;
|
||||
|
||||
this.layoutSubscription = CoreScreen.layoutObservable.subscribe(() => {
|
||||
this.isTablet = CoreScreen.isTablet;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch page content.
|
||||
*/
|
||||
async fetchContent(): Promise<void> {
|
||||
try {
|
||||
this.accessInfo = await CoreDataPrivacy.getAccessInformation();
|
||||
|
||||
this.requests = await CoreDataPrivacy.getDataRequests();
|
||||
|
||||
this.requests.forEach((request) => {
|
||||
request.canCancel = CoreDataPrivacy.canCancelRequest(request);
|
||||
});
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error fetching data privacy information', true);
|
||||
} finally {
|
||||
this.loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the page content.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
*/
|
||||
async refreshContent(refresher?: HTMLIonRefresherElement): Promise<void> {
|
||||
await CoreUtils.ignoreErrors(
|
||||
CoreDataPrivacy.invalidateAll(),
|
||||
);
|
||||
|
||||
await CoreUtils.ignoreErrors(this.fetchContent());
|
||||
|
||||
refresher?.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the contact DPO modal.
|
||||
*/
|
||||
async contactDPO(): Promise<void> {
|
||||
// Create and show the modal.
|
||||
const succeed = await CoreDomUtils.openModal<boolean>({
|
||||
component: CoreDataPrivacyContactDPOComponent,
|
||||
});
|
||||
|
||||
if (succeed) {
|
||||
const modal = await CoreDomUtils.showModalLoading();
|
||||
try {
|
||||
await this.refreshContent();
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the new request modal.
|
||||
*/
|
||||
async newRequest(): Promise<void> {
|
||||
// Create and show the modal.
|
||||
const succeed = await CoreDomUtils.openModal<boolean>({
|
||||
component: CoreDataPrivacyNewRequestComponent,
|
||||
componentProps: {
|
||||
accessInfo: this.accessInfo,
|
||||
},
|
||||
});
|
||||
|
||||
if (succeed) {
|
||||
const modal = await CoreDomUtils.showModalLoading();
|
||||
try {
|
||||
await this.refreshContent();
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a request.
|
||||
*
|
||||
* @param requestId Request ID.
|
||||
*/
|
||||
async cancelRequest(requestId: number): Promise<void> {
|
||||
|
||||
try {
|
||||
await CoreDomUtils.showConfirm(
|
||||
Translate.instant('core.dataprivacy.cancelrequestconfirmation'),
|
||||
Translate.instant('core.dataprivacy.cancelrequest'),
|
||||
Translate.instant('core.dataprivacy.cancelrequest'),
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = await CoreDomUtils.showModalLoading();
|
||||
|
||||
try {
|
||||
await CoreDataPrivacy.cancelDataRequest(requestId);
|
||||
|
||||
await this.refreshContent();
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'Error cancelling data privacy request');
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type CoreDataPrivacyRequestToDisplay = CoreDataPrivacyRequest & {
|
||||
canCancel?: boolean;
|
||||
};
|
|
@ -170,7 +170,7 @@ export class CoreDataPrivacyService {
|
|||
async createDataRequest(type: CoreDataPrivacyDataRequestType, comments: string, siteId?: string): Promise<number> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const params: CoreDataPrivacyCreateDataequestWSParams = {
|
||||
const params: CoreDataPrivacyCreateDataRequestWSParams = {
|
||||
type,
|
||||
comments,
|
||||
};
|
||||
|
@ -292,7 +292,7 @@ type CoreDataPrivacyContactDPOWSResponse = {
|
|||
/**
|
||||
* Params of tool_dataprivacy_create_data_request WS.
|
||||
*/
|
||||
type CoreDataPrivacyCreateDataequestWSParams = {
|
||||
type CoreDataPrivacyCreateDataRequestWSParams = {
|
||||
type: CoreDataPrivacyDataRequestType; // The type of data request to create. 1 for export, 2 for data deletion.
|
||||
comments?: string; // Comments for the data request.
|
||||
foruserid?: number; // The id of the user to create the data request for. Empty for current user.
|
||||
|
|
|
@ -24,8 +24,6 @@ export const CORE_EDITOR_SERVICES: Type<unknown>[] = [
|
|||
];
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
],
|
||||
imports: [
|
||||
CoreEditorComponentsModule,
|
||||
],
|
||||
|
|
Loading…
Reference in New Issue