commit
60e256eda6
|
@ -84,6 +84,7 @@ jobs:
|
||||||
"@core_comments"
|
"@core_comments"
|
||||||
"@core_course"
|
"@core_course"
|
||||||
"@core_courses"
|
"@core_courses"
|
||||||
|
"@core_dataprivacy"
|
||||||
"@core_grades"
|
"@core_grades"
|
||||||
"@core_login"
|
"@core_login"
|
||||||
"@core_mainmenu"
|
"@core_mainmenu"
|
||||||
|
|
|
@ -1677,6 +1677,40 @@
|
||||||
"core.courses.totalcoursesearchresults": "local_moodlemobileapp",
|
"core.courses.totalcoursesearchresults": "local_moodlemobileapp",
|
||||||
"core.currentdevice": "local_moodlemobileapp",
|
"core.currentdevice": "local_moodlemobileapp",
|
||||||
"core.custom": "form",
|
"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.datastoredoffline": "local_moodlemobileapp",
|
||||||
"core.date": "moodle",
|
"core.date": "moodle",
|
||||||
"core.datecreated": "repository",
|
"core.datecreated": "repository",
|
||||||
|
|
|
@ -16,9 +16,9 @@ import { Injectable } from '@angular/core';
|
||||||
import { CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses';
|
import { CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses';
|
||||||
import {
|
import {
|
||||||
CoreUserDelegateContext,
|
CoreUserDelegateContext,
|
||||||
CoreUserDelegateService,
|
|
||||||
CoreUserProfileHandler,
|
CoreUserProfileHandler,
|
||||||
CoreUserProfileHandlerData,
|
CoreUserProfileHandlerData,
|
||||||
|
CoreUserProfileHandlerType,
|
||||||
} from '@features/user/services/user-delegate';
|
} from '@features/user/services/user-delegate';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
|
@ -33,7 +33,7 @@ export class AddonBadgesUserHandlerService implements CoreUserProfileHandler {
|
||||||
|
|
||||||
name = 'AddonBadges:fakename'; // This name doesn't match any disabled feature, they'll be checked in isEnabledForContext.
|
name = 'AddonBadges:fakename'; // This name doesn't match any disabled feature, they'll be checked in isEnabledForContext.
|
||||||
priority = 300;
|
priority = 300;
|
||||||
type = CoreUserDelegateService.TYPE_NEW_PAGE;
|
type = CoreUserProfileHandlerType.LIST_ITEM;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
CoreUserProfileHandler,
|
CoreUserProfileHandler,
|
||||||
CoreUserProfileHandlerData,
|
CoreUserProfileHandlerData,
|
||||||
CoreUserDelegateService,
|
CoreUserProfileHandlerType,
|
||||||
CoreUserDelegateContext,
|
CoreUserDelegateContext,
|
||||||
} from '@features/user/services/user-delegate';
|
} from '@features/user/services/user-delegate';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
@ -32,7 +32,7 @@ export class AddonBlogUserHandlerService implements CoreUserProfileHandler {
|
||||||
|
|
||||||
name = 'AddonBlog'; // This name doesn't match any disabled feature, they'll be checked in isEnabledForContext.
|
name = 'AddonBlog'; // This name doesn't match any disabled feature, they'll be checked in isEnabledForContext.
|
||||||
priority = 200;
|
priority = 200;
|
||||||
type = CoreUserDelegateService.TYPE_NEW_PAGE;
|
type = CoreUserProfileHandlerType.LIST_ITEM;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { COURSE_PAGE_NAME } from '@features/course/course.module';
|
||||||
import { CoreUserProfile } from '@features/user/services/user';
|
import { CoreUserProfile } from '@features/user/services/user';
|
||||||
import {
|
import {
|
||||||
CoreUserProfileHandler,
|
CoreUserProfileHandler,
|
||||||
CoreUserDelegateService,
|
CoreUserProfileHandlerType,
|
||||||
CoreUserProfileHandlerData,
|
CoreUserProfileHandlerData,
|
||||||
CoreUserDelegateContext,
|
CoreUserDelegateContext,
|
||||||
} from '@features/user/services/user-delegate';
|
} from '@features/user/services/user-delegate';
|
||||||
|
@ -36,7 +36,7 @@ export class AddonCompetencyUserHandlerService implements CoreUserProfileHandler
|
||||||
|
|
||||||
name = 'AddonCompetency'; // This name doesn't match any disabled feature, they'll be checked in isEnabledForContext.
|
name = 'AddonCompetency'; // This name doesn't match any disabled feature, they'll be checked in isEnabledForContext.
|
||||||
priority = 100;
|
priority = 100;
|
||||||
type = CoreUserDelegateService.TYPE_NEW_PAGE;
|
type = CoreUserProfileHandlerType.LIST_ITEM;
|
||||||
cacheEnabled = true;
|
cacheEnabled = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
|
||||||
import { CoreUserProfile } from '@features/user/services/user';
|
import { CoreUserProfile } from '@features/user/services/user';
|
||||||
import {
|
import {
|
||||||
CoreUserProfileHandler,
|
CoreUserProfileHandler,
|
||||||
CoreUserDelegateService,
|
CoreUserProfileHandlerType,
|
||||||
CoreUserProfileHandlerData,
|
CoreUserProfileHandlerData,
|
||||||
CoreUserDelegateContext,
|
CoreUserDelegateContext,
|
||||||
} from '@features/user/services/user-delegate';
|
} from '@features/user/services/user-delegate';
|
||||||
|
@ -31,7 +31,7 @@ import { AddonCourseCompletion } from '../coursecompletion';
|
||||||
export class AddonCourseCompletionUserHandlerService implements CoreUserProfileHandler {
|
export class AddonCourseCompletionUserHandlerService implements CoreUserProfileHandler {
|
||||||
|
|
||||||
name = 'AddonCourseCompletion:viewCompletion';
|
name = 'AddonCourseCompletion:viewCompletion';
|
||||||
type = CoreUserDelegateService.TYPE_NEW_PAGE;
|
type = CoreUserProfileHandlerType.LIST_ITEM;
|
||||||
priority = 350;
|
priority = 350;
|
||||||
cacheEnabled = true;
|
cacheEnabled = true;
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,11 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
import { CoreUserProfile } from '@features/user/services/user';
|
import { CoreUserProfile } from '@features/user/services/user';
|
||||||
import { CoreUserDelegateService, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@features/user/services/user-delegate';
|
import {
|
||||||
|
CoreUserProfileHandlerType,
|
||||||
|
CoreUserProfileHandler,
|
||||||
|
CoreUserProfileHandlerData,
|
||||||
|
} from '@features/user/services/user-delegate';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
|
@ -29,7 +33,7 @@ export class AddonMessagesSendMessageUserHandlerService implements CoreUserProfi
|
||||||
|
|
||||||
name = 'AddonMessages:sendMessage';
|
name = 'AddonMessages:sendMessage';
|
||||||
priority = 1000;
|
priority = 1000;
|
||||||
type = CoreUserDelegateService.TYPE_COMMUNICATION;
|
type = CoreUserProfileHandlerType.BUTTON;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
|
||||||
import { CoreUserProfile } from '@features/user/services/user';
|
import { CoreUserProfile } from '@features/user/services/user';
|
||||||
import {
|
import {
|
||||||
CoreUserProfileHandler,
|
CoreUserProfileHandler,
|
||||||
CoreUserDelegateService,
|
CoreUserProfileHandlerType,
|
||||||
CoreUserProfileHandlerData,
|
CoreUserProfileHandlerData,
|
||||||
CoreUserDelegateContext,
|
CoreUserDelegateContext,
|
||||||
} from '@features/user/services/user-delegate';
|
} from '@features/user/services/user-delegate';
|
||||||
|
@ -33,7 +33,7 @@ export class AddonNotesUserHandlerService implements CoreUserProfileHandler {
|
||||||
|
|
||||||
name = 'AddonNotes:notes';
|
name = 'AddonNotes:notes';
|
||||||
priority = 250;
|
priority = 250;
|
||||||
type = CoreUserDelegateService.TYPE_NEW_PAGE;
|
type = CoreUserProfileHandlerType.LIST_ITEM;
|
||||||
cacheEnabled = true;
|
cacheEnabled = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { AddonPrivateFiles } from '@addons/privatefiles/services/privatefiles';
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
import {
|
import {
|
||||||
CoreUserDelegateContext,
|
CoreUserDelegateContext,
|
||||||
CoreUserDelegateService,
|
CoreUserProfileHandlerType,
|
||||||
CoreUserProfileHandler,
|
CoreUserProfileHandler,
|
||||||
CoreUserProfileHandlerData,
|
CoreUserProfileHandlerData,
|
||||||
} from '@features/user/services/user-delegate';
|
} from '@features/user/services/user-delegate';
|
||||||
|
@ -36,7 +36,7 @@ export class AddonPrivateFilesUserHandlerService implements CoreUserProfileHandl
|
||||||
|
|
||||||
name = 'AddonPrivateFiles';
|
name = 'AddonPrivateFiles';
|
||||||
priority = 400;
|
priority = 400;
|
||||||
type = CoreUserDelegateService.TYPE_NEW_PAGE;
|
type = CoreUserProfileHandlerType.LIST_ITEM;
|
||||||
cacheEnabled = true;
|
cacheEnabled = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
--image-size: 120px;
|
--image-size: 120px;
|
||||||
--icon-color: var(--text-color);
|
--icon-color: var(--subdued-text-color);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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,16 @@
|
||||||
|
// (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.
|
||||||
|
|
||||||
|
// Routing.
|
||||||
|
export const CORE_DATAPRIVACY_PAGE_NAME = 'dataprivacy';
|
|
@ -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 {}
|
|
@ -0,0 +1,45 @@
|
||||||
|
// (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 { 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,
|
||||||
|
multi: true,
|
||||||
|
useValue: () => {
|
||||||
|
CoreUserDelegate.registerHandler(CoreDataPrivacyUserHandler.instance);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CoreDataPrivacyModule {}
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"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;
|
||||||
|
};
|
|
@ -0,0 +1,378 @@
|
||||||
|
// (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 { CoreWSError } from '@classes/errors/wserror';
|
||||||
|
import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
|
||||||
|
import { CoreUserSummary } from '@features/user/services/user';
|
||||||
|
import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites';
|
||||||
|
import { CoreWSExternalWarning } from '@services/ws';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to handle data privacy.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreDataPrivacyService {
|
||||||
|
|
||||||
|
static readonly ROOT_CACHE_KEY = 'CoreDataPrivacy:';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if data privacy is enabled on current site.
|
||||||
|
*
|
||||||
|
* @returns Whether data privacy is enabled.
|
||||||
|
*/
|
||||||
|
async isEnabled(): Promise<boolean> {
|
||||||
|
const site = CoreSites.getCurrentSite();
|
||||||
|
|
||||||
|
// Check if the privacy data WS are available in the site.
|
||||||
|
if (!site?.wsAvailable('tool_dataprivacy_get_data_requests')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user can contact the DPO, then data privacy is enabled.
|
||||||
|
const accessInformation = await this.getAccessInformation();
|
||||||
|
|
||||||
|
return accessInformation.cancontactdpo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for data privacy access information WS calls.
|
||||||
|
*
|
||||||
|
* @returns Cache key.
|
||||||
|
*/
|
||||||
|
protected getAccessInformationCacheKey(): string {
|
||||||
|
return CoreDataPrivacyService.ROOT_CACHE_KEY + 'accessInformation';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieving privacy API access (permissions) information for the current user.
|
||||||
|
*
|
||||||
|
* @param options Request options.
|
||||||
|
* @returns Promise resolved with object with access information.
|
||||||
|
* @since 4.4
|
||||||
|
*/
|
||||||
|
async getAccessInformation(
|
||||||
|
options: CoreSitesCommonWSOptions = {},
|
||||||
|
): Promise<CoreDataPrivacyGetAccessInformationWSResponse> {
|
||||||
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
|
||||||
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
cacheKey: this.getAccessInformationCacheKey(),
|
||||||
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
|
};
|
||||||
|
|
||||||
|
return site.read('tool_dataprivacy_get_access_information', undefined, preSets);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidates access information.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @returns Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
protected async invalidateAccessInformation(siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
await site.invalidateWsCacheForKey(this.getAccessInformationCacheKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contact the site Data Protection Officer(s).
|
||||||
|
*
|
||||||
|
* @param message Message to send to the DPO.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @returns Promise resolved with boolean: whether the message was sent.
|
||||||
|
* @since 4.4
|
||||||
|
*/
|
||||||
|
async contactDPO(message: string, siteId?: string): Promise<boolean> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
const params: CoreDataPrivacyContactDPOWSParams = { message };
|
||||||
|
|
||||||
|
const response = await site.write<CoreDataPrivacyContactDPOWSResponse>('tool_dataprivacy_contact_dpo', params);
|
||||||
|
|
||||||
|
if (response.warnings && response.warnings.length) {
|
||||||
|
throw new CoreWSError(response.warnings[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for data requests WS calls.
|
||||||
|
*
|
||||||
|
* @returns Cache key.
|
||||||
|
*/
|
||||||
|
protected getDataRequestsCacheKey(): string {
|
||||||
|
return CoreDataPrivacyService.ROOT_CACHE_KEY + 'datarequests';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the details of a user's data request.
|
||||||
|
*
|
||||||
|
* @param options Request options.
|
||||||
|
* @returns Promise resolved with the data requests.
|
||||||
|
* @since 4.4
|
||||||
|
*/
|
||||||
|
async getDataRequests(
|
||||||
|
options: CoreSitesCommonWSOptions = {},
|
||||||
|
): Promise<CoreDataPrivacyRequest[]> {
|
||||||
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
|
||||||
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
cacheKey: this.getDataRequestsCacheKey(),
|
||||||
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
|
};
|
||||||
|
|
||||||
|
const params: CoreDataPrivacyGetDataRequestsWSParams = {
|
||||||
|
userid: site.getUserId(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response =
|
||||||
|
await site.read<CoreDataPrivacyGetDataRequestsWSResponse>('tool_dataprivacy_get_data_requests', params, preSets);
|
||||||
|
|
||||||
|
return response.requests;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate data requests.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @returns Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateDataRequests(siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
await site.invalidateWsCacheForKey(this.getDataRequestsCacheKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a data request.
|
||||||
|
*
|
||||||
|
* @param type Type of the request.
|
||||||
|
* @param comments Comments for the data request.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @returns Promise resolved when the request is created.
|
||||||
|
* @since 4.4
|
||||||
|
*/
|
||||||
|
async createDataRequest(type: CoreDataPrivacyDataRequestType, comments: string, siteId?: string): Promise<number> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
const params: CoreDataPrivacyCreateDataRequestWSParams = {
|
||||||
|
type,
|
||||||
|
comments,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response =
|
||||||
|
await site.write<CoreDataPrivacyCreateDataRequestWSResponse>('tool_dataprivacy_create_data_request', params);
|
||||||
|
|
||||||
|
if (response.warnings && response.warnings.length) {
|
||||||
|
throw new CoreWSError(response.warnings[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.datarequestid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel the data request made by the user.
|
||||||
|
*
|
||||||
|
* @param requestid ID of the request to cancel.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @returns Promise resolved with boolean: whether the request was canceled.
|
||||||
|
* @since 4.4
|
||||||
|
*/
|
||||||
|
async cancelDataRequest(requestid: number, siteId?: string): Promise<boolean> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
const params: CoreDataPrivacyCancelDataRequestWSParams = { requestid };
|
||||||
|
|
||||||
|
const response =
|
||||||
|
await site.write<CoreDataPrivacyCancelDataRequestWSResponse>('tool_dataprivacy_cancel_data_request', params);
|
||||||
|
|
||||||
|
if (response.warnings && response.warnings.length) {
|
||||||
|
throw new CoreWSError(response.warnings[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate all the data related to data privacy.
|
||||||
|
*/
|
||||||
|
async invalidateAll(): Promise<void> {
|
||||||
|
await Promise.all([
|
||||||
|
this.invalidateAccessInformation(),
|
||||||
|
this.invalidateDataRequests(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the user can cancel a request.
|
||||||
|
*
|
||||||
|
* @param request The request to check.
|
||||||
|
* @returns Whether the user can cancel the request.
|
||||||
|
*/
|
||||||
|
canCancelRequest(request: CoreDataPrivacyRequest): boolean {
|
||||||
|
const cannotCancelStatuses = [
|
||||||
|
CoreDataPrivacyDataRequestStatus.DATAREQUEST_STATUS_COMPLETE,
|
||||||
|
CoreDataPrivacyDataRequestStatus.DATAREQUEST_STATUS_DOWNLOAD_READY,
|
||||||
|
CoreDataPrivacyDataRequestStatus.DATAREQUEST_STATUS_DELETED,
|
||||||
|
CoreDataPrivacyDataRequestStatus.DATAREQUEST_STATUS_EXPIRED,
|
||||||
|
CoreDataPrivacyDataRequestStatus.DATAREQUEST_STATUS_CANCELLED,
|
||||||
|
CoreDataPrivacyDataRequestStatus.DATAREQUEST_STATUS_REJECTED,
|
||||||
|
];
|
||||||
|
|
||||||
|
return !cannotCancelStatuses.includes(request.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CoreDataPrivacy = makeSingleton(CoreDataPrivacyService);
|
||||||
|
|
||||||
|
export enum CoreDataPrivacyDataRequestType {
|
||||||
|
DATAREQUEST_TYPE_EXPORT = 1, // Data export request type.
|
||||||
|
DATAREQUEST_TYPE_DELETE = 2, // Data deletion request type.
|
||||||
|
DATAREQUEST_TYPE_OTHERS = 3, // Other request type. Usually of enquiries to the DPO.
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CoreDataPrivacyDataRequestStatus {
|
||||||
|
DATAREQUEST_STATUS_PENDING = 0, // Newly submitted and we haven't yet started finding out where they have data.
|
||||||
|
DATAREQUEST_STATUS_PREPROCESSING = 1, // Newly submitted and we have started to find the location of data.
|
||||||
|
DATAREQUEST_STATUS_AWAITING_APPROVAL = 2, // Metadata ready and awaiting review and approval by the Data Protection officer.
|
||||||
|
DATAREQUEST_STATUS_APPROVED = 3, // Request approved and will be processed soon.
|
||||||
|
DATAREQUEST_STATUS_PROCESSING = 4, // The request is now being processed.
|
||||||
|
DATAREQUEST_STATUS_COMPLETE = 5, // Information/other request completed.
|
||||||
|
DATAREQUEST_STATUS_CANCELLED = 6, // Data request cancelled by the user.
|
||||||
|
DATAREQUEST_STATUS_REJECTED = 7, // Data request rejected by the DPO.
|
||||||
|
DATAREQUEST_STATUS_DOWNLOAD_READY = 8, // Data request download ready.
|
||||||
|
DATAREQUEST_STATUS_EXPIRED = 9, // Data request expired.
|
||||||
|
DATAREQUEST_STATUS_DELETED = 10, // Data delete request completed, account is removed.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned by tool_dataprivacy_get_access_information WS.
|
||||||
|
*/
|
||||||
|
export type CoreDataPrivacyGetAccessInformationWSResponse = {
|
||||||
|
cancontactdpo: boolean; // Can contact dpo.
|
||||||
|
canmanagedatarequests: boolean; // Can manage data requests.
|
||||||
|
cancreatedatadownloadrequest: boolean; // Can create data download request for self.
|
||||||
|
cancreatedatadeletionrequest: boolean; // Can create data deletion request for self.
|
||||||
|
hasongoingdatadownloadrequest: boolean; // Has ongoing data download request.
|
||||||
|
hasongoingdatadeletionrequest: boolean; // Has ongoing data deletion request.
|
||||||
|
warnings?: CoreWSExternalWarning[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of tool_dataprivacy_contact_dpo WS.
|
||||||
|
*/
|
||||||
|
type CoreDataPrivacyContactDPOWSParams = {
|
||||||
|
message: string; // The user's message to the Data Protection Officer(s).
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned by tool_dataprivacy_contact_dpo WS.
|
||||||
|
*/
|
||||||
|
type CoreDataPrivacyContactDPOWSResponse = {
|
||||||
|
result: boolean; // The processing result
|
||||||
|
warnings?: CoreWSExternalWarning[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of tool_dataprivacy_create_data_request WS.
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned by tool_dataprivacy_create_data_request WS.
|
||||||
|
*/
|
||||||
|
type CoreDataPrivacyCreateDataRequestWSResponse = {
|
||||||
|
datarequestid: number; // The id of the created data request.
|
||||||
|
warnings?: CoreWSExternalWarning[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of tool_dataprivacy_cancel_data_request WS.
|
||||||
|
*/
|
||||||
|
type CoreDataPrivacyCancelDataRequestWSParams = {
|
||||||
|
requestid: number; // The request ID
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned by tool_dataprivacy_cancel_data_request WS.
|
||||||
|
*/
|
||||||
|
type CoreDataPrivacyCancelDataRequestWSResponse = {
|
||||||
|
result: boolean; // The processing result
|
||||||
|
warnings?: CoreWSExternalWarning[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of tool_dataprivacy_get_data_requests WS.
|
||||||
|
*/
|
||||||
|
type CoreDataPrivacyGetDataRequestsWSParams = {
|
||||||
|
userid?: number; // The id of the user to get the data requests for. Empty for all users.
|
||||||
|
statuses?: CoreDataPrivacyDataRequestStatus[]; // The statuses of the data requests to get.
|
||||||
|
// 0 for pending 1 preprocessing, 2 awaiting approval, 3 approved,
|
||||||
|
// 4 processed, 5 completed, 6 cancelled, 7 rejected.
|
||||||
|
types?: number[]; // The types of the data requests to get. 1 for export, 2 for data deletion.
|
||||||
|
creationmethods?: number[]; // The creation methods of the data requests to get. 0 for manual, 1 for automatic.
|
||||||
|
sort?: string; // The field to sort the data requests by.
|
||||||
|
limitfrom?: number; // The number to start getting the data requests from.
|
||||||
|
limitnum?: number; // The number of data requests to get.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned by tool_dataprivacy_get_data_requests WS.
|
||||||
|
*/
|
||||||
|
type CoreDataPrivacyGetDataRequestsWSResponse = {
|
||||||
|
requests: CoreDataPrivacyRequest[]; // The data requests.
|
||||||
|
warnings?: CoreWSExternalWarning[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data for the dataprivacy request.
|
||||||
|
*/
|
||||||
|
export type CoreDataPrivacyRequest = {
|
||||||
|
type: CoreDataPrivacyDataRequestType; // Type.
|
||||||
|
comments: string; // Comments.
|
||||||
|
commentsformat: number; // Commentsformat.
|
||||||
|
userid: number; // Userid.
|
||||||
|
requestedby: number; // Requestedby.
|
||||||
|
status: CoreDataPrivacyDataRequestStatus; // Status.
|
||||||
|
dpo: number; // Dpo.
|
||||||
|
dpocomment: string; // Dpocomment.
|
||||||
|
dpocommentformat: number; // Dpocommentformat.
|
||||||
|
systemapproved: boolean; // Systemapproved.
|
||||||
|
creationmethod: number; // Creationmethod.
|
||||||
|
id: number; // Id.
|
||||||
|
timecreated: number; // Timecreated.
|
||||||
|
timemodified: number; // Timemodified.
|
||||||
|
usermodified: number; // Usermodified.
|
||||||
|
foruser: CoreUserSummary; // The user the request is for.
|
||||||
|
requestedbyuser: CoreUserSummary; // The user who requested the data.
|
||||||
|
dpouser?: CoreUserSummary; // The user who processed the request.
|
||||||
|
messagehtml?: string; // Messagehtml.
|
||||||
|
typename: string; // Typename.
|
||||||
|
typenameshort: string; // Typenameshort.
|
||||||
|
statuslabel: string; // Statuslabel.
|
||||||
|
statuslabelclass: string; // Statuslabelclass.
|
||||||
|
canreview?: boolean; // Canreview.
|
||||||
|
approvedeny?: boolean; // Approvedeny.
|
||||||
|
allowfiltering?: boolean; // Allowfiltering.
|
||||||
|
canmarkcomplete?: boolean; // Canmarkcomplete.
|
||||||
|
};
|
|
@ -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 {
|
||||||
|
CoreUserProfileHandlerType,
|
||||||
|
CoreUserProfileHandler,
|
||||||
|
CoreUserProfileHandlerData,
|
||||||
|
} from '@features/user/services/user-delegate';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { CoreDataPrivacy } from '../dataprivacy';
|
||||||
|
import { CORE_DATAPRIVACY_PAGE_NAME } from '@features/dataprivacy/constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to visualize custom reports.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreDataPrivacyUserHandlerService implements CoreUserProfileHandler {
|
||||||
|
|
||||||
|
protected pageName = CORE_DATAPRIVACY_PAGE_NAME;
|
||||||
|
|
||||||
|
type = CoreUserProfileHandlerType.LIST_ACCOUNT_ITEM;
|
||||||
|
name = 'CoreDataPrivacyDelegate';
|
||||||
|
priority = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async isEnabled(): Promise<boolean> {
|
||||||
|
return await CoreDataPrivacy.isEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getDisplayData(): CoreUserProfileHandlerData {
|
||||||
|
return {
|
||||||
|
class: 'core-data-privacy',
|
||||||
|
icon: 'fas-user-shield',
|
||||||
|
title: 'core.dataprivacy.pluginname',
|
||||||
|
action: async (event): Promise<void> => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
await CoreNavigator.navigateToSitePath(this.pageName);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CoreDataPrivacyUserHandler = makeSingleton(CoreDataPrivacyUserHandlerService);
|
|
@ -0,0 +1,27 @@
|
||||||
|
@core_dataprivacy @app @javascript @lms_from4.4
|
||||||
|
Feature: Contact the privacy officer
|
||||||
|
As a user
|
||||||
|
In order to reach out to the site's privacy officer
|
||||||
|
I need to be able to contact the site's privacy officer in Moodle
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given the following "users" exist:
|
||||||
|
| username | firstname | lastname | email |
|
||||||
|
| student1 | Student | 1 | s1@example.com |
|
||||||
|
|
||||||
|
Scenario: Contacting the privacy officer
|
||||||
|
Given the following config values are set as admin:
|
||||||
|
| contactdataprotectionofficer | 1 | tool_dataprivacy |
|
||||||
|
When I entered the app as "student1"
|
||||||
|
And I press the user menu button in the app
|
||||||
|
And I press "Data privacy" in the app
|
||||||
|
And I press "Contact the privacy officer" in the app
|
||||||
|
And I set the field "Message" to "Hello DPO!" in the app
|
||||||
|
And I press "Send" in the app
|
||||||
|
Then I should find "Your request has been submitted to the privacy officer" in the app
|
||||||
|
And I should find "Hello DPO!" in the app
|
||||||
|
|
||||||
|
Scenario: Contacting the privacy officer when not enabled
|
||||||
|
When I entered the app as "student1"
|
||||||
|
And I press the user menu button in the app
|
||||||
|
Then I should not find ""Data privacy" in the app
|
|
@ -0,0 +1,62 @@
|
||||||
|
@core_dataprivacy @app @javascript @lms_from4.4
|
||||||
|
Feature: Data export and delete from the privacy API
|
||||||
|
In order to export or delete data for users and meet legal requirements
|
||||||
|
I need to be able to request my data data be exported or deleted
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given the following "users" exist:
|
||||||
|
| username | firstname | lastname |
|
||||||
|
| victim | Victim User | 1 |
|
||||||
|
And the following config values are set as admin:
|
||||||
|
| contactdataprotectionofficer | 1 | tool_dataprivacy |
|
||||||
|
| privacyrequestexpiry | 55 | tool_dataprivacy |
|
||||||
|
| dporoles | 1 | tool_dataprivacy |
|
||||||
|
|
||||||
|
Scenario: As a student, request deletion of account and data
|
||||||
|
Given I entered the app as "victim"
|
||||||
|
And I press the user menu button in the app
|
||||||
|
And I press "Data privacy" in the app
|
||||||
|
And I press "New request" in the app
|
||||||
|
And I press "Delete all of my personal data" in the app
|
||||||
|
And I press "Send" in the app
|
||||||
|
Then I should find "Delete all of my personal data" in the app
|
||||||
|
And I should find "Awaiting approval" near "Delete all of my personal data" in the app
|
||||||
|
|
||||||
|
Scenario: As a student, I cannot create data deletion request unless I have permission.
|
||||||
|
Given the following "permission overrides" exist:
|
||||||
|
| capability | permission | role | contextlevel | reference |
|
||||||
|
| tool/dataprivacy:requestdelete | Prevent | user | System | |
|
||||||
|
And I entered the app as "victim"
|
||||||
|
And I press the user menu button in the app
|
||||||
|
And I press "Data privacy" in the app
|
||||||
|
When I press "New request" in the app
|
||||||
|
Then I should not find "Delete all of my personal data" in the app
|
||||||
|
|
||||||
|
Scenario: As a student, request data export and then see the status
|
||||||
|
Given I entered the app as "victim"
|
||||||
|
And I press the user menu button in the app
|
||||||
|
And I press "Data privacy" in the app
|
||||||
|
When I press "New request" in the app
|
||||||
|
And I press "Export all of my personal data" in the app
|
||||||
|
And I set the field "Comments" to "Export my data" in the app
|
||||||
|
And I press "Send" in the app
|
||||||
|
Then I should find "Export all of my personal data" in the app
|
||||||
|
And I should find "Awaiting approval" near "Export all of my personal data" in the app
|
||||||
|
And I should find "Export my data" near "Export all of my personal data" in the app
|
||||||
|
|
||||||
|
# The next step allows to naavigate to site administration
|
||||||
|
When I change viewport size to "1200x640" in the app
|
||||||
|
And I open a browser tab with url "$WWWROOT"
|
||||||
|
And I log in as "admin"
|
||||||
|
And I navigate to "Users > Privacy and policies > Data requests" in site administration
|
||||||
|
And I open the action menu in "Victim User 1" "table_row"
|
||||||
|
And I follow "Approve request"
|
||||||
|
And I press "Approve request"
|
||||||
|
|
||||||
|
And I switch back to the app
|
||||||
|
And I pull to refresh in the app
|
||||||
|
Then I should find "Approved" near "Export all of my personal data" in the app
|
||||||
|
When I run all adhoc tasks
|
||||||
|
And I pull to refresh in the app
|
||||||
|
And I should find "Download ready" near "Export all of my personal data" in the app
|
||||||
|
# TODO: Add download link and test it.
|
|
@ -0,0 +1,24 @@
|
||||||
|
@core_dataprivacy @app @javascript @lms_from4.4
|
||||||
|
Feature: Manage my own data requests
|
||||||
|
In order to manage my own data requests
|
||||||
|
As a user
|
||||||
|
I need to be able to view and cancel all my data requests
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given the following "users" exist:
|
||||||
|
| username | firstname | lastname | email |
|
||||||
|
| student1 | Student | 1 | s1@example.com |
|
||||||
|
And the following config values are set as admin:
|
||||||
|
| contactdataprotectionofficer | 1 | tool_dataprivacy |
|
||||||
|
|
||||||
|
Scenario: Cancel my own data request
|
||||||
|
Given I entered the app as "student1"
|
||||||
|
And I press the user menu button in the app
|
||||||
|
And I press "Data privacy" in the app
|
||||||
|
And I press "Contact the privacy officer" in the app
|
||||||
|
And I set the field "Message" to "Hello DPO!" in the app
|
||||||
|
And I press "Send" in the app
|
||||||
|
Then I should find "Your request has been submitted to the privacy officer" in the app
|
||||||
|
When I press "Cancel" near "Hello DPO!" in the app
|
||||||
|
And I press "Cancel request" "button" in the app
|
||||||
|
Then I should find "Cancelled" near "Hello DPO!" in the app
|
|
@ -24,8 +24,6 @@ export const CORE_EDITOR_SERVICES: Type<unknown>[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
|
||||||
],
|
|
||||||
imports: [
|
imports: [
|
||||||
CoreEditorComponentsModule,
|
CoreEditorComponentsModule,
|
||||||
],
|
],
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { CoreCommentsModule } from './comments/comments.module';
|
||||||
import { CoreContentLinksModule } from './contentlinks/contentlinks.module';
|
import { CoreContentLinksModule } from './contentlinks/contentlinks.module';
|
||||||
import { CoreCourseModule } from './course/course.module';
|
import { CoreCourseModule } from './course/course.module';
|
||||||
import { CoreCoursesModule } from './courses/courses.module';
|
import { CoreCoursesModule } from './courses/courses.module';
|
||||||
|
import { CoreDataPrivacyModule } from './dataprivacy/dataprivacy.module';
|
||||||
import { CoreEditorModule } from './editor/editor.module';
|
import { CoreEditorModule } from './editor/editor.module';
|
||||||
import { CoreEmulatorModule } from './emulator/emulator.module';
|
import { CoreEmulatorModule } from './emulator/emulator.module';
|
||||||
import { CoreEnrolModule } from './enrol/enrol.module';
|
import { CoreEnrolModule } from './enrol/enrol.module';
|
||||||
|
@ -53,6 +54,7 @@ import { CoreReportBuilderModule } from './reportbuilder/reportbuilder.module';
|
||||||
CoreContentLinksModule,
|
CoreContentLinksModule,
|
||||||
CoreCourseModule,
|
CoreCourseModule,
|
||||||
CoreCoursesModule,
|
CoreCoursesModule,
|
||||||
|
CoreDataPrivacyModule,
|
||||||
CoreEditorModule,
|
CoreEditorModule,
|
||||||
CoreEnrolModule,
|
CoreEnrolModule,
|
||||||
CoreFileUploaderModule,
|
CoreFileUploaderModule,
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<core-loading [hideUntil]="loaded">
|
<core-loading [hideUntil]="loaded">
|
||||||
<core-empty-box *ngIf="!rows.length" icon="fas-chart-bar" [message]="'core.grades.nogradesreturned' | translate" />
|
<core-empty-box *ngIf="!rows.length" icon="fas-chart-bar" [message]="'core.grades.nogradesreturned' | translate" />
|
||||||
<div *ngIf="rows.length" class="core-grades-container">
|
<div *ngIf="rows.length" class="core-grades-container">
|
||||||
<table class="core-grades-table" [class.summary]="showSummary">
|
<table class="core-table core-grades-table" [class.summary]="showSummary">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th *ngFor="let column of columns" id="{{column.name}}" class="ion-text-start"
|
<th *ngFor="let column of columns" id="{{column.name}}" class="ion-text-start"
|
||||||
|
|
|
@ -1,67 +1,22 @@
|
||||||
@use "theme/globals" as *;
|
@use "theme/globals" as *;
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
--header-background: var(--white);
|
|
||||||
--odd-cell-background: var(--light);
|
|
||||||
--odd-cell-hover: var(--gray-200);
|
|
||||||
--even-cell-background: var(--white);
|
|
||||||
--even-cell-hover: var(--light);
|
|
||||||
--icon-color: var(--gray-500);
|
--icon-color: var(--gray-500);
|
||||||
--border-color: var(--stroke);
|
--core-table-border-color: var(--stroke);
|
||||||
|
|
||||||
.odd {
|
|
||||||
--cell-background: var(--odd-cell-background);
|
|
||||||
--cell-hover: var(--odd-cell-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.even {
|
|
||||||
--cell-background: var(--even-cell-background);
|
|
||||||
--cell-hover: var(--even-cell-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:host-context(html.dark) {
|
:host-context(html.dark) {
|
||||||
--header-background: var(--gray-900);
|
|
||||||
--odd-cell-background: var(--gray-800);
|
|
||||||
--odd-cell-hover: var(--gray-600);
|
|
||||||
--even-cell-background: var(--gray-900);
|
|
||||||
--even-cell-hover: var(--gray-700);
|
|
||||||
--icon-color: var(--gray-200);
|
--icon-color: var(--gray-200);
|
||||||
}
|
}
|
||||||
|
|
||||||
.core-grades-table {
|
table.core-table.core-grades-table {
|
||||||
border-collapse: collapse;
|
|
||||||
line-height: 20px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 16px;
|
margin: 0px;
|
||||||
color: var(--ion-text-color);
|
|
||||||
|
|
||||||
tr {
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
|
||||||
@include padding(8px, 8px, 8px, null);
|
|
||||||
vertical-align: top;
|
|
||||||
white-space: normal;
|
|
||||||
text-align: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead th {
|
|
||||||
vertical-align: bottom;
|
|
||||||
font-weight: bold;
|
|
||||||
background-color: var(--header-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
thead #gradeitem {
|
thead #gradeitem {
|
||||||
@include padding(null, null, null, 24px);
|
@include padding(null, null, null, 24px);
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody th {
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr.core-bold th {
|
tbody tr.core-bold th {
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
}
|
}
|
||||||
|
@ -70,9 +25,13 @@
|
||||||
@include padding(null, null, null, 4px);
|
@include padding(null, null, null, 4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
@include padding(8px, 8px, 8px, 0px);
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
.core-grades-table-gradeitem {
|
.core-grades-table-gradeitem {
|
||||||
@include padding(null, null, null, 4px);
|
@include padding(null, null, null, 4px);
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
&.column-itemname {
|
&.column-itemname {
|
||||||
@include padding(null, null, null, 0);
|
@include padding(null, null, null, 0);
|
||||||
|
@ -90,7 +49,6 @@
|
||||||
--filter: var(--module-icon-filter);
|
--filter: var(--module-icon-filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ion-icon {
|
ion-icon {
|
||||||
color: var(--icon-color);
|
color: var(--icon-color);
|
||||||
}
|
}
|
||||||
|
@ -130,21 +88,6 @@
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ion-no-border {
|
|
||||||
border: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dimmed_text,
|
|
||||||
.hidden {
|
|
||||||
opacity: .7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.odd, .even {
|
|
||||||
td, th, th[aria-current="page"] {
|
|
||||||
background-color: var(--cell-background);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.core-grades-grade-clickable {
|
.core-grades-grade-clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { CoreGrades } from '@features/grades/services/grades';
|
||||||
import { CoreUserProfile } from '@features/user/services/user';
|
import { CoreUserProfile } from '@features/user/services/user';
|
||||||
import {
|
import {
|
||||||
CoreUserDelegateContext,
|
CoreUserDelegateContext,
|
||||||
CoreUserDelegateService ,
|
CoreUserProfileHandlerType ,
|
||||||
CoreUserProfileHandler,
|
CoreUserProfileHandler,
|
||||||
CoreUserProfileHandlerData,
|
CoreUserProfileHandlerData,
|
||||||
} from '@features/user/services/user-delegate';
|
} from '@features/user/services/user-delegate';
|
||||||
|
@ -38,7 +38,7 @@ export class CoreGradesUserHandlerService implements CoreUserProfileHandler {
|
||||||
|
|
||||||
name = 'CoreGrades'; // This name doesn't match any disabled feature, they'll be checked in isEnabledForContext.
|
name = 'CoreGrades'; // This name doesn't match any disabled feature, they'll be checked in isEnabledForContext.
|
||||||
priority = 500;
|
priority = 500;
|
||||||
type = CoreUserDelegateService.TYPE_NEW_PAGE;
|
type = CoreUserProfileHandlerType.LIST_ITEM;
|
||||||
cacheEnabled = true;
|
cacheEnabled = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Binary file not shown.
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
@ -40,7 +40,7 @@
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item button class="core-usermenu-handler ion-text-wrap" *ngIf="siteInfo" lines="full" (click)="openUserProfile($event)"
|
<ion-item button class="core-usermenu-profile ion-text-wrap" *ngIf="siteInfo" lines="full" (click)="openUserProfile($event)"
|
||||||
[detail]="true" [attr.aria-label]="'core.user.profile' | translate">
|
[detail]="true" [attr.aria-label]="'core.user.profile' | translate">
|
||||||
<core-user-avatar [site]="siteInfo" [userId]="siteInfo.userid" [linkProfile]="false" slot="start" />
|
<core-user-avatar [site]="siteInfo" [userId]="siteInfo.userid" [linkProfile]="false" slot="start" />
|
||||||
<ion-label>
|
<ion-label>
|
||||||
|
@ -48,12 +48,7 @@
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ion-item class="ion-text-center" *ngIf="(!handlers || !handlers.length) && !handlersLoaded">
|
@if (handlers.length + accountHandlers.length > 0) {
|
||||||
<ion-label>
|
|
||||||
<ion-spinner [attr.aria-label]="'core.loading' | translate" />
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
|
|
||||||
<ion-item button *ngFor="let handler of handlers" class="ion-text-wrap" (click)="handlerClicked($event, handler)"
|
<ion-item button *ngFor="let handler of handlers" class="ion-text-wrap" (click)="handlerClicked($event, handler)"
|
||||||
[ngClass]="['core-user-menu-handler', handler.class || '']" [hidden]="handler.hidden"
|
[ngClass]="['core-user-menu-handler', handler.class || '']" [hidden]="handler.hidden"
|
||||||
[attr.aria-label]="handler.title | translate" [detail]="true">
|
[attr.aria-label]="handler.title | translate" [detail]="true">
|
||||||
|
@ -70,8 +65,35 @@
|
||||||
<ion-spinner slot="end" *ngIf="handler.showBadge && handler.loading" [attr.aria-label]="'core.loading' | translate" />
|
<ion-spinner slot="end" *ngIf="handler.showBadge && handler.loading" [attr.aria-label]="'core.loading' | translate" />
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item button *ngFor="let handler of accountHandlers; let first = first" class="ion-text-wrap"
|
||||||
|
(click)="handlerClicked($event, handler)"
|
||||||
|
[ngClass]="['core-user-account-menu-handler', handler.class || '', first ? 'core-user-menu-separator' : '']"
|
||||||
|
[hidden]="handler.hidden" [attr.aria-label]="handler.title | translate" [detail]="true">
|
||||||
|
<ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true" />
|
||||||
|
<ion-label>
|
||||||
|
<p class="item-heading">{{ handler.title | translate }}</p>
|
||||||
|
</ion-label>
|
||||||
|
<ion-badge slot="end" *ngIf="handler.showBadge" [hidden]="handler.loading || !handler.badge" aria-hidden="true">
|
||||||
|
{{handler.badge}}
|
||||||
|
</ion-badge>
|
||||||
|
<span *ngIf="handler.showBadge && handler.badge && handler.badgeA11yText" class="sr-only">
|
||||||
|
{{ handler.badgeA11yText | translate: {$a : handler.badge } }}
|
||||||
|
</span>
|
||||||
|
<ion-spinner slot="end" *ngIf="handler.showBadge && handler.loading" [attr.aria-label]="'core.loading' | translate" />
|
||||||
|
</ion-item>
|
||||||
|
} @else {
|
||||||
|
<ion-item class="ion-text-center">
|
||||||
|
<ion-label>
|
||||||
|
<ion-spinner [attr.aria-label]="'core.loading' | translate" />
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<ion-item button (click)="openPreferences($event)" [attr.aria-label]="'core.settings.preferences' | translate" [detail]="true"
|
<ion-item button (click)="openPreferences($event)" [attr.aria-label]="'core.settings.preferences' | translate" [detail]="true"
|
||||||
class="core-user-menu-preferences">
|
class="core-user-menu-preferences core-user-menu-separator">
|
||||||
<ion-icon name="fas-wrench" slot="start" aria-hidden="true" />
|
<ion-icon name="fas-wrench" slot="start" aria-hidden="true" />
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<p class="item-heading">{{ 'core.settings.preferences' | translate }}</p>
|
<p class="item-heading">{{ 'core.settings.preferences' | translate }}</p>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
@use "theme/globals" as *;
|
@use "theme/globals" as *;
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
.core-user-menu-preferences {
|
.core-user-menu-separator {
|
||||||
--inner-border-width: 0;
|
--inner-border-width: 0;
|
||||||
--border-width: 1px 0 0 0;
|
--border-width: 1px 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,12 +25,13 @@ import { CoreUser, CoreUserProfile } from '@features/user/services/user';
|
||||||
import {
|
import {
|
||||||
CoreUserProfileHandlerData,
|
CoreUserProfileHandlerData,
|
||||||
CoreUserDelegate,
|
CoreUserDelegate,
|
||||||
CoreUserDelegateService,
|
CoreUserProfileHandlerType,
|
||||||
CoreUserDelegateContext,
|
CoreUserDelegateContext,
|
||||||
} from '@features/user/services/user-delegate';
|
} from '@features/user/services/user-delegate';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { ModalController, Translate } from '@singletons';
|
import { ModalController, Translate } from '@singletons';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
|
@ -52,6 +53,7 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy {
|
||||||
siteUrl?: string;
|
siteUrl?: string;
|
||||||
displaySiteUrl = false;
|
displaySiteUrl = false;
|
||||||
handlers: CoreUserProfileHandlerData[] = [];
|
handlers: CoreUserProfileHandlerData[] = [];
|
||||||
|
accountHandlers: CoreUserProfileHandlerData[] = [];
|
||||||
handlersLoaded = false;
|
handlersLoaded = false;
|
||||||
user?: CoreUserProfile;
|
user?: CoreUserProfile;
|
||||||
displaySwitchAccount = true;
|
displaySwitchAccount = true;
|
||||||
|
@ -76,37 +78,48 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
this.loadSiteLogo(currentSite);
|
this.loadSiteLogo(currentSite);
|
||||||
|
|
||||||
// Load the handlers.
|
if (!this.siteInfo) {
|
||||||
if (this.siteInfo) {
|
return;
|
||||||
try {
|
|
||||||
this.user = await CoreUser.getProfile(this.siteInfo.userid);
|
|
||||||
} catch {
|
|
||||||
this.user = {
|
|
||||||
id: this.siteInfo.userid,
|
|
||||||
fullname: this.siteInfo.fullname,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.subscription = CoreUserDelegate.getProfileHandlersFor(this.user, CoreUserDelegateContext.USER_MENU)
|
|
||||||
.subscribe((handlers) => {
|
|
||||||
if (!handlers || !this.user) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newHandlers = handlers
|
|
||||||
.filter((handler) => handler.type === CoreUserDelegateService.TYPE_NEW_PAGE)
|
|
||||||
.map((handler) => handler.data);
|
|
||||||
|
|
||||||
// Only update handlers if they have changed, to prevent a blink effect.
|
|
||||||
if (newHandlers.length !== this.handlers.length ||
|
|
||||||
JSON.stringify(newHandlers) !== JSON.stringify(this.handlers)) {
|
|
||||||
this.handlers = newHandlers;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.handlersLoaded = CoreUserDelegate.areHandlersLoaded(this.user.id, CoreUserDelegateContext.USER_MENU);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load the handlers.
|
||||||
|
try {
|
||||||
|
this.user = await CoreUser.getProfile(this.siteInfo.userid);
|
||||||
|
} catch {
|
||||||
|
this.user = {
|
||||||
|
id: this.siteInfo.userid,
|
||||||
|
fullname: this.siteInfo.fullname,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subscription = CoreUserDelegate.getProfileHandlersFor(this.user, CoreUserDelegateContext.USER_MENU)
|
||||||
|
.subscribe((handlers) => {
|
||||||
|
if (!handlers.length || !this.user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newHandlers = handlers
|
||||||
|
.filter((handler) => handler.type === CoreUserProfileHandlerType.LIST_ITEM)
|
||||||
|
.map((handler) => handler.data);
|
||||||
|
|
||||||
|
// Only update handlers if they have changed, to prevent a blink effect.
|
||||||
|
if (newHandlers.length !== this.handlers.length ||
|
||||||
|
JSON.stringify(newHandlers) !== JSON.stringify(this.handlers)) {
|
||||||
|
this.handlers = newHandlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
newHandlers = handlers
|
||||||
|
.filter((handler) => handler.type === CoreUserProfileHandlerType.LIST_ACCOUNT_ITEM)
|
||||||
|
.map((handler) => handler.data);
|
||||||
|
|
||||||
|
// Only update handlers if they have changed, to prevent a blink effect.
|
||||||
|
if (newHandlers.length !== this.handlers.length ||
|
||||||
|
JSON.stringify(newHandlers) !== JSON.stringify(this.handlers)) {
|
||||||
|
this.accountHandlers = newHandlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handlersLoaded = CoreUserDelegate.areHandlersLoaded(this.user.id, CoreUserDelegateContext.USER_MENU);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -123,15 +136,9 @@ export class CoreMainMenuUserMenuComponent implements OnInit, OnDestroy {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const siteConfig = await CoreUtils.ignoreErrors(currentSite.getPublicConfig());
|
||||||
const siteConfig = await currentSite.getPublicConfig();
|
this.siteLogo = currentSite.getLogoUrl(siteConfig);
|
||||||
|
this.siteLogoLoaded = true;
|
||||||
this.siteLogo = currentSite.getLogoUrl(siteConfig);
|
|
||||||
} catch {
|
|
||||||
// Ignore errors.
|
|
||||||
} finally {
|
|
||||||
this.siteLogoLoaded = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="!isCardLayout">
|
<ng-container *ngIf="!isCardLayout">
|
||||||
<table>
|
<table class="core-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th *ngFor="let header of state.report.data.headers">
|
<th *ngFor="let header of state.report.data.headers">
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody class="auto-striped">
|
||||||
<tr *ngFor="let row of state.report.data.rows">
|
<tr *ngFor="let row of state.report.data.rows">
|
||||||
<td *ngFor="let column of row.columns">
|
<td *ngFor="let column of row.columns">
|
||||||
<core-format-text *ngIf="isString(column); else notText" [text]="column" [contextLevel]="source$ | async"
|
<core-format-text *ngIf="isString(column); else notText" [text]="column" [contextLevel]="source$ | async"
|
||||||
|
|
|
@ -1,9 +1,4 @@
|
||||||
@use "theme/globals" as *;
|
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
--header-background: var(--white);
|
|
||||||
--border-color: var(--stroke);
|
|
||||||
|
|
||||||
.report-title {
|
.report-title {
|
||||||
ion-item {
|
ion-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -11,32 +6,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
width: 98%;
|
|
||||||
margin: 1em auto;
|
|
||||||
border-collapse: collapse;
|
|
||||||
color: var(--ion-text-color);
|
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
display: block;
|
|
||||||
|
|
||||||
tbody {
|
|
||||||
display: table;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
background-color: var(--header-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
tr {
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
|
|
||||||
&:nth-child(even) {
|
|
||||||
background: var(--light);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
th, td {
|
||||||
@include padding(8px, 8px, 8px, null);
|
|
||||||
text-align: start;
|
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,11 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { CoreUserDelegateService, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@features/user/services/user-delegate';
|
import {
|
||||||
|
CoreUserProfileHandlerType,
|
||||||
|
CoreUserProfileHandler,
|
||||||
|
CoreUserProfileHandlerData,
|
||||||
|
} from '@features/user/services/user-delegate';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
import { CoreReportBuilder } from '../reportbuilder';
|
import { CoreReportBuilder } from '../reportbuilder';
|
||||||
|
@ -26,7 +30,7 @@ export class CoreReportBuilderHandlerService implements CoreUserProfileHandler {
|
||||||
|
|
||||||
static readonly PAGE_NAME = 'reportbuilder';
|
static readonly PAGE_NAME = 'reportbuilder';
|
||||||
|
|
||||||
type = CoreUserDelegateService.TYPE_NEW_PAGE;
|
type = CoreUserProfileHandlerType.LIST_ITEM;
|
||||||
cacheEnabled = true;
|
cacheEnabled = true;
|
||||||
name = 'CoreReportBuilderDelegate';
|
name = 'CoreReportBuilderDelegate';
|
||||||
priority = 350;
|
priority = 350;
|
||||||
|
|
|
@ -22,7 +22,7 @@ import {
|
||||||
import { CoreUserProfile } from '@features/user/services/user';
|
import { CoreUserProfile } from '@features/user/services/user';
|
||||||
import {
|
import {
|
||||||
CoreUserDelegateContext,
|
CoreUserDelegateContext,
|
||||||
CoreUserDelegateService,
|
CoreUserProfileHandlerType,
|
||||||
CoreUserProfileHandler,
|
CoreUserProfileHandler,
|
||||||
CoreUserProfileHandlerData,
|
CoreUserProfileHandlerData,
|
||||||
} from '@features/user/services/user-delegate';
|
} from '@features/user/services/user-delegate';
|
||||||
|
@ -36,7 +36,7 @@ import { CoreSitePluginsBaseHandler } from './base-handler';
|
||||||
export class CoreSitePluginsUserProfileHandler extends CoreSitePluginsBaseHandler implements CoreUserProfileHandler {
|
export class CoreSitePluginsUserProfileHandler extends CoreSitePluginsBaseHandler implements CoreUserProfileHandler {
|
||||||
|
|
||||||
priority: number;
|
priority: number;
|
||||||
type: string;
|
type: CoreUserProfileHandlerType;
|
||||||
|
|
||||||
protected updatingDefer?: CorePromisedValue<void>;
|
protected updatingDefer?: CorePromisedValue<void>;
|
||||||
|
|
||||||
|
@ -51,9 +51,10 @@ export class CoreSitePluginsUserProfileHandler extends CoreSitePluginsBaseHandle
|
||||||
|
|
||||||
this.priority = handlerSchema.priority || 0;
|
this.priority = handlerSchema.priority || 0;
|
||||||
|
|
||||||
// Only support TYPE_COMMUNICATION and TYPE_NEW_PAGE.
|
// Only support LIST_ITEM and BUTTON.
|
||||||
this.type = handlerSchema.type != CoreUserDelegateService.TYPE_COMMUNICATION ?
|
this.type = !handlerSchema.type || handlerSchema.type === CoreUserProfileHandlerType.LIST_ACCOUNT_ITEM
|
||||||
CoreUserDelegateService.TYPE_NEW_PAGE : CoreUserDelegateService.TYPE_COMMUNICATION;
|
? CoreUserProfileHandlerType.LIST_ITEM
|
||||||
|
: handlerSchema.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -33,6 +33,7 @@ import { CorePromisedValue } from '@classes/promised-value';
|
||||||
import { CorePlatform } from '@services/platform';
|
import { CorePlatform } from '@services/platform';
|
||||||
import { CoreEnrolAction, CoreEnrolInfoIcon } from '@features/enrol/services/enrol-delegate';
|
import { CoreEnrolAction, CoreEnrolInfoIcon } from '@features/enrol/services/enrol-delegate';
|
||||||
import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
|
import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site';
|
||||||
|
import { CoreUserProfileHandlerType } from '@features/user/services/user-delegate';
|
||||||
|
|
||||||
const ROOT_CACHE_KEY = 'CoreSitePlugins:';
|
const ROOT_CACHE_KEY = 'CoreSitePlugins:';
|
||||||
|
|
||||||
|
@ -907,7 +908,7 @@ export type CoreSitePluginsUserHandlerData = CoreSitePluginsHandlerCommonData &
|
||||||
icon?: string;
|
icon?: string;
|
||||||
class?: string;
|
class?: string;
|
||||||
};
|
};
|
||||||
type?: string;
|
type?: CoreUserProfileHandlerType;
|
||||||
priority?: number;
|
priority?: number;
|
||||||
ptrenabled?: boolean;
|
ptrenabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
|
@ -33,11 +33,10 @@
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<div class="core-user-communication-handlers"
|
<div class="core-user-communication-handlers" *ngIf="(buttonHandlers && buttonHandlers.length) || isLoadingHandlers">
|
||||||
*ngIf="(communicationHandlers && communicationHandlers.length) || isLoadingHandlers">
|
<ion-item *ngIf="buttonHandlers && buttonHandlers.length">
|
||||||
<ion-item *ngIf="communicationHandlers && communicationHandlers.length">
|
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<ion-button *ngFor="let handler of communicationHandlers" expand="block" size="default"
|
<ion-button *ngFor="let handler of buttonHandlers" expand="block" size="default"
|
||||||
[ngClass]="['core-user-profile-handler', handler.class || '']" (click)="handlerClicked($event, handler)"
|
[ngClass]="['core-user-profile-handler', handler.class || '']" (click)="handlerClicked($event, handler)"
|
||||||
[hidden]="handler.hidden" [attr.aria-label]="handler.title | translate" [disabled]="handler.spinner">
|
[hidden]="handler.hidden" [attr.aria-label]="handler.title | translate" [disabled]="handler.spinner">
|
||||||
<ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true" />
|
<ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true" />
|
||||||
|
@ -61,7 +60,7 @@
|
||||||
<ion-spinner [attr.aria-label]="'core.loading' | translate" />
|
<ion-spinner [attr.aria-label]="'core.loading' | translate" />
|
||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item button *ngFor="let handler of newPageHandlers" class="ion-text-wrap" (click)="handlerClicked($event, handler)"
|
<ion-item button *ngFor="let handler of listItemHandlers" class="ion-text-wrap" (click)="handlerClicked($event, handler)"
|
||||||
[ngClass]="['core-user-profile-handler', handler.class || '']" [hidden]="handler.hidden"
|
[ngClass]="['core-user-profile-handler', handler.class || '']" [hidden]="handler.hidden"
|
||||||
[attr.aria-label]="handler.title | translate" [detail]="true">
|
[attr.aria-label]="handler.title | translate" [detail]="true">
|
||||||
<ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true" />
|
<ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true" />
|
||||||
|
@ -76,17 +75,6 @@
|
||||||
</span>
|
</span>
|
||||||
<ion-spinner slot="end" *ngIf="handler.showBadge && handler.loading" [attr.aria-label]="'core.loading' | translate" />
|
<ion-spinner slot="end" *ngIf="handler.showBadge && handler.loading" [attr.aria-label]="'core.loading' | translate" />
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item *ngIf="actionHandlers && actionHandlers.length">
|
|
||||||
<ion-label>
|
|
||||||
<ion-button *ngFor="let handler of actionHandlers" expand="block" fill="outline" size="default"
|
|
||||||
[ngClass]="['core-user-profile-handler', handler.class || '']" (click)="handlerClicked($event, handler)"
|
|
||||||
[hidden]="handler.hidden" [attr.aria-label]="handler.title | translate" [disabled]="handler.spinner">
|
|
||||||
<ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start" aria-hidden="true" />
|
|
||||||
{{ handler.title | translate }}
|
|
||||||
<ion-spinner *ngIf="handler.spinner" slot="end" [attr.aria-label]="'core.loading' | translate" />
|
|
||||||
</ion-button>
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</ion-list>
|
</ion-list>
|
||||||
<core-empty-box *ngIf="!user && !isDeleted && isEnrolled" icon="far-user"
|
<core-empty-box *ngIf="!user && !isDeleted && isEnrolled" icon="far-user"
|
||||||
[message]=" 'core.user.detailsnotavailable' | translate" />
|
[message]=" 'core.user.detailsnotavailable' | translate" />
|
||||||
|
|
|
@ -25,7 +25,7 @@ import { CoreUserHelper } from '@features/user/services/user-helper';
|
||||||
import {
|
import {
|
||||||
CoreUserDelegate,
|
CoreUserDelegate,
|
||||||
CoreUserDelegateContext,
|
CoreUserDelegateContext,
|
||||||
CoreUserDelegateService,
|
CoreUserProfileHandlerType,
|
||||||
CoreUserProfileHandlerData,
|
CoreUserProfileHandlerData,
|
||||||
} from '@features/user/services/user-delegate';
|
} from '@features/user/services/user-delegate';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
@ -59,9 +59,8 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
|
||||||
isSuspended = false;
|
isSuspended = false;
|
||||||
isEnrolled = true;
|
isEnrolled = true;
|
||||||
rolesFormatted?: string;
|
rolesFormatted?: string;
|
||||||
actionHandlers: CoreUserProfileHandlerData[] = [];
|
listItemHandlers: CoreUserProfileHandlerData[] = [];
|
||||||
newPageHandlers: CoreUserProfileHandlerData[] = [];
|
buttonHandlers: CoreUserProfileHandlerData[] = [];
|
||||||
communicationHandlers: CoreUserProfileHandlerData[] = [];
|
|
||||||
|
|
||||||
users?: CoreUserSwipeItemsManager;
|
users?: CoreUserSwipeItemsManager;
|
||||||
|
|
||||||
|
@ -153,20 +152,19 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
|
||||||
const context = this.courseId ? CoreUserDelegateContext.COURSE : CoreUserDelegateContext.SITE;
|
const context = this.courseId ? CoreUserDelegateContext.COURSE : CoreUserDelegateContext.SITE;
|
||||||
|
|
||||||
this.subscription = CoreUserDelegate.getProfileHandlersFor(user, context, this.courseId).subscribe((handlers) => {
|
this.subscription = CoreUserDelegate.getProfileHandlersFor(user, context, this.courseId).subscribe((handlers) => {
|
||||||
this.actionHandlers = [];
|
this.listItemHandlers = [];
|
||||||
this.newPageHandlers = [];
|
this.buttonHandlers = [];
|
||||||
this.communicationHandlers = [];
|
|
||||||
handlers.forEach((handler) => {
|
handlers.forEach((handler) => {
|
||||||
switch (handler.type) {
|
switch (handler.type) {
|
||||||
case CoreUserDelegateService.TYPE_COMMUNICATION:
|
case CoreUserProfileHandlerType.BUTTON:
|
||||||
this.communicationHandlers.push(handler.data);
|
this.buttonHandlers.push(handler.data);
|
||||||
break;
|
break;
|
||||||
case CoreUserDelegateService.TYPE_ACTION:
|
case CoreUserProfileHandlerType.LIST_ACCOUNT_ITEM:
|
||||||
this.actionHandlers.push(handler.data);
|
// Discard this for now.
|
||||||
break;
|
break;
|
||||||
case CoreUserDelegateService.TYPE_NEW_PAGE:
|
case CoreUserProfileHandlerType.LIST_ITEM:
|
||||||
default:
|
default:
|
||||||
this.newPageHandlers.push(handler.data);
|
this.listItemHandlers.push(handler.data);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,7 +14,11 @@
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { CoreUserDelegateService, CoreUserProfileHandler, CoreUserProfileHandlerData } from '../user-delegate';
|
import {
|
||||||
|
CoreUserProfileHandlerType,
|
||||||
|
CoreUserProfileHandler,
|
||||||
|
CoreUserProfileHandlerData,
|
||||||
|
} from '../user-delegate';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreUserProfile } from '../user';
|
import { CoreUserProfile } from '../user';
|
||||||
|
@ -28,7 +32,7 @@ export class CoreUserProfileMailHandlerService implements CoreUserProfileHandler
|
||||||
|
|
||||||
name = 'CoreUserProfileMail';
|
name = 'CoreUserProfileMail';
|
||||||
priority = 700;
|
priority = 700;
|
||||||
type = CoreUserDelegateService.TYPE_COMMUNICATION;
|
type = CoreUserProfileHandlerType.BUTTON;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
|
|
|
@ -23,6 +23,12 @@ import { makeSingleton } from '@singletons';
|
||||||
import { CoreCourses, CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses';
|
import { CoreCourses, CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
|
|
||||||
|
export enum CoreUserProfileHandlerType {
|
||||||
|
LIST_ITEM = 'listitem', // User profile handler type to be shown as a list item.
|
||||||
|
LIST_ACCOUNT_ITEM = 'account_listitem', // User profile handler type to be shown as a list item and it's related to an account.
|
||||||
|
BUTTON = 'button', // User profile handler type to be shown as a button.
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@singletons/events' {
|
declare module '@singletons/events' {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,13 +52,11 @@ export interface CoreUserProfileHandler extends CoreDelegateHandler {
|
||||||
priority: number;
|
priority: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A type should be specified among these:
|
* The type of Handler.
|
||||||
* - TYPE_COMMUNICATION: will be displayed under the user avatar. Should have icon. Spinner not used.
|
*
|
||||||
* - TYPE_NEW_PAGE: will be displayed as a list of items. Should have icon. Spinner not used.
|
* @see CoreUserProfileHandlerType for more info.
|
||||||
* Default value if none is specified.
|
|
||||||
* - TYPE_ACTION: will be displayed as a button and should not redirect to any state. Spinner use is recommended.
|
|
||||||
*/
|
*/
|
||||||
type: string;
|
type: CoreUserProfileHandlerType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If isEnabledForUser Cache should be enabled.
|
* If isEnabledForUser Cache should be enabled.
|
||||||
|
@ -106,7 +110,7 @@ export interface CoreUserProfileHandlerData {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the icon to display. Mandatory for TYPE_COMMUNICATION.
|
* Name of the icon to display. Mandatory for CoreUserProfileHandlerType.BUTTON.
|
||||||
*/
|
*/
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
|
||||||
|
@ -116,32 +120,34 @@ export interface CoreUserProfileHandlerData {
|
||||||
class?: string;
|
class?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If enabled, element will be hidden. Only for TYPE_NEW_PAGE and TYPE_ACTION.
|
* If enabled, element will be hidden. Only for CoreUserProfileHandlerType.LIST_ITEM.
|
||||||
*/
|
*/
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If enabled will show an spinner. Only for TYPE_ACTION.
|
* If enabled will show an spinner.
|
||||||
|
*
|
||||||
|
* @deprecated since 4.4. Not used anymore.
|
||||||
*/
|
*/
|
||||||
spinner?: boolean;
|
spinner?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the handler has badge to show or not. Only for TYPE_NEW_PAGE.
|
* If the handler has badge to show or not. Only for CoreUserProfileHandlerType.LIST_ITEM.
|
||||||
*/
|
*/
|
||||||
showBadge?: boolean;
|
showBadge?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Text to display on the badge. Only used if showBadge is true and only for TYPE_NEW_PAGE.
|
* Text to display on the badge. Only used if showBadge is true and only for CoreUserProfileHandlerType.LIST_ITEM.
|
||||||
*/
|
*/
|
||||||
badge?: string;
|
badge?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accessibility text to add on the badge. Only used if showBadge is true and only for TYPE_NEW_PAGE.
|
* Accessibility text to add on the badge. Only used if showBadge is true and only for CoreUserProfileHandlerType.LIST_ITEM.
|
||||||
*/
|
*/
|
||||||
badgeA11yText?: string;
|
badgeA11yText?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If true, the badge number is being loaded. Only used if showBadge is true and only for TYPE_NEW_PAGE.
|
* If true, the badge number is being loaded. Only used if showBadge is true and only for CoreUserProfileHandlerType.LIST_ITEM.
|
||||||
*/
|
*/
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
|
||||||
|
@ -195,14 +201,20 @@ export class CoreUserDelegateService extends CoreDelegate<CoreUserProfileHandler
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User profile handler type for communication.
|
* User profile handler type for communication.
|
||||||
|
*
|
||||||
|
* @deprecated since 4.4. Use CoreUserProfileHandlerType.BUTTON instead.
|
||||||
*/
|
*/
|
||||||
static readonly TYPE_COMMUNICATION = 'communication';
|
static readonly TYPE_COMMUNICATION = 'communication';
|
||||||
/**
|
/**
|
||||||
* User profile handler type for new page.
|
* User profile handler type for new page.
|
||||||
|
*
|
||||||
|
* @deprecated since 4.4. Use CoreUserProfileHandlerType.LIST_ITEM instead.
|
||||||
*/
|
*/
|
||||||
static readonly TYPE_NEW_PAGE = 'newpage';
|
static readonly TYPE_NEW_PAGE = 'newpage';
|
||||||
/**
|
/**
|
||||||
* User profile handler type for actions.
|
* User profile handler type for actions.
|
||||||
|
*
|
||||||
|
* @deprecated since 4.4. Use CoreUserProfileHandlerType.BUTTON instead.
|
||||||
*/
|
*/
|
||||||
static readonly TYPE_ACTION = 'action';
|
static readonly TYPE_ACTION = 'action';
|
||||||
|
|
||||||
|
@ -341,7 +353,7 @@ export class CoreUserDelegateService extends CoreDelegate<CoreUserProfileHandler
|
||||||
name: name,
|
name: name,
|
||||||
data: handler.getDisplayData(user, context, courseId),
|
data: handler.getDisplayData(user, context, courseId),
|
||||||
priority: handler.priority || 0,
|
priority: handler.priority || 0,
|
||||||
type: handler.type || CoreUserDelegateService.TYPE_NEW_PAGE,
|
type: handler.type || CoreUserProfileHandlerType.LIST_ITEM,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -485,6 +497,24 @@ export class CoreUserDelegateService extends CoreDelegate<CoreUserProfileHandler
|
||||||
return this.userHandlers[userId][contextKey];
|
return this.userHandlers[userId][contextKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
registerHandler(handler: CoreUserProfileHandler): boolean {
|
||||||
|
const type = handler.type as string;
|
||||||
|
|
||||||
|
// eslint-disable-next-line deprecation/deprecation
|
||||||
|
if (type == CoreUserDelegateService.TYPE_COMMUNICATION || type == CoreUserDelegateService.TYPE_ACTION) {
|
||||||
|
handler.type = CoreUserProfileHandlerType.BUTTON;
|
||||||
|
// eslint-disable-next-line deprecation/deprecation
|
||||||
|
} else if (type == CoreUserDelegateService.TYPE_NEW_PAGE) {
|
||||||
|
handler.type = CoreUserProfileHandlerType.LIST_ITEM;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.registerHandler(handler);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CoreUserDelegate = makeSingleton(CoreUserDelegateService);
|
export const CoreUserDelegate = makeSingleton(CoreUserDelegateService);
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
@ -1711,9 +1711,11 @@ ion-input .native-input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
ion-input,
|
||||||
|
ion-input input,
|
||||||
|
ion-textarea {
|
||||||
--placeholder-color: var(--ion-placeholder-color);
|
--placeholder-color: var(--ion-placeholder-color);
|
||||||
--placeholder-opacity: .85;
|
--placeholder-opacity: .65;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable scroll on parent ion contents to enabled PTR on the ones inside the splitview. See split-view component for more info.
|
// Disable scroll on parent ion contents to enabled PTR on the ones inside the splitview. See split-view component for more info.
|
||||||
|
@ -1905,6 +1907,10 @@ swiper-container {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.core-flex-no-grow {
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Loader animation.
|
// Loader animation.
|
||||||
.core-loading {
|
.core-loading {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -2015,3 +2021,83 @@ ion-item.item-label-stacked ion-datetime-button {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
align-self: self-end;
|
align-self: self-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Table App styles
|
||||||
|
table.core-table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
line-height: 20px;
|
||||||
|
width: 98%;
|
||||||
|
margin: 1em auto;
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
|
|
||||||
|
thead th {
|
||||||
|
vertical-align: bottom;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: var(--core-table-header-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody th {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 8px;
|
||||||
|
white-space: normal;
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
border-bottom: 1px solid var(--core-table-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody td {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.odd {
|
||||||
|
--cell-background: var(--core-table-odd-cell-background);
|
||||||
|
--cell-hover: var(--core-table-odd-cell-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.even {
|
||||||
|
--cell-background: var(--core-table-even-cell-background);
|
||||||
|
--cell-hover: var(--core-table-even-cell-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.odd, .even {
|
||||||
|
td, th, th[aria-current="page"] {
|
||||||
|
background-color: var(--cell-background);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--cell-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.auto-striped tr:nth-child(odd) {
|
||||||
|
background-color: var(--core-table-odd-cell-background);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--core-table-even-odd-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.auto-striped tr:nth-child(even) {
|
||||||
|
background-color: var(--core-table-even-cell-background);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--core-table-even-cell-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ion-no-border {
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dimmed_text,
|
||||||
|
.hidden {
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -169,4 +169,10 @@ html.dark {
|
||||||
|
|
||||||
--addon-forum-border-color: var(--gray-500);
|
--addon-forum-border-color: var(--gray-500);
|
||||||
--addon-forum-highlight-color: var(--gray-800);
|
--addon-forum-highlight-color: var(--gray-800);
|
||||||
|
|
||||||
|
--core-table-header-background: var(--gray-900);
|
||||||
|
--core-table-odd-cell-background: var(--gray-800);
|
||||||
|
--core-table-odd-cell-hover: var(--gray-600);
|
||||||
|
--core-table-even-cell-background: var(--gray-900);
|
||||||
|
--core-table-even-cell-hover: var(--gray-700);
|
||||||
}
|
}
|
||||||
|
|
|
@ -381,6 +381,13 @@ html {
|
||||||
--core-dd-question-radius: 10px;
|
--core-dd-question-radius: 10px;
|
||||||
--core-dd-question-border: var(--medium);
|
--core-dd-question-border: var(--medium);
|
||||||
|
|
||||||
|
--core-table-header-background: var(--white);
|
||||||
|
--core-table-odd-cell-background: var(--light);
|
||||||
|
--core-table-odd-cell-hover: var(--gray-200);
|
||||||
|
--core-table-even-cell-background: var(--white);
|
||||||
|
--core-table-even-cell-hover: var(--light);
|
||||||
|
--core-table-border-color: var(--stroke);
|
||||||
|
|
||||||
--rotate-expandable: rotate(90deg);
|
--rotate-expandable: rotate(90deg);
|
||||||
&[dir=rtl] {
|
&[dir=rtl] {
|
||||||
--rotate-expandable: rotate(-90deg);
|
--rotate-expandable: rotate(-90deg);
|
||||||
|
|
Loading…
Reference in New Issue