user pdf exports: participant lists, access data

This commit is contained in:
Maximilian Krambach 2019-01-25 18:14:44 +01:00
parent 00b15228f7
commit 6b0f129067
9 changed files with 522 additions and 45 deletions

View File

@ -429,6 +429,19 @@ export class PdfDocumentService {
margin: [0, 10, 0, 0], margin: [0, 10, 0, 0],
bold: true bold: true
}, },
userDataHeading: {
fontSize: 14,
margin: [0, 10],
bold: true
},
userDataTopic: {
fontSize: 12,
margin: [0, 5]
},
userDataValue: {
fontSize: 12,
margin: [15, 5]
},
tocEntry: { tocEntry: {
fontSize: 12, fontSize: 12,
margin: [0, 0, 0, 0], margin: [0, 0, 0, 0],
@ -446,6 +459,15 @@ export class PdfDocumentService {
}, },
tocCategorySection: { tocCategorySection: {
margin: [0, 0, 0, 10] margin: [0, 0, 0, 10]
},
userDataTitle: {
fontSize: 26,
margin: [0, 0, 0, 0],
bold: true
},
tableHeader: {
bold: true,
fillColor: 'white'
} }
}; };
} }

View File

@ -29,6 +29,11 @@
<mat-icon>security</mat-icon> <mat-icon>security</mat-icon>
<span translate>Change password</span> <span translate>Change password</span>
</button> </button>
<!-- PDF -->
<button mat-menu-item *ngIf="isAllowed('seePersonal')" (click)="onDownloadPdf()">
<mat-icon>picture_as_pdf</mat-icon>
<span translate>PDF</span>
</button>
</mat-menu> </mat-menu>
</os-head-bar> </os-head-bar>

View File

