diff --git a/client/src/app/core/services/pdf-document.service.ts b/client/src/app/core/services/pdf-document.service.ts index 6387fe248..a2ca65bb2 100644 --- a/client/src/app/core/services/pdf-document.service.ts +++ b/client/src/app/core/services/pdf-document.service.ts @@ -429,6 +429,19 @@ export class PdfDocumentService { margin: [0, 10, 0, 0], bold: true }, + userDataHeading: { + fontSize: 14, + margin: [0, 10], + bold: true + }, + userDataTopic: { + fontSize: 12, + margin: [0, 5] + }, + userDataValue: { + fontSize: 12, + margin: [15, 5] + }, tocEntry: { fontSize: 12, margin: [0, 0, 0, 0], @@ -446,6 +459,15 @@ export class PdfDocumentService { }, tocCategorySection: { margin: [0, 0, 0, 10] + }, + userDataTitle: { + fontSize: 26, + margin: [0, 0, 0, 0], + bold: true + }, + tableHeader: { + bold: true, + fillColor: 'white' } }; } diff --git a/client/src/app/site/users/components/user-detail/user-detail.component.html b/client/src/app/site/users/components/user-detail/user-detail.component.html index 7fa20b161..083149d96 100644 --- a/client/src/app/site/users/components/user-detail/user-detail.component.html +++ b/client/src/app/site/users/components/user-detail/user-detail.component.html @@ -29,6 +29,11 @@ security Change password + + diff --git a/client/src/app/site/users/components/user-detail/user-detail.component.ts b/client/src/app/site/users/components/user-detail/user-detail.component.ts index 23e0c4c1a..5c20722ad 100644 --- a/client/src/app/site/users/components/user-detail/user-detail.component.ts +++ b/client/src/app/site/users/components/user-detail/user-detail.component.ts @@ -6,14 +6,15 @@ import { Title } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; -import { genders } from 'app/shared/models/users/user'; -import { ViewUser } from '../../models/view-user'; -import { UserRepositoryService } from '../../services/user-repository.service'; -import { Group } from '../../../../shared/models/users/group'; -import { DataStoreService } from '../../../../core/services/data-store.service'; -import { OperatorService } from '../../../../core/services/operator.service'; import { BaseViewComponent } from '../../../base/base-view'; +import { DataStoreService } from '../../../../core/services/data-store.service'; +import { genders } from 'app/shared/models/users/user'; +import { Group } from '../../../../shared/models/users/group'; +import { OperatorService } from '../../../../core/services/operator.service'; import { PromptService } from '../../../../core/services/prompt.service'; +import { UserPdfExportService } from '../../services/user-pdf-export.service'; +import { UserRepositoryService } from '../../services/user-repository.service'; +import { ViewUser } from '../../models/view-user'; /** * Users detail component for both new and existing users @@ -82,6 +83,7 @@ export class UserDetailComponent extends BaseViewComponent implements OnInit { * @param DS DataStoreService * @param operator OperatorService * @param promptService PromptService + * @param pdfService UserPdfExportService used for export to pdf */ public constructor( title: Title, @@ -93,7 +95,8 @@ export class UserDetailComponent extends BaseViewComponent implements OnInit { private repo: UserRepositoryService, private DS: DataStoreService, private operator: OperatorService, - private promptService: PromptService + private promptService: PromptService, + private pdfService: UserPdfExportService ) { super(title, translate, matSnackBar); @@ -359,4 +362,11 @@ export class UserDetailComponent extends BaseViewComponent implements OnInit { } }); } + + /** + * Triggers the pdf download for this user + */ + public onDownloadPdf(): void { + this.pdfService.exportSingleUserAccessPDF(this.user); + } } diff --git a/client/src/app/site/users/components/user-list/user-list.component.html b/client/src/app/site/users/components/user-list/user-list.component.html index 59b0034e1..ceb673c74 100644 --- a/client/src/app/site/users/components/user-list/user-list.component.html +++ b/client/src/app/site/users/components/user-list/user-list.component.html @@ -97,12 +97,16 @@ Groups -
- -
+ + + + +
- +
+ + - - +
+ - + - - - - + + + + + + + + + + +
diff --git a/client/src/app/site/users/components/user-list/user-list.component.ts b/client/src/app/site/users/components/user-list/user-list.component.ts index 18a8b9160..0ca818a20 100644 --- a/client/src/app/site/users/components/user-list/user-list.component.ts +++ b/client/src/app/site/users/components/user-list/user-list.component.ts @@ -4,18 +4,19 @@ import { Router, ActivatedRoute } from '@angular/router'; import { Title } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; -import { CsvExportService } from '../../../../core/services/csv-export.service'; import { ChoiceService } from '../../../../core/services/choice.service'; import { ConfigService } from 'app/core/services/config.service'; -import { ListViewBaseComponent } from '../../../base/list-view-base'; +import { CsvExportService } from '../../../../core/services/csv-export.service'; import { GroupRepositoryService } from '../../services/group-repository.service'; +import { ListViewBaseComponent } from '../../../base/list-view-base'; import { PromptService } from '../../../../core/services/prompt.service'; -import { UserRepositoryService } from '../../services/user-repository.service'; -import { ViewUser } from '../../models/view-user'; import { UserFilterListService } from '../../services/user-filter-list.service'; +import { UserRepositoryService } from '../../services/user-repository.service'; +import { UserPdfExportService } from '../../services/user-pdf-export.service'; import { UserSortListService } from '../../services/user-sort-list.service'; import { ViewportService } from '../../../../core/services/viewport.service'; import { OperatorService } from '../../../../core/services/operator.service'; +import { ViewUser } from '../../models/view-user'; /** * Component for the user list view. @@ -51,7 +52,6 @@ export class UserListComponent extends ListViewBaseComponent implement } /** - * /** * The usual constructor for components * @param titleService Serivce for setting the title * @param translate Service for translation handling @@ -68,6 +68,7 @@ export class UserListComponent extends ListViewBaseComponent implement * @param filterService * @param sortService * @param config ConfigService + * @param userPdf Service for downloading pdf */ public constructor( titleService: Title, @@ -84,7 +85,8 @@ export class UserListComponent extends ListViewBaseComponent implement private promptService: PromptService, public filterService: UserFilterListService, public sortService: UserSortListService, - config: ConfigService + config: ConfigService, + private userPdf: UserPdfExportService ) { super(titleService, translate, matSnackBar); @@ -128,7 +130,8 @@ export class UserListComponent extends ListViewBaseComponent implement } /** - * Export all users as CSV + * Export all users currently matching the filter + * as CSV (including personal information such as initial passwords) */ public csvExportUserList(): void { this.csvExport.export( @@ -151,6 +154,22 @@ export class UserListComponent extends ListViewBaseComponent implement ); } + /** + * Export all users currently matching the filter as PDF + * (access information, including personal information such as initial passwords) + */ + public onDownloadAccessPdf(): void { + this.userPdf.exportMultipleUserAccessPDF(this.dataSource.data); + } + + /** + * triggers the download of a simple participant list (no details on user name and passwords) + * with all users currently matching the filter + */ + public pdfExportUserList(): void { + this.userPdf.exportUserList(this.dataSource.data); + } + /** * Bulk deletes users. Needs multiSelect mode to fill selectedRows */ diff --git a/client/src/app/site/users/services/user-pdf-export.service.spec.ts b/client/src/app/site/users/services/user-pdf-export.service.spec.ts new file mode 100644 index 000000000..97bdbb497 --- /dev/null +++ b/client/src/app/site/users/services/user-pdf-export.service.spec.ts @@ -0,0 +1,17 @@ +import { TestBed } from '@angular/core/testing'; + +import { UserPdfExportService } from './user-pdf-export.service'; +import { E2EImportsModule } from 'e2e-imports.module'; + +describe('UserPdfExportService', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [E2EImportsModule] + }) + ); + + it('should be created', () => { + const service: UserPdfExportService = TestBed.get(UserPdfExportService); + expect(service).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/users/services/user-pdf-export.service.ts b/client/src/app/site/users/services/user-pdf-export.service.ts new file mode 100644 index 000000000..29543b726 --- /dev/null +++ b/client/src/app/site/users/services/user-pdf-export.service.ts @@ -0,0 +1,73 @@ +import { Injectable } from '@angular/core'; + +import { TranslateService } from '@ngx-translate/core'; + +import { PdfDocumentService } from 'app/core/services/pdf-document.service'; +import { UserPdfService } from './user-pdf.service'; +import { ViewUser } from '../models/view-user'; + +/** + * Export service to handle various kind of exporting necessities for participants. + */ +@Injectable({ + providedIn: 'root' +}) +export class UserPdfExportService { + /** + * Constructor + * + * @param translate TranslateService - handle translations + * @param userPdfService UserPdfService - convert users to PDF + * @param pdfDocumentService PdfDocumentService Actual pdfmake functions and global doc definitions + */ + public constructor( + private translate: TranslateService, + private userPdfService: UserPdfService, + private pdfDocumentService: PdfDocumentService + ) {} + + /** + * Exports a single user with access information to PDF + * + * @param user The user to export + */ + public exportSingleUserAccessPDF(user: ViewUser): void { + const doc = this.userPdfService.userAccessToDocDef(user); + const filename = `${this.translate.instant('User')} ${user.short_name}`; + const metadata = { + title: filename + }; + this.pdfDocumentService.download(doc, filename, metadata); + } + + /** + * Exports multiple users with access information to a collection of PDFs + * + * @param Users + */ + public exportMultipleUserAccessPDF(users: ViewUser[]): void { + const doc: object[] = []; + users.forEach(user => { + doc.push(this.userPdfService.userAccessToDocDef(user)); + doc.push({ text: '', pageBreak: 'after' }); + }); + const filename = this.translate.instant('User'); + const metadata = { + title: filename + }; + this.pdfDocumentService.download(doc, filename, metadata); + } + + /** + * Export a participant list + * @param users: The users to appear on that list + * + */ + public exportUserList(users: ViewUser[]): void { + const filename = this.translate.instant('List of participants'); + const metadata = { + title: filename + }; + this.pdfDocumentService.download(this.userPdfService.createUserListDocDef(users), filename, metadata); + } +} diff --git a/client/src/app/site/users/services/user-pdf.service.spec.ts b/client/src/app/site/users/services/user-pdf.service.spec.ts new file mode 100644 index 000000000..60a3a76a4 --- /dev/null +++ b/client/src/app/site/users/services/user-pdf.service.spec.ts @@ -0,0 +1,17 @@ +import { TestBed } from '@angular/core/testing'; + +import { UserPdfService } from './user-pdf.service'; +import { E2EImportsModule } from 'e2e-imports.module'; + +describe('UserPdfService', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [E2EImportsModule] + }) + ); + + it('should be created', () => { + const service: UserPdfService = TestBed.get(UserPdfService); + expect(service).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/users/services/user-pdf.service.ts b/client/src/app/site/users/services/user-pdf.service.ts new file mode 100644 index 000000000..89712be49 --- /dev/null +++ b/client/src/app/site/users/services/user-pdf.service.ts @@ -0,0 +1,291 @@ +import { Injectable } from '@angular/core'; + +import { TranslateService } from '@ngx-translate/core'; + +import { ConfigService } from 'app/core/services/config.service'; +import { ViewUser } from '../models/view-user'; + +/** + * Creates a pdf for a user, containing greetings and initial login information + * Provides the public methods `userAccessToDocDef(user: ViewUser)` and + * `createUserListDocDef(users:ViewUser[])` which should be convenient to use. + * @example + * ```ts + * const pdfMakeCompatibleDocDef = this.UserPdfService.userAccessToDocDef(User); + * const pdfMakeCompatibleDocDef = this.UserPdfService.createUserListDocDef(Users); + * ``` + */ +@Injectable({ + providedIn: 'root' +}) +export class UserPdfService { + /** + * Constructor + * + * @param translate handle translations + * @param configService Read config variables + */ + public constructor(private translate: TranslateService, private configService: ConfigService) {} + + /** + * Converts a user to PdfMake doc definition, containing access information + * for login and wlan access, if applicable + * + * @returns doc def for the user + */ + public userAccessToDocDef(user: ViewUser): object { + const userHeadline = [ + { + text: user.short_name, + style: 'userDataTitle' + } + ]; + if (user.structure_level) { + userHeadline.push({ + text: user.structure_level, + style: 'userDataHeading' + }); + } + return [userHeadline, this.createAccessDataContent(user), this.createWelcomeText()]; + } + + /** + * Generates the document definitions for a participant list with the given users, sorted as in the input. + * + * @param users An array of users + * @returns pdfMake definitions for a table including name, structure level and groups for each user + */ + public createUserListDocDef(users: ViewUser[]): object { + const title = { + text: this.translate.instant('List of participants'), + style: 'title' + }; + return [title, this.createUserList(users)]; + } + + /** + * Handles the creation of access data for the given user + * + * @param user + * @returns + */ + private createAccessDataContent(user: ViewUser): object { + return { + columns: [this.createWifiAccessContent(), this.createUserAccessContent(user)], + margin: [0, 20] + }; + } + + /** + * Creates the wifi access data, including qr code, for the configured event wlan parameters + * + * @returns pdfMake definitions + */ + private createWifiAccessContent(): object { + const wifiColumn: object[] = [ + { + text: this.translate.instant('WLAN access data'), + style: 'userDataHeading' + }, + { + text: this.translate.instant('WLAN name (SSID)') + ':', + style: 'userDataTopic' + }, + { + text: this.configService.instant('users_pdf_wlan_ssid') || '-', + style: 'userDataValue' + }, + { + text: this.translate.instant('WLAN password') + ':', + style: 'userDataTopic' + }, + { + text: this.configService.instant('users_pdf_wlan_password') || '-', + style: 'userDataValue' + }, + { + text: this.translate.instant('WLAN encryption') + ':', + style: 'userDataTopic' + }, + { + text: this.configService.instant('users_pdf_wlan_encryption') || '-', + style: 'userDataValue' + }, + { + text: '\n' + } + ]; + if ( + this.configService.instant('users_pdf_wlan_ssid') && + this.configService.instant('users_pdf_wlan_encryption') + ) { + const wifiQrCode = + 'WIFI:S:' + + this.configService.instant('users_pdf_wlan_ssid') + + ';T:' + + this.configService.instant('users_pdf_wlan_encryption') + + ';P:' + + this.configService.instant('users_pdf_wlan_password') + + ';;'; + wifiColumn.push( + { + qr: wifiQrCode, + fit: 120, + margin: [0, 0, 0, 8] + }, + { + text: this.translate.instant('Scan this QR code to connect to WLAN.'), + style: 'small' + } + ); + } + return wifiColumn; + } + + /** + * Creates access information (login name, initial password) for the given user, + * additionally encoded in a qr code + * + * @param user + * @returns pdfMake definitions + */ + private createUserAccessContent(user: ViewUser): object { + const columnOpenSlides: object[] = [ + { + text: this.translate.instant('OpenSlides access data'), + style: 'userDataHeading' + }, + { + text: this.translate.instant('Username') + ':', + style: 'userDataTopic' + }, + { + text: user.username, + style: 'userDataValue' + }, + { + text: this.translate.instant('Initial password') + ':', + style: 'userDataTopic' + }, + { + text: user.default_password, + style: 'userDataValue' + }, + { + text: 'URL:', + style: 'userDataTopic' + }, + { + text: this.configService.instant('users_pdf_url') || '-', + link: this.configService.instant('users_pdf_url'), + style: 'userDataValue' + }, + { + text: '\n' + } + ]; + // url qr code + if (this.configService.instant('users_pdf_url')) { + columnOpenSlides.push( + { + qr: this.configService.instant('users_pdf_url'), + fit: 120, + margin: [0, 0, 0, 8] + }, + { + text: this.translate.instant('Scan this QR code to open URL.'), + style: 'small' + } + ); + } + return columnOpenSlides; + } + + /** + * Generates a welcone text according to the events' configuration + * + * @returns pdfMake definitions + */ + private createWelcomeText(): object { + return [ + { + text: this.translate.instant(this.configService.instant('users_pdf_welcometitle')), + style: 'userDataHeading' + }, + { + text: this.translate.instant(this.configService.instant('users_pdf_welcometext')), + style: 'userDataTopic' + } + ]; + } + + /** + * Handles the creation of the participant lists' table structure + * + * @param users: passed through to getListUsers + * @returns a pdfMake table definition ready to be put into a page + */ + private createUserList(users: ViewUser[]): object { + const userTableBody: object[] = [ + [ + { + text: '#', + style: 'tableHeader' + }, + { + text: this.translate.instant('Name'), + style: 'tableHeader' + }, + { + text: this.translate.instant('Structure level'), + style: 'tableHeader' + }, + { + text: this.translate.instant('Groups'), + style: 'tableHeader' + } + ] + ]; + return { + table: { + widths: ['auto', '*', 'auto', 'auto'], + headerRows: 1, + body: userTableBody.concat(this.getListUsers(users)) + }, + layout: { + hLineWidth: rowIndex => { + return rowIndex === 1; + }, + vLineWidth: () => { + return 0; + }, + fillColor: rowIndex => { + return rowIndex % 2 === 0 ? '#EEEEEE' : null; + } + } + }; + } + + /** + * parses the incoming users and generates pdfmake table lines for each of them + * + * @param users: The users to include, in order + * @returns column definitions with odd and even entries styled differently, + * short name, structure level and group name columns + */ + private getListUsers(users: ViewUser[]): object[] { + const result = []; + let counter = 1; + users.forEach(user => { + const groupList = user.groups.map(grp => this.translate.instant(grp.name)); + result.push([ + { text: '' + counter }, + { text: user.short_name }, + { text: user.structure_level }, + { text: groupList.join(', ') } + ]); + counter += 1; + }); + return result; + } +}