Merge pull request #4195 from MaximilianKrambach/userPdf

user pdf exports: participant lists, access data
This commit is contained in:
Emanuel Schütze 2019-01-29 19:49:07 +01:00 committed by GitHub
commit e0177893aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 522 additions and 45 deletions

View File

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

View File

@ -29,6 +29,11 @@
<mat-icon>security</mat-icon>
<span translate>Change password</span>
</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>
</os-head-bar>

View File

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

View File

@ -97,12 +97,16 @@
<span translate>Groups</span>
</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 (click)="pdfExportUserList()">
<mat-icon>picture_as_pdf</mat-icon>
<span translate>List of participants (PDF)</span>
</button>
<button mat-menu-item (click)="onDownloadAccessPdf()">
<mat-icon>picture_as_pdf</mat-icon>
<span translate>Access data (PDF)</span>
</button>
<button mat-menu-item (click)="csvExportUserList()">
<mat-icon>archive</mat-icon>
@ -119,10 +123,12 @@
<mat-icon>done_all</mat-icon>
<span translate>Select all</span>
</button>
<button mat-menu-item (click)="deselectAll()">
<mat-icon>clear</mat-icon>
<span translate>Deselect all</span>
</button>
<div *osPerms="'users.can_manage'">
<mat-divider></mat-divider>
<button mat-menu-item (click)="setGroupSelected()">
@ -130,35 +136,52 @@
<span translate>Add/remove groups ...</span>
</button>
<button mat-menu-item (click)="setActiveSelected()">
<mat-icon>block</mat-icon>
<span translate>Enable/disable account ...</span>
<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>
<button mat-menu-item (click)="setPresentSelected()">
<mat-icon>check_box</mat-icon>
<span translate>Set presence ...</span>
</button>
<button mat-menu-item (click)="setCommitteeSelected()">
<mat-icon>account_balance</mat-icon>
<span translate>Set committee ...</span>
</button>
<div *osPerms="'users.can_manage'">
<mat-divider></mat-divider>
<mat-divider></mat-divider>
<button mat-menu-item (click)="setActiveSelected()">
<mat-icon>block</mat-icon>
<span translate>Enable/disable account ...</span>
</button>
<button mat-menu-item (click)="sendInvitationEmailSelected()">
<mat-icon>mail</mat-icon>
<span translate>Send invitation email</span>
</button>
<button mat-menu-item (click)="resetPasswordsSelected()">
<mat-icon>vpn_key</mat-icon>
<span translate>Generate new passwords</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item class="red-warning-text" (click)="deleteSelected()">
<mat-icon>delete</mat-icon>
<span translate>Delete</span>
</button>
<button mat-menu-item (click)="setPresentSelected()">
<mat-icon>check_box</mat-icon>
<span translate>Set presence ...</span>
</button>
<button mat-menu-item (click)="setCommitteeSelected()">
<mat-icon>account_balance</mat-icon>
<span translate>Set committee ...</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item (click)="sendInvitationEmailSelected()">
<mat-icon>mail</mat-icon>
<span translate>Send invitation email</span>
</button>
<button mat-menu-item (click)="resetPasswordsSelected()">
<mat-icon>vpn_key</mat-icon>
<span translate>Generate new passwords</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item class="red-warning-text" (click)="deleteSelected()">
<mat-icon>delete</mat-icon>
<span translate>Delete</span>
</button>
</div>
</div>
</div>
</mat-menu>

View File

@ -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<ViewUser> 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<ViewUser> 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<ViewUser> 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<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 {
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
*/

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