Skip to content

Commit

Permalink
Switch account import to backend (#2674)
Browse files Browse the repository at this point in the history
  • Loading branch information
luisa-beerboom authored Sep 29, 2023
1 parent 0fd4488 commit 74438b4
Show file tree
Hide file tree
Showing 18 changed files with 162 additions and 106 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { BaseViewModel } from 'src/app/site/base/base-view-model';
import { ExportServiceModule } from '../export-service.module';
import { FileExportService } from '../file-export.service';
import {
CsvColumnsDefinition,
BackendCsvColumnsDefinition,
DEFAULT_COLUMN_SEPARATOR,
DEFAULT_ENCODING,
DEFAULT_LINE_SEPARATOR,
Expand All @@ -30,7 +30,7 @@ export class CsvExportForBackendService {
*/
public export<T extends BaseViewModel>(
models: T[],
columns: CsvColumnsDefinition<T>,
columns: BackendCsvColumnsDefinition<T>,
filename: string,
{
lineSeparator = DEFAULT_LINE_SEPARATOR,
Expand All @@ -54,7 +54,7 @@ export class CsvExportForBackendService {
const header = columns.map(column => {
let label = ``;
if (isPropertyDefinition(column)) {
label = column.label ? column.label : (column.property as string);
label = column.property as string;
} else if (isMapDefinition(column)) {
label = column.label;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/**
* Defines a csv column with a property of the model and an optional label. If this is not given, the
* translated and capitalized property name is used.
*/
export interface BackendCsvColumnDefinitionProperty<T> {
property: keyof T;
}

/**
* Defines a csv column with a property of the model and an optional label. If this is not given, the
* translated and capitalized property name is used.
Expand Down Expand Up @@ -44,6 +52,12 @@ export function isMapDefinition<T>(obj: any): obj is CsvColumnDefinitionMap<T> {
*/
export type CsvColumnsDefinition<T> = (CsvColumnDefinitionProperty<T> | CsvColumnDefinitionMap<T>)[];

/**
* The definition of columns in the export. Either use a property for every model or do a custom mapping to
* a string to be put into the csv.
*/
export type BackendCsvColumnsDefinition<T> = (BackendCsvColumnDefinitionProperty<T> | CsvColumnDefinitionMap<T>)[];

export const ISO_8859_15_ENCODING = `iso-8859-15`;
export const DEFAULT_LINE_SEPARATOR = `\r\n`;
export const DEFAULT_COLUMN_SEPARATOR = `,`;
Expand Down
2 changes: 2 additions & 0 deletions client/src/app/gateways/repositories/users/user-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ export class UserAction {
public static readonly FORGET_PASSWORD_CONFIRM = `user.forget_password_confirm`;
public static readonly ASSIGN_MEETINGS = `user.assign_meetings`;
public static readonly MERGE_TOGETHER = `user.merge_together`;
public static readonly ACCOUNT_JSON_UPLOAD = `account.json_upload`;
public static readonly ACCOUNT_IMPORT = `account.import`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { BaseRepository } from 'src/app/gateways/repositories/base-repository';
import { UserAction } from 'src/app/gateways/repositories/users/user-action';
import { ActiveMeetingIdService } from 'src/app/site/pages/meetings/services/active-meeting-id.service';
import { ViewMeetingUser } from 'src/app/site/pages/meetings/view-models/view-meeting-user';
import { BackendImportRawPreview } from 'src/app/ui/modules/import-list/definitions/backend-import-preview';

import { Id } from '../../../domain/definitions/key-types';
import { Displayable } from '../../../domain/interfaces/displayable';
Expand Down Expand Up @@ -497,6 +498,14 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {
return this.createAction(UserAction.SET_PRESENT, payload);
}

public accountJsonUpload(payload: { [key: string]: any }): Action<BackendImportRawPreview> {
return this.createAction<BackendImportRawPreview>(UserAction.ACCOUNT_JSON_UPLOAD, payload);
}

public accountImport(payload: { id: number; import: boolean }[]): Action<BackendImportRawPreview | void> {
return this.createAction<BackendImportRawPreview | void>(UserAction.ACCOUNT_IMPORT, payload);
}

private sanitizePayload(payload: any): any {
const temp = { ...payload };
for (const key of Object.keys(temp).filter(field => !this.isFieldAllowedToBeEmpty(field))) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,15 +282,24 @@ export abstract class BaseBackendImportService implements BackendImportService {
isBackendImportRawPreview(result)
) as BackendImportRawPreview[];
this.processRawPreviews(updatedPreviews);
if (this.previewHasRowErrors) {
const statesSet = new Set(updatedPreviews.map(preview => preview.state));
if (statesSet.has(BackendImportState.Error)) {
this._currentImportPhaseSubject.next(BackendImportPhase.ERROR);
} else if (statesSet.has(BackendImportState.Warning)) {
this.processRawPreviews(
updatedPreviews
.filter(preview => preview.state === BackendImportState.Warning)
.map(preview => ({
...preview,
rows: preview.rows.filter(row => row.messages && row.messages.length)
}))
);
this._currentImportPhaseSubject.next(BackendImportPhase.FINISHED_WITH_WARNING);
} else {
this._currentImportPhaseSubject.next(BackendImportPhase.TRY_AGAIN);
this._currentImportPhaseSubject.next(BackendImportPhase.FINISHED);
this._csvLines = [];
return true;
}
} else {
this._currentImportPhaseSubject.next(BackendImportPhase.FINISHED);
this._csvLines = [];
return true;
}
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export abstract class BaseViaBackendImportListComponent extends BaseComponent im
* True if the import is in a state in which an import can be conducted
*/
public get canImport(): boolean {
return this._state === BackendImportPhase.AWAITING_CONFIRM || this.tryAgain;
return this._state === BackendImportPhase.AWAITING_CONFIRM;
}

/**
Expand All @@ -42,8 +42,8 @@ export abstract class BaseViaBackendImportListComponent extends BaseComponent im
/**
* True if, after an attempted import failed, the view is waiting for the user to confirm the import on the new preview.
*/
public get tryAgain(): boolean {
return this._state === BackendImportPhase.TRY_AGAIN;
public get finishedWithWarnings(): boolean {
return this._state === BackendImportPhase.FINISHED_WITH_WARNING;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@ <h2>{{ 'Import topics' | translate }}</h2>

<div class="menu-slot">
<button *ngIf="canImport" mat-button (click)="doImport()">
<span class="upper">{{ tryAgain ? 'Try again' : ('Import' | translate) }}</span>
<span class="upper">{{ 'Import' | translate }}</span>
</button>
<div *ngIf="finishedSuccessfully || hasErrors" style="padding-top: 6px">
<mat-icon *ngIf="finishedSuccessfully" matTooltip="{{ 'Import successful' | translate }}">done</mat-icon>
<mat-icon
*ngIf="finishedWithWarnings"
matTooltip="{{ 'Import successful with some warnings' | translate }}"
>
done
</mat-icon>
<mat-icon *ngIf="hasErrors" matTooltip="{{ 'Can not import because of errors' | translate }}">
block
</mat-icon>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ <h2>{{ 'Import accounts' | translate }}</h2>
</div>
</os-head-bar>

<os-import-list
[columns]="generateImportColumns"
<os-backend-import-list
[defaultColumns]="columns"
[importer]="importer"
[possibleFields]="possibleFields"
modelName="Account"
></os-import-list>
></os-backend-import-list>
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { UntypedFormBuilder } from '@angular/forms';
import { Component } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { GeneralUser } from 'src/app/gateways/repositories/users';
import { BaseUserImportListComponent } from 'src/app/site/base/base-user-import-list.component';
import { BaseViaBackendImportListComponent } from 'src/app/site/base/base-via-backend-import-list.component';
import { ComponentServiceCollectorService } from 'src/app/site/services/component-service-collector.service';
import { ImportListHeaderDefinition } from 'src/app/ui/modules/import-list';

import { AccountControllerService } from '../../../../services/common/account-controller.service';
import { accountHeadersAndVerboseNames } from '../../definitions';
import { AccountImportService } from '../../services/account-import.service/account-import.service';

Expand All @@ -14,45 +12,20 @@ import { AccountImportService } from '../../services/account-import.service/acco
templateUrl: `./account-import-list.component.html`,
styleUrls: [`./account-import-list.component.scss`]
})
export class AccountImportListComponent extends BaseUserImportListComponent implements OnInit {
export class AccountImportListComponent extends BaseViaBackendImportListComponent {
public possibleFields = Object.keys(accountHeadersAndVerboseNames);

public columns: ImportListHeaderDefinition[] = Object.keys(accountHeadersAndVerboseNames).map(header => ({
property: header,
label: (<any>accountHeadersAndVerboseNames)[header],
isTableColumn: true
}));

public constructor(
componentServiceCollector: ComponentServiceCollectorService,
protected override translate: TranslateService,
formBuilder: UntypedFormBuilder,
public override importer: AccountImportService,
private accountController: AccountControllerService
public override importer: AccountImportService
) {
super(componentServiceCollector, translate, importer, formBuilder, accountHeadersAndVerboseNames);
}

public override ngOnInit(): void {
super.ngOnInit();
this.loadUsers();
}

private async loadUsers(): Promise<void> {
try {
// const request = await this.accountController.getAllOrgaUsersModelRequest();
// this.subscribe(request, `load_users`);
} catch (e) {
console.log(`Error`, e);
}
}

/**
* Guess the type of the property, since
* `const type = typeof User[property];`
* always returns undefined
*/
protected guessType(userProperty: keyof GeneralUser): 'string' | 'number' | 'boolean' {
const numberProperties: (keyof GeneralUser)[] = [`id`, `vote_weight`];
const booleanProperties: (keyof GeneralUser)[] = [`is_physical_person`, `is_active`];
if (numberProperties.includes(userProperty)) {
return `number`;
} else if (booleanProperties.includes(userProperty)) {
return `boolean`;
} else {
return `string`;
}
super(componentServiceCollector, translate, importer);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { User } from 'src/app/domain/models/users/user';
import { userHeadersAndVerboseNames } from 'src/app/domain/models/users/user.constants';

export const accountHeadersAndVerboseNames: { [key in keyof User]?: string } = {
...userHeadersAndVerboseNames,
default_structure_level: `Structure level`,
default_number: `Participant number`,
default_vote_weight: `Vote weight`
default_structure_level: _(`Structure level`),
default_number: _(`Participant number`),
default_vote_weight: _(`Vote weight`),
saml_id: _(`SSO identification`)
};
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { Injectable } from '@angular/core';
import { GeneralUser } from 'src/app/gateways/repositories/users';
import { ImportConfig } from 'src/app/infrastructure/utils/import/import-utils';
import { BaseUserImportService } from 'src/app/site/base/base-user-import.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { BaseBackendImportService } from 'src/app/site/base/base-import.service/base-backend-import.service';
import { ImportServiceCollectorService } from 'src/app/site/services/import-service-collector.service';
import { UserControllerService } from 'src/app/site/services/user-controller.service';
import { BackendImportRawPreview } from 'src/app/ui/modules/import-list/definitions/backend-import-preview';

import { AccountExportService } from '../../../../services/account-export.service';
import { accountHeadersAndVerboseNames } from '../../definitions';
import { AccountControllerService } from '../../../../services/common/account-controller.service';
import { AccountImportServiceModule } from '../account-import-service.module';

@Injectable({
providedIn: AccountImportServiceModule
})
export class AccountImportService extends BaseUserImportService {
public override errorList = {
export class AccountImportService extends BaseBackendImportService {
/**
* The minimimal number of header entries needed to successfully create an entry
*/
public override requiredHeaderLength = 1;

public errorList = {
Duplicates: `This user already exists`,
NoName: `Entry has no valid name`,
DuplicateImport: `Entry cannot be imported twice. This line will be ommitted`,
Expand All @@ -22,9 +26,17 @@ export class AccountImportService extends BaseUserImportService {
vote_weight: `The vote weight has too many decimal places (max.: 6).`
};

public override readonly verboseSummaryTitles: { [title: string]: string } = {
total: _(`Total accounts`),
created: _(`Accounts created`),
updated: _(`Accounts updated`),
error: _(`Accounts with errors`),
warning: _(`Accounts with warnings (will be skipped)`)
};

public constructor(
importServiceCollector: ImportServiceCollectorService,
private repo: UserControllerService,
private repo: AccountControllerService,
private exporter: AccountExportService
) {
super(importServiceCollector);
Expand All @@ -34,13 +46,14 @@ export class AccountImportService extends BaseUserImportService {
this.exporter.downloadCsvImportExample();
}

protected getConfig(): ImportConfig<GeneralUser> {
return {
modelHeadersAndVerboseNames: accountHeadersAndVerboseNames,
verboseNameFn: plural => (plural ? `Accounts` : `Account`),
getDuplicatesFn: (entry: Partial<GeneralUser>) =>
this.repo.getViewModelList().filter(user => user.username === entry.username),
createFn: (entries: any[]) => this.repo.create(...entries)
};
protected async import(
actionWorkerIds: number[],
abort = false
): Promise<void | (BackendImportRawPreview | void)[]> {
return await this.repo.import(actionWorkerIds.map(id => ({ id, import: !abort }))).resolve();
}

protected async jsonUpload(payload: { [key: string]: any }): Promise<void | BackendImportRawPreview[]> {
return await this.repo.jsonUpload(payload).resolve();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ <h2>{{ 'Accounts' | translate }}</h2>
(deselectAll)="deselectAll()"
(deleting)="deleteUsers()"
>
<button mat-menu-item [disabled]="!selectedRows.length" (click)="csvExportMemberList(selectedRows)">
<mat-icon>archive</mat-icon>
<span>{{ 'Export as CSV' | translate }}</span>
</button>
<button mat-menu-item [disabled]="!selectedRows.length" (click)="assignMeetingToUsers()">
<mat-icon>event_available</mat-icon>
<span>{{ 'Set/remove meeting' | translate }} ...</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ export class AccountListComponent extends BaseListViewComponent<ViewUser> {
}
}

public csvExportMemberList(): void {
this.exporter.downloadAccountCsvFile(this.listComponent.source);
public csvExportMemberList(users = this.listComponent.source): void {
this.exporter.downloadAccountCsvFile(users);
}

public getOmlByUser(user: ViewUser): string {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { UserExport } from 'src/app/domain/models/users/user.export';
import { CsvExportService } from 'src/app/gateways/export/csv-export.service';
import { CsvExportForBackendService } from 'src/app/gateways/export/csv-export.service/csv-export-for-backend.service';
import { ViewUser } from 'src/app/site/pages/meetings/view-models/view-user';

import { AccountCsvExportExample } from '../../export/csv-export-example';
Expand All @@ -12,7 +12,7 @@ import { AccountExportServiceModule } from '../account-export-service.module';
providedIn: AccountExportServiceModule
})
export class AccountExportService {
public constructor(private csvExportService: CsvExportService, private translate: TranslateService) {}
public constructor(private csvExportService: CsvExportForBackendService, private translate: TranslateService) {}

public downloadCsvImportExample(): void {
this.csvExportService.dummyCSVExport<UserExport>(
Expand All @@ -25,9 +25,8 @@ export class AccountExportService {
public downloadAccountCsvFile(dataSource: ViewUser[]): void {
this.csvExportService.export(
dataSource,
Object.entries(accountHeadersAndVerboseNames).map(([key, value]) => ({
property: key as keyof ViewUser,
label: value
Object.keys(accountHeadersAndVerboseNames).map(key => ({
property: key as keyof ViewUser
})),
`${this.translate.instant(`Accounts`)}.csv`
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ViewMeeting } from 'src/app/site/pages/meetings/view-models/view-meetin
import { ViewUser } from 'src/app/site/pages/meetings/view-models/view-user';
import { ControllerServiceCollectorService } from 'src/app/site/services/controller-service-collector.service';
import { OperatorService } from 'src/app/site/services/operator.service';
import { BackendImportRawPreview } from 'src/app/ui/modules/import-list/definitions/backend-import-preview';
import { PromptService } from 'src/app/ui/modules/prompt-dialog';

import { AccountCommonServiceModule } from './account-common-service.module';
Expand Down Expand Up @@ -75,4 +76,12 @@ export class AccountControllerService extends BaseController<ViewUser, User> {
}
return answer as boolean;
}

public jsonUpload(payload: { [key: string]: any }): Action<BackendImportRawPreview> {
return this.repo.accountJsonUpload(payload);
}

public import(payload: { id: number; import: boolean }[]): Action<BackendImportRawPreview | void> {
return this.repo.accountImport(payload);
}
}
Loading

0 comments on commit 74438b4

Please sign in to comment.