@ -6,14 +6,15 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; 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 { 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 { 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 * Users detail component for both new and existing users
@ -82,6 +83,7 @@ export class UserDetailComponent extends BaseViewComponent implements OnInit {
* @param DS DataStoreService * @param DS DataStoreService
* @param operator OperatorService * @param operator OperatorService
* @param promptService PromptService * @param promptService PromptService
* @param pdfService UserPdfExportService used for export to pdf
*/ */
public constructor( public constructor(
title: Title, title: Title,
@ -93,7 +95,8 @@ export class UserDetailComponent extends BaseViewComponent implements OnInit {
private repo: UserRepositoryService, private repo: UserRepositoryService,
private DS: DataStoreService, private DS: DataStoreService,
private operator: OperatorService, private operator: OperatorService,
private promptService: PromptService private promptService: PromptService,
private pdfService: UserPdfExportService
) { ) {
super(title, translate, matSnackBar); 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);
}
} }

View File

@ -97,12 +97,16 @@
<span translate>Groups</span> <span translate>Groups</span>
</button> </button>
<div *ngIf="presenceViewConfigured"> <button mat-menu-item (click)="pdfExportUserList()">
<button mat-menu-item *osPerms="'users.can_manage'" routerLink="presence"> <mat-icon>picture_as_pdf</mat-icon>
<mat-icon>transfer_within_a_station</mat-icon> <span translate>List of participants (PDF)</span>
<span translate>Presence</span>
</button>
<button mat-menu-item (click)="onDownloadAccessPdf()">
<mat-icon>picture_as_pdf</mat-icon>
<span translate>Access data (PDF)</span>
</button> </button>
</div>
<button mat-menu-item (click)="csvExportUserList()"> <button mat-menu-item (click)="csvExportUserList()">
<mat-icon>archive</mat-icon> <mat-icon>archive</mat-icon>
@ -119,10 +123,12 @@
<mat-icon>done_all</mat-icon> <mat-icon>done_all</mat-icon>
<span translate>Select all</span> <span translate>Select all</span>
</button> </button>
<button mat-menu-item (click)="deselectAll()"> <button mat-menu-item (click)="deselectAll()">
<mat-icon>clear</mat-icon> <mat-icon>clear</mat-icon>
<span translate>Deselect all</span> <span translate>Deselect all</span>
</button> </button>
<div *osPerms="'users.can_manage'"> <div *osPerms="'users.can_manage'">
<mat-divider></mat-divider> <mat-divider></mat-divider>
<button mat-menu-item (click)="setGroupSelected()"> <button mat-menu-item (click)="setGroupSelected()">
@ -130,6 +136,21 @@
<span translate>Add/remove groups ...</span> <span translate>Add/remove groups ...</span>
</button> </button>
<div *ngIf="presenceViewConfigured">
<button mat-menu-item *osPerms="'users.can_manage'" routerLink="presence">
<mat-icon>transfer_within_a_station</mat-icon>
<span translate>Presence</span>
</button>
</div>
<button mat-menu-item *osPerms="'users.can_manage'" routerLink="import">
<mat-icon>save_alt</mat-icon>
<span translate>Import</span><span>&nbsp;...</span>
</button>
<div *osPerms="'users.can_manage'">
<mat-divider></mat-divider>
<button mat-menu-item (click)="setActiveSelected()"> <button mat-menu-item (click)="setActiveSelected()">
<mat-icon>block</mat-icon> <mat-icon>block</mat-icon>
<span translate>Enable/disable account ...</span> <span translate>Enable/disable account ...</span>
@ -139,6 +160,7 @@
<mat-icon>check_box</mat-icon> <mat-icon>check_box</mat-icon>
<span translate>Set presence ...</span> <span translate>Set presence ...</span>
</button> </button>
<button mat-menu-item (click)="setCommitteeSelected()"> <button mat-menu-item (click)="setCommitteeSelected()">
<mat-icon>account_balance</mat-icon> <mat-icon>account_balance</mat-icon>
<span translate>Set committee ...</span> <span translate>Set committee ...</span>
@ -161,5 +183,6 @@
</button> </button>
</div> </div>
</div> </div>
</div>
</mat-menu> </mat-menu>
</mat-drawer-container> </mat-drawer-container>

View File

@ -4,18 +4,19 @@ import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CsvExportService } from '../../../../core/services/csv-export.service';
import { ChoiceService } from '../../../../core/services/choice.service'; import { ChoiceService } from '../../../../core/services/choice.service';
import { ConfigService } from 'app/core/services/config.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 { GroupRepositoryService } from '../../services/group-repository.service';
import { ListViewBaseComponent } from '../../../base/list-view-base';
import { PromptService } from '../../../../core/services/prompt.service'; 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 { 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 { UserSortListService } from '../../services/user-sort-list.service';
import { ViewportService } from '../../../../core/services/viewport.service'; import { ViewportService } from '../../../../core/services/viewport.service';
import { OperatorService } from '../../../../core/services/operator.service'; import { OperatorService } from '../../../../core/services/operator.service';
import { ViewUser } from '../../models/view-user';
/** /**
* Component for the user list view. * Component for the user list view.
@ -51,7 +52,6 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
} }
/** /**
* /**
* The usual constructor for components * The usual constructor for components
* @param titleService Serivce for setting the title * @param titleService Serivce for setting the title
* @param translate Service for translation handling * @param translate Service for translation handling
@ -68,6 +68,7 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
* @param filterService * @param filterService
* @param sortService * @param sortService
* @param config ConfigService * @param config ConfigService
* @param userPdf Service for downloading pdf
*/ */
public constructor( public constructor(
titleService: Title, titleService: Title,
@ -84,7 +85,8 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
private promptService: PromptService, private promptService: PromptService,
public filterService: UserFilterListService, public filterService: UserFilterListService,
public sortService: UserSortListService, public sortService: UserSortListService,
config: ConfigService config: ConfigService,
private userPdf: UserPdfExportService
) { ) {
super(titleService, translate, matSnackBar); super(titleService, translate, matSnackBar);
@ -128,7 +130,8 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> 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 { public csvExportUserList(): void {
this.csvExport.export( this.csvExport.export(
@ -151,6 +154,22 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> 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 * Bulk deletes users. Needs multiSelect mode to fill selectedRows
*/ */

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -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();
});
});

View File

@ -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<string>('users_pdf_wlan_ssid') || '-',
style: 'userDataValue'
},
{
text: this.translate.instant('WLAN password') + ':',
style: 'userDataTopic'
},
{
text: this.configService.instant<string>('users_pdf_wlan_password') || '-',
style: 'userDataValue'
},
{
text: this.translate.instant('WLAN encryption') + ':',
style: 'userDataTopic'
},
{
text: this.configService.instant<string>('users_pdf_wlan_encryption') || '-',
style: 'userDataValue'
},
{
text: '\n'
}
];
if (
this.configService.instant<string>('users_pdf_wlan_ssid') &&
this.configService.instant<string>('users_pdf_wlan_encryption')
) {
const wifiQrCode =
'WIFI:S:' +
this.configService.instant<string>('users_pdf_wlan_ssid') +
';T:' +
this.configService.instant<string>('users_pdf_wlan_encryption') +
';P:' +
this.configService.instant<string>('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<string>('users_pdf_url') || '-',
link: this.configService.instant<string>('users_pdf_url'),
style: 'userDataValue'
},
{
text: '\n'
}
];
// url qr code
if (this.configService.instant<string>('users_pdf_url')) {
columnOpenSlides.push(
{
qr: this.configService.instant<string>('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<string>('users_pdf_welcometitle')),
style: 'userDataHeading'
},
{
text: this.translate.instant(this.configService.instant<string>('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;
}
}