Merge pull request #5104 from jsangmeister/csv-import-fix

Fixed CSV import
This commit is contained in:
Emanuel Schütze 2019-11-06 11:03:08 +01:00 committed by GitHub
commit d286378524
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 158 additions and 270 deletions

View File

@ -209,9 +209,9 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
* *
* @param newEntries * @param newEntries
*/ */
public async bulkCreate(newEntries: NewEntry<ViewUser>[]): Promise<number[]> { public async bulkCreate(newEntries: NewEntry<User>[]): Promise<number[]> {
const data = newEntries.map(entry => { const data = newEntries.map(entry => {
return { ...entry.newEntry.user, importTrackId: entry.importTrackId }; return { ...entry.newEntry, importTrackId: entry.importTrackId };
}); });
const response = (await this.httpService.post(`/rest/users/user/mass_import/`, { users: data })) as { const response = (await this.httpService.post(`/rest/users/user/mass_import/`, { users: data })) as {
detail: string; detail: string;

View File

@ -5,7 +5,7 @@ import { TranslateService } from '@ngx-translate/core';
import { Papa, ParseConfig } from 'ngx-papaparse'; import { Papa, ParseConfig } from 'ngx-papaparse';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { BaseViewModel } from 'app/site/base/base-view-model'; import { BaseModel } from 'app/shared/models/base/base-model';
/** /**
* Interface for value- Label combinations. * Interface for value- Label combinations.
@ -53,7 +53,7 @@ type CsvImportStatus = 'new' | 'error' | 'done';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export abstract class BaseImportService<V extends BaseViewModel> { export abstract class BaseImportService<M extends BaseModel> {
/** /**
* List of possible errors and their verbose explanation * List of possible errors and their verbose explanation
*/ */
@ -127,12 +127,12 @@ export abstract class BaseImportService<V extends BaseViewModel> {
/** /**
* the list of parsed models that have been extracted from the opened file * the list of parsed models that have been extracted from the opened file
*/ */
private _entries: NewEntry<V>[] = []; private _entries: NewEntry<M>[] = [];
/** /**
* BehaviorSubject for displaying a preview for the currently selected entries * BehaviorSubject for displaying a preview for the currently selected entries
*/ */
public newEntries = new BehaviorSubject<NewEntry<V>[]>([]); public newEntries = new BehaviorSubject<NewEntry<M>[]>([]);
/** /**
* Emits an error string to display if a file import cannot be done * Emits an error string to display if a file import cannot be done
@ -159,7 +159,7 @@ export abstract class BaseImportService<V extends BaseViewModel> {
* Returns the current entries. For internal use in extending classes, as it * Returns the current entries. For internal use in extending classes, as it
* might not be filled with data at all times (see {@link newEntries} for a BehaviorSubject) * might not be filled with data at all times (see {@link newEntries} for a BehaviorSubject)
*/ */
protected get entries(): NewEntry<V>[] { protected get entries(): NewEntry<M>[] {
return this._entries; return this._entries;
} }
@ -220,7 +220,7 @@ export abstract class BaseImportService<V extends BaseViewModel> {
* *
* @param entries: an array of prepared newEntry objects * @param entries: an array of prepared newEntry objects
*/ */
public setParsedEntries(entries: NewEntry<V>[]): void { public setParsedEntries(entries: NewEntry<M>[]): void {
this.clearData(); this.clearData();
this.clearPreview(); this.clearPreview();
if (!entries) { if (!entries) {
@ -236,7 +236,7 @@ export abstract class BaseImportService<V extends BaseViewModel> {
* returning a new entry object * returning a new entry object
* @param line a line extracted by the CSV (not including the header) * @param line a line extracted by the CSV (not including the header)
*/ */
public abstract mapData(line: string): NewEntry<V>; public abstract mapData(line: string): NewEntry<M>;
/** /**
* Trigger for executing the import. * Trigger for executing the import.
@ -279,7 +279,7 @@ export abstract class BaseImportService<V extends BaseViewModel> {
* *
* @returns an observable BehaviorSubject * @returns an observable BehaviorSubject
*/ */
public getNewEntries(): Observable<NewEntry<V>[]> { public getNewEntries(): Observable<NewEntry<M>[]> {
return this.newEntries.asObservable(); return this.newEntries.asObservable();
} }
@ -357,7 +357,7 @@ export abstract class BaseImportService<V extends BaseViewModel> {
/** /**
* set a list of short names for error, indicating which column failed * set a list of short names for error, indicating which column failed
*/ */
public setError(entry: NewEntry<V>, error: string): void { public setError(entry: NewEntry<M>, error: string): void {
if (this.errorList.hasOwnProperty(error)) { if (this.errorList.hasOwnProperty(error)) {
if (!entry.errors) { if (!entry.errors) {
entry.errors = [error]; entry.errors = [error];
@ -385,7 +385,7 @@ export abstract class BaseImportService<V extends BaseViewModel> {
* @param error The error to check for * @param error The error to check for
* @returns true if the error is present * @returns true if the error is present
*/ */
public hasError(entry: NewEntry<V>, error: string): boolean { public hasError(entry: NewEntry<M>, error: string): boolean {
return entry.errors.includes(error); return entry.errors.includes(error);
} }
} }

View File

@ -1,15 +1,15 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { AgendaImportListComponent } from './components/agenda-import-list/agenda-import-list.component';
import { AgendaListComponent } from './components/agenda-list/agenda-list.component'; import { AgendaListComponent } from './components/agenda-list/agenda-list.component';
import { AgendaSortComponent } from './components/agenda-sort/agenda-sort.component'; import { AgendaSortComponent } from './components/agenda-sort/agenda-sort.component';
import { WatchForChangesGuard } from 'app/shared/utils/watch-for-changes.guard'; import { WatchForChangesGuard } from 'app/shared/utils/watch-for-changes.guard';
import { TopicImportListComponent } from 'app/site/topics/components/topic-import-list/topic-import-list.component';
import { ListOfSpeakersComponent } from './components/list-of-speakers/list-of-speakers.component'; import { ListOfSpeakersComponent } from './components/list-of-speakers/list-of-speakers.component';
const routes: Routes = [ const routes: Routes = [
{ path: '', component: AgendaListComponent, pathMatch: 'full' }, { path: '', component: AgendaListComponent, pathMatch: 'full' },
{ path: 'import', component: AgendaImportListComponent, data: { basePerm: 'agenda.can_manage' } }, { path: 'import', component: TopicImportListComponent, data: { basePerm: 'agenda.can_manage' } },
{ {
path: 'sort-agenda', path: 'sort-agenda',
component: AgendaSortComponent, component: AgendaSortComponent,

View File

@ -1,10 +1,10 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { AgendaImportListComponent } from './components/agenda-import-list/agenda-import-list.component';
import { AgendaListComponent } from './components/agenda-list/agenda-list.component'; import { AgendaListComponent } from './components/agenda-list/agenda-list.component';
import { AgendaRoutingModule } from './agenda-routing.module'; import { AgendaRoutingModule } from './agenda-routing.module';
import { AgendaSortComponent } from './components/agenda-sort/agenda-sort.component'; import { AgendaSortComponent } from './components/agenda-sort/agenda-sort.component';
import { TopicImportListComponent } from 'app/site/topics/components/topic-import-list/topic-import-list.component';
import { ItemInfoDialogComponent } from './components/item-info-dialog/item-info-dialog.component'; import { ItemInfoDialogComponent } from './components/item-info-dialog/item-info-dialog.component';
import { ListOfSpeakersComponent } from './components/list-of-speakers/list-of-speakers.component'; import { ListOfSpeakersComponent } from './components/list-of-speakers/list-of-speakers.component';
import { SharedModule } from '../../shared/shared.module'; import { SharedModule } from '../../shared/shared.module';
@ -18,7 +18,7 @@ import { SharedModule } from '../../shared/shared.module';
declarations: [ declarations: [
AgendaListComponent, AgendaListComponent,
ItemInfoDialogComponent, ItemInfoDialogComponent,
AgendaImportListComponent, TopicImportListComponent,
AgendaSortComponent, AgendaSortComponent,
ListOfSpeakersComponent ListOfSpeakersComponent
] ]

View File

@ -8,15 +8,15 @@ import { TranslateService } from '@ngx-translate/core';
import { auditTime } from 'rxjs/operators'; import { auditTime } from 'rxjs/operators';
import { BaseImportService, NewEntry, ValueLabelCombination } from 'app/core/ui-services/base-import.service'; import { BaseImportService, NewEntry, ValueLabelCombination } from 'app/core/ui-services/base-import.service';
import { BaseModel } from 'app/shared/models/base/base-model';
import { getLongPreview, getShortPreview } from 'app/shared/utils/previewStrings'; import { getLongPreview, getShortPreview } from 'app/shared/utils/previewStrings';
import { BaseViewComponent } from './base-view'; import { BaseViewComponent } from './base-view';
import { BaseViewModel } from './base-view-model';
export abstract class BaseImportListComponent<V extends BaseViewModel> extends BaseViewComponent implements OnInit { export abstract class BaseImportListComponent<M extends BaseModel> extends BaseViewComponent implements OnInit {
/** /**
* The data source for a table. Requires to be initialised with a BaseViewModel * The data source for a table. Requires to be initialised with a BaseViewModel
*/ */
public dataSource: MatTableDataSource<NewEntry<V>>; public dataSource: MatTableDataSource<NewEntry<M>>;
/** /**
* Helper function for previews * Helper function for previews
@ -48,7 +48,7 @@ export abstract class BaseImportListComponent<V extends BaseViewModel> extends B
* The table itself * The table itself
*/ */
@ViewChild(MatTable, { static: false }) @ViewChild(MatTable, { static: false })
protected table: MatTable<NewEntry<V>>; protected table: MatTable<NewEntry<M>>;
/** /**
* @returns the amount of total item successfully parsed * @returns the amount of total item successfully parsed
@ -112,7 +112,7 @@ export abstract class BaseImportListComponent<V extends BaseViewModel> extends B
*/ */
public constructor( public constructor(
protected importer: BaseImportService<V>, protected importer: BaseImportService<M>,
titleService: Title, titleService: Title,
translate: TranslateService, translate: TranslateService,
matSnackBar: MatSnackBar matSnackBar: MatSnackBar
@ -204,7 +204,7 @@ export abstract class BaseImportListComponent<V extends BaseViewModel> extends B
* @param row a newEntry object with a current status * @param row a newEntry object with a current status
* @returns a css class name * @returns a css class name
*/ */
public getStateClass(row: NewEntry<V>): string { public getStateClass(row: NewEntry<M>): string {
switch (row.status) { switch (row.status) {
case 'done': case 'done':
return 'import-done import-decided'; return 'import-done import-decided';
@ -220,7 +220,7 @@ export abstract class BaseImportListComponent<V extends BaseViewModel> extends B
* @param entry a newEntry object with a current status * @param entry a newEntry object with a current status
* @eturn the icon for the action of the item * @eturn the icon for the action of the item
*/ */
public getActionIcon(entry: NewEntry<V>): string { public getActionIcon(entry: NewEntry<M>): string {
switch (entry.status) { switch (entry.status) {
case 'error': // no import possible case 'error': // no import possible
return 'block'; return 'block';
@ -286,7 +286,7 @@ export abstract class BaseImportListComponent<V extends BaseViewModel> extends B
* @param error An error as defined as key of {@link errorList} * @param error An error as defined as key of {@link errorList}
* @returns true if the error is present in the entry described in the row * @returns true if the error is present in the entry described in the row
*/ */
public hasError(row: NewEntry<V>, error: string): boolean { public hasError(row: NewEntry<M>, error: string): boolean {
return this.importer.hasError(row, error); return this.importer.hasError(row, error);
} }
} }

View File

@ -6,14 +6,4 @@ import { Motion } from 'app/shared/models/motions/motion';
*/ */
export class CreateMotion extends Motion { export class CreateMotion extends Motion {
public submitters_id: number[]; public submitters_id: number[];
public category_id: number;
public motion_block_id: number;
public tags_id: number[];
public constructor(input?: any) {
super(input);
}
} }

View File

@ -1,5 +1,4 @@
import { CreateMotion } from './create-motion'; import { CreateMotion } from './create-motion';
import { ViewCreateMotion } from './view-create-motion';
/** /**
* Interface for correlating between strings representing BaseModels and existing * Interface for correlating between strings representing BaseModels and existing
@ -17,9 +16,7 @@ export interface CsvMapping {
* *
* @ignore * @ignore
*/ */
export class ViewCsvCreateMotion extends ViewCreateMotion { export class ImportCreateMotion extends CreateMotion {
protected _motion: CreateMotion;
/** /**
* Mapping for a new/existing category. * Mapping for a new/existing category.
*/ */
@ -40,10 +37,6 @@ export class ViewCsvCreateMotion extends ViewCreateMotion {
*/ */
public csvTags: CsvMapping[]; public csvTags: CsvMapping[];
public constructor(motion?: CreateMotion) {
super(motion);
}
/** /**
* takes a list of motion block mappings to update the current csvMotionblock. * takes a list of motion block mappings to update the current csvMotionblock.
* Returns the amount of entries that remain unmatched * Returns the amount of entries that remain unmatched
@ -54,13 +47,13 @@ export class ViewCsvCreateMotion extends ViewCreateMotion {
if (!this.csvMotionblock) { if (!this.csvMotionblock) {
return 0; return 0;
} else if (this.csvMotionblock.id) { } else if (this.csvMotionblock.id) {
this.motion.motion_block_id = this.csvMotionblock.id; this.motion_block_id = this.csvMotionblock.id;
return 0; return 0;
} else { } else {
const newBlock = motionBlocks.find(newMotionBlock => newMotionBlock.name === this.csvMotionblock.name); const newBlock = motionBlocks.find(newMotionBlock => newMotionBlock.name === this.csvMotionblock.name);
if (newBlock) { if (newBlock) {
this.csvMotionblock = newBlock; this.csvMotionblock = newBlock;
this.motion.motion_block_id = newBlock.id; this.motion_block_id = newBlock.id;
return 0; return 0;
} else { } else {
return 1; return 1;
@ -78,13 +71,13 @@ export class ViewCsvCreateMotion extends ViewCreateMotion {
if (!this.csvCategory) { if (!this.csvCategory) {
return 0; return 0;
} else if (this.csvCategory.id) { } else if (this.csvCategory.id) {
this.motion.category_id = this.csvCategory.id; this.category_id = this.csvCategory.id;
return 0; return 0;
} else { } else {
const newCat = categories.find(newCategory => newCategory.name === this.csvCategory.name); const newCat = categories.find(newCategory => newCategory.name === this.csvCategory.name);
if (newCat) { if (newCat) {
this.csvCategory = newCat; this.csvCategory = newCat;
this.motion.category_id = newCat.id; this.category_id = newCat.id;
return 0; return 0;
} else { } else {
return 1; return 1;
@ -118,7 +111,7 @@ export class ViewCsvCreateMotion extends ViewCreateMotion {
open += 1; open += 1;
} }
}); });
this.motion.submitters_id = ids; this.submitters_id = ids;
return open; return open;
} }
@ -149,7 +142,7 @@ export class ViewCsvCreateMotion extends ViewCreateMotion {
++open; ++open;
} }
} }
this.motion.tags_id = ids; this.tags_id = ids;
return open; return open;
} }
} }

View File

@ -4,8 +4,8 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Motion } from 'app/shared/models/motions/motion';
import { BaseImportListComponent } from 'app/site/base/base-import-list'; import { BaseImportListComponent } from 'app/site/base/base-import-list';
import { ViewMotion } from 'app/site/motions/models/view-motion';
import { MotionCsvExportService } from 'app/site/motions/services/motion-csv-export.service'; import { MotionCsvExportService } from 'app/site/motions/services/motion-csv-export.service';
import { MotionImportService } from 'app/site/motions/services/motion-import.service'; import { MotionImportService } from 'app/site/motions/services/motion-import.service';
@ -16,7 +16,7 @@ import { MotionImportService } from 'app/site/motions/services/motion-import.ser
selector: 'os-motion-import-list', selector: 'os-motion-import-list',
templateUrl: './motion-import-list.component.html' templateUrl: './motion-import-list.component.html'
}) })
export class MotionImportListComponent extends BaseImportListComponent<ViewMotion> { export class MotionImportListComponent extends BaseImportListComponent<Motion> {
/** /**
* Fetach a list of the headers expected by the importer, and prepare them * Fetach a list of the headers expected by the importer, and prepare them
* to be translateable (upper case) * to be translateable (upper case)

View File

@ -4,8 +4,8 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { StatuteParagraph } from 'app/shared/models/motions/statute-paragraph';
import { BaseImportListComponent } from 'app/site/base/base-import-list'; import { BaseImportListComponent } from 'app/site/base/base-import-list';
import { ViewStatuteParagraph } from 'app/site/motions/models/view-statute-paragraph';
import { StatuteCsvExportService } from 'app/site/motions/services/statute-csv-export.service'; import { StatuteCsvExportService } from 'app/site/motions/services/statute-csv-export.service';
import { StatuteImportService } from 'app/site/motions/services/statute-import.service'; import { StatuteImportService } from 'app/site/motions/services/statute-import.service';
@ -16,7 +16,7 @@ import { StatuteImportService } from 'app/site/motions/services/statute-import.s
selector: 'os-statute-import-list', selector: 'os-statute-import-list',
templateUrl: './statute-import-list.component.html' templateUrl: './statute-import-list.component.html'
}) })
export class StatuteImportListComponent extends BaseImportListComponent<ViewStatuteParagraph> { export class StatuteImportListComponent extends BaseImportListComponent<StatuteParagraph> {
/** /**
* Constructor for list view bases * Constructor for list view bases
* *

View File

@ -12,11 +12,11 @@ import { UserRepositoryService } from 'app/core/repositories/users/user-reposito
import { BaseImportService, NewEntry } from 'app/core/ui-services/base-import.service'; import { BaseImportService, NewEntry } from 'app/core/ui-services/base-import.service';
import { Tag } from 'app/shared/models/core/tag'; import { Tag } from 'app/shared/models/core/tag';
import { Category } from 'app/shared/models/motions/category'; import { Category } from 'app/shared/models/motions/category';
import { Motion } from 'app/shared/models/motions/motion';
import { MotionBlock } from 'app/shared/models/motions/motion-block'; import { MotionBlock } from 'app/shared/models/motions/motion-block';
import { CreateMotion } from '../models/create-motion'; import { CreateMotion } from '../models/create-motion';
import { CsvMapping, ImportCreateMotion } from '../models/import-create-motion';
import { motionExportOnly, motionImportExportHeaderOrder } from '../motions.constants'; import { motionExportOnly, motionImportExportHeaderOrder } from '../motions.constants';
import { CsvMapping, ViewCsvCreateMotion } from '../models/view-csv-create-motion';
import { ViewMotion } from '../models/view-motion';
/** /**
* Service for motion imports * Service for motion imports
@ -24,7 +24,7 @@ import { ViewMotion } from '../models/view-motion';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class MotionImportService extends BaseImportService<ViewMotion> { export class MotionImportService extends BaseImportService<Motion> {
/** /**
* List of possible errors and their verbose explanation * List of possible errors and their verbose explanation
*/ */
@ -104,8 +104,8 @@ export class MotionImportService extends BaseImportService<ViewMotion> {
* @param line * @param line
* @returns a new Entry representing a Motion * @returns a new Entry representing a Motion
*/ */
public mapData(line: string): NewEntry<ViewMotion> { public mapData(line: string): NewEntry<Motion> {
const newEntry = new ViewCsvCreateMotion(new CreateMotion()); const newEntry = new ImportCreateMotion(new CreateMotion());
const headerLength = Math.min(this.expectedHeader.length, line.length); const headerLength = Math.min(this.expectedHeader.length, line.length);
for (let idx = 0; idx < headerLength; idx++) { for (let idx = 0; idx < headerLength; idx++) {
switch (this.expectedHeader[idx]) { switch (this.expectedHeader[idx]) {
@ -122,11 +122,11 @@ export class MotionImportService extends BaseImportService<ViewMotion> {
newEntry.csvTags = this.getTags(line[idx]); newEntry.csvTags = this.getTags(line[idx]);
break; break;
default: default:
newEntry.motion[this.expectedHeader[idx]] = line[idx]; newEntry[this.expectedHeader[idx]] = line[idx];
} }
} }
const hasDuplicates = this.repo.getViewModelList().some(motion => motion.identifier === newEntry.identifier); const hasDuplicates = this.repo.getViewModelList().some(motion => motion.identifier === newEntry.identifier);
const entry: NewEntry<ViewMotion> = { const entry: NewEntry<Motion> = {
newEntry: newEntry, newEntry: newEntry,
hasDuplicates: hasDuplicates, hasDuplicates: hasDuplicates,
status: hasDuplicates ? 'error' : 'new', status: hasDuplicates ? 'error' : 'new',
@ -157,31 +157,31 @@ export class MotionImportService extends BaseImportService<ViewMotion> {
if (entry.status !== 'new') { if (entry.status !== 'new') {
continue; continue;
} }
const openBlocks = (entry.newEntry as ViewCsvCreateMotion).solveMotionBlocks(this.newMotionBlocks); const openBlocks = (entry.newEntry as ImportCreateMotion).solveMotionBlocks(this.newMotionBlocks);
if (openBlocks) { if (openBlocks) {
this.setError(entry, 'MotionBlock'); this.setError(entry, 'MotionBlock');
this.updatePreview(); this.updatePreview();
continue; continue;
} }
const openCategories = (entry.newEntry as ViewCsvCreateMotion).solveCategory(this.newCategories); const openCategories = (entry.newEntry as ImportCreateMotion).solveCategory(this.newCategories);
if (openCategories) { if (openCategories) {
this.setError(entry, 'Category'); this.setError(entry, 'Category');
this.updatePreview(); this.updatePreview();
continue; continue;
} }
const openUsers = (entry.newEntry as ViewCsvCreateMotion).solveSubmitters(this.newSubmitters); const openUsers = (entry.newEntry as ImportCreateMotion).solveSubmitters(this.newSubmitters);
if (openUsers) { if (openUsers) {
this.setError(entry, 'Submitters'); this.setError(entry, 'Submitters');
this.updatePreview(); this.updatePreview();
continue; continue;
} }
const openTags = (entry.newEntry as ViewCsvCreateMotion).solveTags(this.newTags); const openTags = (entry.newEntry as ImportCreateMotion).solveTags(this.newTags);
if (openTags) { if (openTags) {
this.setError(entry, 'Tags'); this.setError(entry, 'Tags');
this.updatePreview(); this.updatePreview();
continue; continue;
} }
await this.repo.create((entry.newEntry as ViewCsvCreateMotion).motion); await this.repo.create(entry.newEntry as ImportCreateMotion);
entry.status = 'done'; entry.status = 'done';
} }
this.updatePreview(); this.updatePreview();

View File

@ -7,7 +7,6 @@ import { Papa } from 'ngx-papaparse';
import { StatuteParagraphRepositoryService } from 'app/core/repositories/motions/statute-paragraph-repository.service'; import { StatuteParagraphRepositoryService } from 'app/core/repositories/motions/statute-paragraph-repository.service';
import { BaseImportService, NewEntry } from 'app/core/ui-services/base-import.service'; import { BaseImportService, NewEntry } from 'app/core/ui-services/base-import.service';
import { StatuteParagraph } from 'app/shared/models/motions/statute-paragraph'; import { StatuteParagraph } from 'app/shared/models/motions/statute-paragraph';
import { ViewStatuteParagraph } from '../models/view-statute-paragraph';
/** /**
* Service for motion imports * Service for motion imports
@ -15,7 +14,7 @@ import { ViewStatuteParagraph } from '../models/view-statute-paragraph';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class StatuteImportService extends BaseImportService<ViewStatuteParagraph> { export class StatuteImportService extends BaseImportService<StatuteParagraph> {
/** /**
* List of possible errors and their verbose explanation * List of possible errors and their verbose explanation
*/ */
@ -60,16 +59,16 @@ export class StatuteImportService extends BaseImportService<ViewStatuteParagraph
* @param line * @param line
* @returns a new Entry representing a Motion * @returns a new Entry representing a Motion
*/ */
public mapData(line: string): NewEntry<ViewStatuteParagraph> { public mapData(line: string): NewEntry<StatuteParagraph> {
const newEntry = new ViewStatuteParagraph(new StatuteParagraph()); const newEntry = new StatuteParagraph(new StatuteParagraph());
const headerLength = Math.min(this.expectedHeader.length, line.length); const headerLength = Math.min(this.expectedHeader.length, line.length);
for (let idx = 0; idx < headerLength; idx++) { for (let idx = 0; idx < headerLength; idx++) {
switch (this.expectedHeader[idx]) { switch (this.expectedHeader[idx]) {
case 'title': case 'title':
newEntry.statuteParagraph.title = line[idx]; newEntry.title = line[idx];
break; break;
case 'text': case 'text':
newEntry.statuteParagraph.text = line[idx]; newEntry.text = line[idx];
break; break;
} }
} }
@ -91,7 +90,7 @@ export class StatuteImportService extends BaseImportService<ViewStatuteParagraph
if (entry.status !== 'new') { if (entry.status !== 'new') {
continue; continue;
} }
await this.repo.create(entry.newEntry.statuteParagraph); await this.repo.create(entry.newEntry);
entry.status = 'done'; entry.status = 'done';
} }
this.updatePreview(); this.updatePreview();

View File

@ -197,21 +197,21 @@
</ng-container> </ng-container>
<!-- duration column --> <!-- duration column -->
<ng-container matColumnDef="duration"> <ng-container matColumnDef="agenda_duration">
<mat-header-cell *matHeaderCellDef translate>Duration</mat-header-cell> <mat-header-cell *matHeaderCellDef translate>Duration</mat-header-cell>
<mat-cell *matCellDef="let entry"> {{ getDuration(entry.newEntry.duration) }} </mat-cell> <mat-cell *matCellDef="let entry"> {{ getDuration(entry.newEntry.agenda_duration) }} </mat-cell>
</ng-container> </ng-container>
<!-- comment column--> <!-- comment column-->
<ng-container matColumnDef="comment"> <ng-container matColumnDef="agenda_comment">
<mat-header-cell *matHeaderCellDef translate>Comment</mat-header-cell> <mat-header-cell *matHeaderCellDef translate>Comment</mat-header-cell>
<mat-cell *matCellDef="let entry"> {{ entry.newEntry.comment }} </mat-cell> <mat-cell *matCellDef="let entry"> {{ entry.newEntry.agenda_comment }} </mat-cell>
</ng-container> </ng-container>
<!-- type column --> <!-- type column -->
<ng-container matColumnDef="type"> <ng-container matColumnDef="agenda_type">
<mat-header-cell *matHeaderCellDef translate>Type</mat-header-cell> <mat-header-cell *matHeaderCellDef translate>Type</mat-header-cell>
<mat-cell *matCellDef="let entry"> {{ getTypeString(entry.newEntry.type) | translate }} </mat-cell> <mat-cell *matCellDef="let entry"> {{ getTypeString(entry.newEntry.agenda_type) | translate }} </mat-cell>
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row> <mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>

View File

@ -2,21 +2,21 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module'; import { E2EImportsModule } from 'e2e-imports.module';
import { AgendaImportListComponent } from './agenda-import-list.component'; import { TopicImportListComponent } from './topic-import-list.component';
describe('AgendaImportListComponent', () => { describe('TopicImportListComponent', () => {
let component: AgendaImportListComponent; let component: TopicImportListComponent;
let fixture: ComponentFixture<AgendaImportListComponent>; let fixture: ComponentFixture<TopicImportListComponent>;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [AgendaImportListComponent], declarations: [TopicImportListComponent],
imports: [E2EImportsModule] imports: [E2EImportsModule]
}).compileComponents(); }).compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(AgendaImportListComponent); fixture = TestBed.createComponent(TopicImportListComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@ -5,21 +5,21 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { AgendaImportService } from '../../services/agenda-import.service';
import { CsvExportService } from 'app/core/ui-services/csv-export.service'; import { CsvExportService } from 'app/core/ui-services/csv-export.service';
import { DurationService } from 'app/core/ui-services/duration.service'; import { DurationService } from 'app/core/ui-services/duration.service';
import { ItemVisibilityChoices } from 'app/shared/models/agenda/item'; import { ItemVisibilityChoices } from 'app/shared/models/agenda/item';
import { BaseImportListComponent } from 'app/site/base/base-import-list'; import { BaseImportListComponent } from 'app/site/base/base-import-list';
import { ViewCreateTopic } from 'app/site/topics/models/view-create-topic'; import { CreateTopic } from 'app/site/topics/models/create-topic';
import { TopicImportService } from '../../../topics/services/topic-import.service';
/** /**
* Component for the agenda import list view. * Component for the agenda import list view.
*/ */
@Component({ @Component({
selector: 'os-agenda-import-list', selector: 'os-topic-import-list',
templateUrl: './agenda-import-list.component.html' templateUrl: './topic-import-list.component.html'
}) })
export class AgendaImportListComponent extends BaseImportListComponent<ViewCreateTopic> { export class TopicImportListComponent extends BaseImportListComponent<CreateTopic> {
/** /**
* A form for text input * A form for text input
*/ */
@ -40,7 +40,7 @@ export class AgendaImportListComponent extends BaseImportListComponent<ViewCreat
titleService: Title, titleService: Title,
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
translate: TranslateService, translate: TranslateService,
importer: AgendaImportService, importer: TopicImportService,
formBuilder: FormBuilder, formBuilder: FormBuilder,
private exporter: CsvExportService, private exporter: CsvExportService,
private durationService: DurationService private durationService: DurationService
@ -81,7 +81,7 @@ export class AgendaImportListComponent extends BaseImportListComponent<ViewCreat
* Sends the data in the text field input area to the importer * Sends the data in the text field input area to the importer
*/ */
public parseTextArea(): void { public parseTextArea(): void {
(this.importer as AgendaImportService).parseTextArea(this.textAreaForm.get('inputtext').value); (this.importer as TopicImportService).parseTextArea(this.textAreaForm.get('inputtext').value);
} }
/** /**

View File

@ -14,4 +14,13 @@ export class CreateTopic extends Topic {
public constructor(input?: any) { public constructor(input?: any) {
super(input); super(input);
} }
/**
* Checks if the CreateTopic is valid. Currently only requires an existing title
*
* @returns true if it is a valid Topic
*/
public get isValid(): boolean {
return this.title ? true : false;
}
} }

View File

@ -1,114 +0,0 @@
import { CreateTopic } from './create-topic';
import { ViewTopic } from './view-topic';
/**
* View model for Topic('Agenda item') creation.
*
*/
export class ViewCreateTopic extends ViewTopic {
public get topic(): CreateTopic {
return this._model as CreateTopic;
}
/**
* Fetches the field representing the new title
*
* @returns title string as set during import (may be different from getTitle)
*/
public get title(): string {
return this.topic.title;
}
/**
* Setter for the title. Sets the title of the underlying CreateTopic
*
* @param title
*/
public set title(title: string) {
this.topic.title = title;
}
/**
* @returns the duration in minutes
*/
public get duration(): number {
return this.topic.agenda_duration;
}
/**
* Setter for the duration. Expects values as in {@link DurationService}
*/
public set duration(duration: number) {
this.topic.agenda_duration = duration;
}
/**
* @returns the comment string as set during the import
*/
public get comment(): string {
return this.topic.agenda_comment;
}
/**
* Sets the comment string of the underlying topic
* @param comment A string to set as comment
*/
public set comment(comment: string) {
this.topic.agenda_comment = comment;
}
/**
* @returns a number representing the item type
*/
public get type(): number {
return this.topic.agenda_type;
}
/**
* sets the item type for the topic's agenda entry. No validation is done here.
*
* @param A number representing the item's type. See {@link itemVisibilityChoices}
* for the interpretation of type numbers.
*/
public set type(type: number) {
this.topic.agenda_type = type;
}
/**
* Sets the text string of the underlying topic
*
* @param text A string.
*/
public set text(text: string) {
this.topic.text = text;
}
/**
* @returns the comment string of the underlying topic
*/
public get text(): string {
return this.topic.text;
}
/**
* Checks if the CreateTopic is valid. Currently only requires an existing title
*
* @returns true if it is a valid Topic
*/
public get isValid(): boolean {
return this.title ? true : false;
}
/**
* Constructor. Empty
*
* @param topic A CreateTopic
*/
public constructor(topic: CreateTopic) {
super(topic);
}
public getVerboseName = () => {
throw new Error('This should not be used');
};
}

View File

@ -2,9 +2,9 @@ import { TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module'; import { E2EImportsModule } from 'e2e-imports.module';
import { AgendaImportService } from './agenda-import.service'; import { TopicImportService } from './topic-import.service';
describe('AgendaImportService', () => { describe('TopicImportService', () => {
beforeEach(() => beforeEach(() =>
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [E2EImportsModule] imports: [E2EImportsModule]
@ -12,7 +12,7 @@ describe('AgendaImportService', () => {
); );
it('should be created', () => { it('should be created', () => {
const service: AgendaImportService = TestBed.get(AgendaImportService); const service: TopicImportService = TestBed.get(TopicImportService);
expect(service).toBeTruthy(); expect(service).toBeTruthy();
}); });
}); });

View File

@ -8,18 +8,17 @@ import { TopicRepositoryService } from 'app/core/repositories/topics/topic-repos
import { BaseImportService, NewEntry } from 'app/core/ui-services/base-import.service'; import { BaseImportService, NewEntry } from 'app/core/ui-services/base-import.service';
import { DurationService } from 'app/core/ui-services/duration.service'; import { DurationService } from 'app/core/ui-services/duration.service';
import { ItemVisibilityChoices } from 'app/shared/models/agenda/item'; import { ItemVisibilityChoices } from 'app/shared/models/agenda/item';
import { ViewCreateTopic } from 'app/site/topics/models/view-create-topic'; import { CreateTopic } from '../models/create-topic';
import { CreateTopic } from '../../topics/models/create-topic';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class AgendaImportService extends BaseImportService<ViewCreateTopic> { export class TopicImportService extends BaseImportService<CreateTopic> {
/** /**
* Helper for mapping the expected header in a typesafe way. Values will be passed to * Helper for mapping the expected header in a typesafe way. Values will be passed to
* {@link expectedHeader} * {@link expectedHeader}
*/ */
public headerMap: (keyof ViewCreateTopic)[] = ['title', 'text', 'duration', 'comment', 'type']; public headerMap: (keyof CreateTopic)[] = ['title', 'text', 'agenda_duration', 'agenda_comment', 'agenda_type'];
/** /**
* The minimimal number of header entries needed to successfully create an entry * The minimimal number of header entries needed to successfully create an entry
@ -31,7 +30,7 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
*/ */
public errorList = { public errorList = {
NoTitle: 'A Topic needs a title', NoTitle: 'A Topic needs a title',
Duplicates: 'A topic tiwh this title already exists', Duplicates: 'A topic with this title already exists',
ParsingErrors: 'Some csv values could not be read correctly.' ParsingErrors: 'Some csv values could not be read correctly.'
}; };
@ -67,17 +66,17 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
* @param line a line extracted by the CSV (without the header) * @param line a line extracted by the CSV (without the header)
* @returns a new entry for a Topic * @returns a new entry for a Topic
*/ */
public mapData(line: string): NewEntry<ViewCreateTopic> { public mapData(line: string): NewEntry<CreateTopic> {
const newEntry = new ViewCreateTopic(new CreateTopic()); const newEntry = new CreateTopic();
const headerLength = Math.min(this.expectedHeader.length, line.length); const headerLength = Math.min(this.expectedHeader.length, line.length);
let hasErrors = false; let hasErrors = false;
for (let idx = 0; idx < headerLength; idx++) { for (let idx = 0; idx < headerLength; idx++) {
switch (this.expectedHeader[idx]) { switch (this.expectedHeader[idx]) {
case 'duration': case 'agenda_duration':
try { try {
const duration = this.parseDuration(line[idx]); const duration = this.parseDuration(line[idx]);
if (duration > 0) { if (duration > 0) {
newEntry.duration = duration; newEntry.agenda_duration = duration;
} }
} catch (e) { } catch (e) {
if (e instanceof TypeError) { if (e instanceof TypeError) {
@ -86,9 +85,9 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
} }
} }
break; break;
case 'type': case 'agenda_type':
try { try {
newEntry.type = this.parseType(line[idx]); newEntry.agenda_type = this.parseType(line[idx]);
} catch (e) { } catch (e) {
if (e instanceof TypeError) { if (e instanceof TypeError) {
hasErrors = true; hasErrors = true;
@ -103,10 +102,10 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
const hasDuplicates = this.repo.getViewModelList().some(topic => topic.title === newEntry.title); const hasDuplicates = this.repo.getViewModelList().some(topic => topic.title === newEntry.title);
// set type to 'public' if none is given in import // set type to 'public' if none is given in import
if (!newEntry.type) { if (!newEntry.agenda_type) {
newEntry.type = 1; newEntry.agenda_type = 1;
} }
const mappedEntry: NewEntry<ViewCreateTopic> = { const mappedEntry: NewEntry<CreateTopic> = {
newEntry: newEntry, newEntry: newEntry,
hasDuplicates: hasDuplicates, hasDuplicates: hasDuplicates,
status: 'new', status: 'new',
@ -133,7 +132,7 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
if (entry.status !== 'new') { if (entry.status !== 'new') {
continue; continue;
} }
await this.repo.create(entry.newEntry.topic); await this.repo.create(entry.newEntry);
entry.status = 'done'; entry.status = 'done';
} }
this.updatePreview(); this.updatePreview();
@ -183,7 +182,7 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
* @param data a string as produced by textArea input * @param data a string as produced by textArea input
*/ */
public parseTextArea(data: string): void { public parseTextArea(data: string): void {
const newEntries: NewEntry<ViewCreateTopic>[] = []; const newEntries: NewEntry<CreateTopic>[] = [];
this.clearData(); this.clearData();
this.clearPreview(); this.clearPreview();
const lines = data.split('\n'); const lines = data.split('\n');
@ -191,14 +190,14 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
if (!line.length) { if (!line.length) {
return; return;
} }
const newTopic = new ViewCreateTopic( const newTopic = new CreateTopic(
new CreateTopic({ new CreateTopic({
title: line, title: line,
agenda_type: 1 // set type to 'public item' by default agenda_type: 1 // set type to 'public item' by default
}) })
); );
const hasDuplicates = this.repo.getViewModelList().some(topic => topic.title === newTopic.title); const hasDuplicates = this.repo.getViewModelList().some(topic => topic.title === newTopic.title);
const newEntry: NewEntry<ViewCreateTopic> = { const newEntry: NewEntry<CreateTopic> = {
newEntry: newTopic, newEntry: newTopic,
hasDuplicates: hasDuplicates, hasDuplicates: hasDuplicates,
status: 'new', status: 'new',

View File

@ -12,6 +12,7 @@
<mat-tab-group (selectedTabChange)="onTabChange()"> <mat-tab-group (selectedTabChange)="onTabChange()">
<!-- textarea import tab --> <!-- textarea import tab -->
<mat-tab label="{{ 'Text import' | translate }}"> <mat-tab label="{{ 'Text import' | translate }}">
<br>
<div [formGroup]="textAreaForm"> <div [formGroup]="textAreaForm">
<div> <div>
<span translate> Copy and paste your participant names in this textbox.</span> <span translate> Copy and paste your participant names in this textbox.</span>
@ -36,19 +37,20 @@
</mat-tab> </mat-tab>
<!-- CSV import tab --> <!-- CSV import tab -->
<mat-tab label="{{ 'CSV import' | translate }}"> <mat-tab label="{{ 'CSV import' | translate }}">
<br>
<span translate <span translate
>Required comma or semicolon separated values with these column header names in the first row:</span >Required comma or semicolon separated values with these column header names in the first row:</span
>: <br /> ><br />
<div class="code red-warning-text"> <div class="code red-warning-text">
<span translate>Title</span>, <span translate>Given name</span>, <span translate>Surname</span> , <span translate>Title</span>, <span translate>Given name</span>, <span translate>Surname</span>,
<span translate>Structure level</span>, <span translate>Participant number</span>, <span translate>Structure level</span>, <span translate>Participant number</span>,
<span translate>Groups</span> , <span translate>Comment</span>, <span translate>Is active</span>, <span translate>Groups</span>, <span translate>Comment</span>, <span translate>Is active</span>,
<span translate>Is present</span> , <span translate>Is committee</span>, <span translate>Is present</span>, <span translate>Is committee</span>, <span translate>Initial password</span>,
<span translate>Initial password</span>, <span translate>Email</span> <span translate>Email</span>, <span translate>Username</span>, <span translate>Gender</span>
</div> </div>
<ul> <ul>
<li translate> <li translate>
At least given name or surname have to be filled in. All other fields are optional and may be empty. One of given name, surname and username has to be filled in. All other fields are optional and may be empty.
</li> </li>
<li translate> <li translate>
Additional columns after the required ones may be present and won't affect the import. Additional columns after the required ones may be present and won't affect the import.
@ -224,7 +226,7 @@
<ng-container matColumnDef="number"> <ng-container matColumnDef="number">
<mat-header-cell *matHeaderCellDef translate>Participant number</mat-header-cell> <mat-header-cell *matHeaderCellDef translate>Participant number</mat-header-cell>
<mat-cell *matCellDef="let entry"> {{ entry.newEntry.user.number }} </mat-cell> <mat-cell *matCellDef="let entry"> {{ entry.newEntry.number }} </mat-cell>
</ng-container> </ng-container>
<!-- groups column --> <!-- groups column -->
@ -277,6 +279,14 @@
<mat-header-cell *matHeaderCellDef translate>Email</mat-header-cell> <mat-header-cell *matHeaderCellDef translate>Email</mat-header-cell>
<mat-cell *matCellDef="let entry"> {{ entry.newEntry.email }} </mat-cell> <mat-cell *matCellDef="let entry"> {{ entry.newEntry.email }} </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="username">
<mat-header-cell *matHeaderCellDef translate>Username</mat-header-cell>
<mat-cell *matCellDef="let entry"> {{ entry.newEntry.username }} </mat-cell>
</ng-container>
<ng-container matColumnDef="gender">
<mat-header-cell *matHeaderCellDef translate>Gender</mat-header-cell>
<mat-cell *matCellDef="let entry"> {{ entry.newEntry.gender }} </mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row> <mat-header-row *matHeaderRowDef="getColumnDefinition()"></mat-header-row>
<mat-row [ngClass]="getStateClass(row)" *matRowDef="let row; columns: getColumnDefinition()"> </mat-row> <mat-row [ngClass]="getStateClass(row)" *matRowDef="let row; columns: getColumnDefinition()"> </mat-row>

View File

@ -7,9 +7,9 @@ import { TranslateService } from '@ngx-translate/core';
import { NewEntry } from 'app/core/ui-services/base-import.service'; import { NewEntry } from 'app/core/ui-services/base-import.service';
import { CsvExportService } from 'app/core/ui-services/csv-export.service'; import { CsvExportService } from 'app/core/ui-services/csv-export.service';
import { User } from 'app/shared/models/users/user';
import { BaseImportListComponent } from 'app/site/base/base-import-list'; import { BaseImportListComponent } from 'app/site/base/base-import-list';
import { UserImportService } from '../../services/user-import.service'; import { UserImportService } from '../../services/user-import.service';
import { ViewUser } from '../../models/view-user';
/** /**
* Component for the user import list view. * Component for the user import list view.
@ -18,7 +18,7 @@ import { ViewUser } from '../../models/view-user';
selector: 'os-user-import-list', selector: 'os-user-import-list',
templateUrl: './user-import-list.component.html' templateUrl: './user-import-list.component.html'
}) })
export class UserImportListComponent extends BaseImportListComponent<ViewUser> { export class UserImportListComponent extends BaseImportListComponent<User> {
public textAreaForm: FormGroup; public textAreaForm: FormGroup;
/** /**
@ -59,7 +59,9 @@ export class UserImportListComponent extends BaseImportListComponent<ViewUser> {
'Is present', 'Is present',
'Is a committee', 'Is a committee',
'Initial password', 'Initial password',
'Email' 'Email',
'Username',
'Gender'
]; ];
const rows = [ const rows = [
[ [
@ -74,7 +76,9 @@ export class UserImportListComponent extends BaseImportListComponent<ViewUser> {
1, 1,
, ,
'initialPassword', 'initialPassword',
null null,
'mmustermann',
'm'
], ],
[ [
null, null,
@ -88,10 +92,12 @@ export class UserImportListComponent extends BaseImportListComponent<ViewUser> {
1, 1,
null, null,
null, null,
'john.doe@email.com' 'john.doe@email.com',
'jdoe',
'diverse'
], ],
[null, 'Fred', 'Bloggs', 'London', null, null, null, null, null, null, null, null], [null, 'Julia', 'Bloggs', 'London', null, null, null, null, null, null, null, null, 'jbloggs', 'f'],
[null, null, 'Executive Board', null, null, null, null, null, null, 1, null, null] [null, null, 'Executive Board', null, null, null, null, null, null, 1, null, null, 'executive', null]
]; ];
this.exporter.dummyCSVExport(headerRow, rows, `${this.translate.instant('participants-example')}.csv`); this.exporter.dummyCSVExport(headerRow, rows, `${this.translate.instant('participants-example')}.csv`);
} }
@ -102,7 +108,7 @@ export class UserImportListComponent extends BaseImportListComponent<ViewUser> {
* @param row * @param row
* @returns an error string similar to getVerboseError * @returns an error string similar to getVerboseError
*/ */
public nameErrors(row: NewEntry<ViewUser>): string { public nameErrors(row: NewEntry<User>): string {
for (const name of ['NoName', 'Duplicates', 'DuplicateImport']) { for (const name of ['NoName', 'Duplicates', 'DuplicateImport']) {
if (this.importer.hasError(row, name)) { if (this.importer.hasError(row, name)) {
return this.importer.verbose(name); return this.importer.verbose(name);

View File

@ -1,5 +1,4 @@
import { User } from 'app/shared/models/users/user'; import { User } from 'app/shared/models/users/user';
import { ViewUser } from './view-user';
/** /**
* Interface for correlating between strings representing BaseModels and existing * Interface for correlating between strings representing BaseModels and existing
@ -17,7 +16,7 @@ export interface CsvMapping {
* *
* @ignore * @ignore
*/ */
export class ViewCsvCreateUser extends ViewUser { export class ImportCreateUser extends User {
/** /**
* Mapping for a new/existing groups. * Mapping for a new/existing groups.
*/ */
@ -28,17 +27,10 @@ export class ViewCsvCreateUser extends ViewUser {
/** /**
* Getter if the minimum requrements for a user are met: A name * Getter if the minimum requrements for a user are met: A name
* *
* @returns false if the user has neither first nor last name * @returns false if the user has neither first nor last name nor username
*/ */
public get isValid(): boolean { public get isValid(): boolean {
if (this.user && (this.first_name || this.last_name)) { return !!(this.first_name || this.last_name || this.username);
return true;
}
return false;
}
public constructor(user?: User) {
super(user);
} }
/** /**
@ -67,7 +59,7 @@ export class ViewCsvCreateUser extends ViewUser {
open += 1; open += 1;
} }
}); });
this.user.groups_id = ids; this.groups_id = ids;
return open; return open;
} }
} }

View File

@ -9,18 +9,17 @@ import { UserRepositoryService } from 'app/core/repositories/users/user-reposito
import { BaseImportService, NewEntry } from 'app/core/ui-services/base-import.service'; import { BaseImportService, NewEntry } from 'app/core/ui-services/base-import.service';
import { Group } from 'app/shared/models/users/group'; import { Group } from 'app/shared/models/users/group';
import { User } from 'app/shared/models/users/user'; import { User } from 'app/shared/models/users/user';
import { CsvMapping, ViewCsvCreateUser } from '../models/view-csv-create-user'; import { CsvMapping, ImportCreateUser } from '../models/import-create-user';
import { ViewUser } from '../models/view-user';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class UserImportService extends BaseImportService<ViewUser> { export class UserImportService extends BaseImportService<User> {
/** /**
* Helper for mapping the expected header in a typesafe way. Values and order * Helper for mapping the expected header in a typesafe way. Values and order
* will be passed to {@link expectedHeader} * will be passed to {@link expectedHeader}
*/ */
public headerMap: (keyof ViewCsvCreateUser)[] = [ public headerMap: (keyof ImportCreateUser)[] = [
'title', 'title',
'first_name', 'first_name',
'last_name', 'last_name',
@ -32,7 +31,9 @@ export class UserImportService extends BaseImportService<ViewUser> {
'is_present', 'is_present',
'is_committee', 'is_committee',
'default_password', 'default_password',
'email' 'email',
'username',
'gender'
]; ];
/** /**
@ -91,8 +92,8 @@ export class UserImportService extends BaseImportService<ViewUser> {
* @param line * @param line
* @returns a new entry representing an User * @returns a new entry representing an User
*/ */
public mapData(line: string): NewEntry<ViewUser> { public mapData(line: string): NewEntry<User> {
const newViewUser = new ViewCsvCreateUser(new User()); const newViewUser = new ImportCreateUser();
const headerLength = Math.min(this.expectedHeader.length, line.length); const headerLength = Math.min(this.expectedHeader.length, line.length);
let hasErrors = false; let hasErrors = false;
for (let idx = 0; idx < headerLength; idx++) { for (let idx = 0; idx < headerLength; idx++) {
@ -104,7 +105,7 @@ export class UserImportService extends BaseImportService<ViewUser> {
case 'is_committee': case 'is_committee':
case 'is_present': case 'is_present':
try { try {
newViewUser.user[this.expectedHeader[idx]] = this.toBoolean(line[idx]); newViewUser[this.expectedHeader[idx]] = this.toBoolean(line[idx]);
} catch (e) { } catch (e) {
if (e instanceof TypeError) { if (e instanceof TypeError) {
console.log(e); console.log(e);
@ -114,10 +115,10 @@ export class UserImportService extends BaseImportService<ViewUser> {
} }
break; break;
case 'number': case 'number':
newViewUser.user.number = line[idx]; newViewUser.number = line[idx];
break; break;
default: default:
newViewUser.user[this.expectedHeader[idx]] = line[idx]; newViewUser[this.expectedHeader[idx]] = line[idx];
break; break;
} }
} }
@ -136,13 +137,13 @@ export class UserImportService extends BaseImportService<ViewUser> {
*/ */
public async doImport(): Promise<void> { public async doImport(): Promise<void> {
this.newGroups = await this.createNewGroups(); this.newGroups = await this.createNewGroups();
const importUsers: NewEntry<ViewUser>[] = []; const importUsers: NewEntry<User>[] = [];
let trackId = 1; let trackId = 1;
for (const entry of this.entries) { for (const entry of this.entries) {
if (entry.status !== 'new') { if (entry.status !== 'new') {
continue; continue;
} }
const openBlocks = (entry.newEntry as ViewCsvCreateUser).solveGroups(this.newGroups); const openBlocks = (entry.newEntry as ImportCreateUser).solveGroups(this.newGroups);
if (openBlocks) { if (openBlocks) {
this.setError(entry, 'Group'); this.setError(entry, 'Group');
this.updatePreview(); this.updatePreview();
@ -245,7 +246,7 @@ export class UserImportService extends BaseImportService<ViewUser> {
* @param data a string as produced by textArea input * @param data a string as produced by textArea input
*/ */
public parseTextArea(data: string): void { public parseTextArea(data: string): void {
const newEntries: NewEntry<ViewUser>[] = []; const newEntries: NewEntry<User>[] = [];
this.clearData(); this.clearData();
this.clearPreview(); this.clearPreview();
const lines = data.split('\n'); const lines = data.split('\n');
@ -254,7 +255,7 @@ export class UserImportService extends BaseImportService<ViewUser> {
return; return;
} }
const nameSchema = line.includes(',') ? 'lastCommaFirst' : 'firstSpaceLast'; const nameSchema = line.includes(',') ? 'lastCommaFirst' : 'firstSpaceLast';
const newUser = new ViewCsvCreateUser(this.repo.parseUserString(line, nameSchema)); const newUser = new ImportCreateUser(this.repo.parseUserString(line, nameSchema));
const newEntry = this.userToEntry(newUser); const newEntry = this.userToEntry(newUser);
newEntries.push(newEntry); newEntries.push(newEntry);
}); });
@ -267,15 +268,17 @@ export class UserImportService extends BaseImportService<ViewUser> {
* @param newUser * @param newUser
* @returns a NewEntry with duplicate/error information * @returns a NewEntry with duplicate/error information
*/ */
private userToEntry(newUser: ViewCsvCreateUser): NewEntry<ViewUser> { private userToEntry(newUser: ImportCreateUser): NewEntry<User> {
const newEntry: NewEntry<ViewUser> = { const newEntry: NewEntry<User> = {
newEntry: newUser, newEntry: newUser,
hasDuplicates: false, hasDuplicates: false,
status: 'new', status: 'new',
errors: [] errors: []
}; };
if (newUser.isValid) { if (newUser.isValid) {
newEntry.hasDuplicates = this.repo.getViewModelList().some(user => user.full_name === newUser.full_name); newEntry.hasDuplicates = this.repo
.getViewModelList()
.some(user => user.full_name === this.repo.getFullName(newUser));
if (newEntry.hasDuplicates) { if (newEntry.hasDuplicates) {
this.setError(newEntry, 'Duplicates'); this.setError(newEntry, 'Duplicates');
} }

View File

@ -352,6 +352,7 @@ class UserViewSet(ModelViewSet):
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
except ValidationError: except ValidationError:
# Skip invalid users. # Skip invalid users.
continue continue
data = serializer.prepare_password(serializer.data) data = serializer.prepare_password(serializer.data)
groups = data["groups_id"] groups = data["groups_id"]
@ -364,7 +365,7 @@ class UserViewSet(ModelViewSet):
if "importTrackId" in user: if "importTrackId" in user:
imported_track_ids.append(user["importTrackId"]) imported_track_ids.append(user["importTrackId"])
# Now infom all clients and send a response # Now inform all clients and send a response
inform_changed_data(created_users) inform_changed_data(created_users)
return Response( return Response(
{ {