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
*/
public async bulkCreate(newEntries: NewEntry<ViewUser>[]): Promise<number[]> {
public async bulkCreate(newEntries: NewEntry<User>[]): Promise<number[]> {
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 {
detail: string;

View File

@ -5,7 +5,7 @@ import { TranslateService } from '@ngx-translate/core';
import { Papa, ParseConfig } from 'ngx-papaparse';
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.
@ -53,7 +53,7 @@ type CsvImportStatus = 'new' | 'error' | 'done';
@Injectable({
providedIn: 'root'
})
export abstract class BaseImportService<V extends BaseViewModel> {
export abstract class BaseImportService<M extends BaseModel> {
/**
* 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
*/
private _entries: NewEntry<V>[] = [];
private _entries: NewEntry<M>[] = [];
/**
* 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
@ -159,7 +159,7 @@ export abstract class BaseImportService<V extends BaseViewModel> {
* 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)
*/
protected get entries(): NewEntry<V>[] {
protected get entries(): NewEntry<M>[] {
return this._entries;
}
@ -220,7 +220,7 @@ export abstract class BaseImportService<V extends BaseViewModel> {
*
* @param entries: an array of prepared newEntry objects
*/
public setParsedEntries(entries: NewEntry<V>[]): void {
public setParsedEntries(entries: NewEntry<M>[]): void {
this.clearData();
this.clearPreview();
if (!entries) {
@ -236,7 +236,7 @@ export abstract class BaseImportService<V extends BaseViewModel> {
* returning a new entry object
* @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.
@ -279,7 +279,7 @@ export abstract class BaseImportService<V extends BaseViewModel> {
*
* @returns an observable BehaviorSubject
*/
public getNewEntries(): Observable<NewEntry<V>[]> {
public getNewEntries(): Observable<NewEntry<M>[]> {
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
*/
public setError(entry: NewEntry<V>, error: string): void {
public setError(entry: NewEntry<M>, error: string): void {
if (this.errorList.hasOwnProperty(error)) {
if (!entry.errors) {
entry.errors = [error];
@ -385,7 +385,7 @@ export abstract class BaseImportService<V extends BaseViewModel> {
* @param error The error to check for
* @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);
}
}

View File

@ -1,15 +1,15 @@
import { NgModule } from '@angular/core';
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 { AgendaSortComponent } from './components/agenda-sort/agenda-sort.component';
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';
const routes: Routes = [
{ 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',
component: AgendaSortComponent,

View File

@ -1,10 +1,10 @@
import { CommonModule } from '@angular/common';
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 { AgendaRoutingModule } from './agenda-routing.module';
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 { ListOfSpeakersComponent } from './components/list-of-speakers/list-of-speakers.component';
import { SharedModule } from '../../shared/shared.module';
@ -18,7 +18,7 @@ import { SharedModule } from '../../shared/shared.module';
declarations: [
AgendaListComponent,
ItemInfoDialogComponent,
AgendaImportListComponent,
TopicImportListComponent,
AgendaSortComponent,
ListOfSpeakersComponent
]

View File

@ -8,15 +8,15 @@ import { TranslateService } from '@ngx-translate/core';
import { auditTime } from 'rxjs/operators';
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 { 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
*/
public dataSource: MatTableDataSource<NewEntry<V>>;
public dataSource: MatTableDataSource<NewEntry<M>>;
/**
* Helper function for previews
@ -48,7 +48,7 @@ export abstract class BaseImportListComponent<V extends BaseViewModel> extends B
* The table itself
*/
@ViewChild(MatTable, { static: false })
protected table: MatTable<NewEntry<V>>;
protected table: MatTable<NewEntry<M>>;
/**
* @returns the amount of total item successfully parsed
@ -112,7 +112,7 @@ export abstract class BaseImportListComponent<V extends BaseViewModel> extends B
*/
public constructor(
protected importer: BaseImportService<V>,
protected importer: BaseImportService<M>,
titleService: Title,
translate: TranslateService,
matSnackBar: MatSnackBar
@ -204,7 +204,7 @@ export abstract class BaseImportListComponent<V extends BaseViewModel> extends B
* @param row a newEntry object with a current status
* @returns a css class name
*/
public getStateClass(row: NewEntry<V>): string {
public getStateClass(row: NewEntry<M>): string {
switch (row.status) {
case 'done':
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
* @eturn the icon for the action of the item
*/
public getActionIcon(entry: NewEntry<V>): string {
public getActionIcon(entry: NewEntry<M>): string {
switch (entry.status) {
case 'error': // no import possible
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}
* @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);
}
}

View File

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

View File

@ -4,8 +4,8 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { Motion } from 'app/shared/models/motions/motion';
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 { 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',
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
* to be translateable (upper case)

View File

@ -4,8 +4,8 @@ import { Title } from '@angular/platform-browser';
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 { ViewStatuteParagraph } from 'app/site/motions/models/view-statute-paragraph';
import { StatuteCsvExportService } from 'app/site/motions/services/statute-csv-export.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',
templateUrl: './statute-import-list.component.html'
})
export class StatuteImportListComponent extends BaseImportListComponent<ViewStatuteParagraph> {
export class StatuteImportListComponent extends BaseImportListComponent<StatuteParagraph> {
/**
* 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 { Tag } from 'app/shared/models/core/tag';
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 { CreateMotion } from '../models/create-motion';
import { CsvMapping, ImportCreateMotion } from '../models/import-create-motion';
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
@ -24,7 +24,7 @@ import { ViewMotion } from '../models/view-motion';
@Injectable({
providedIn: 'root'
})
export class MotionImportService extends BaseImportService<ViewMotion> {
export class MotionImportService extends BaseImportService<Motion> {
/**
* List of possible errors and their verbose explanation
*/
@ -104,8 +104,8 @@ export class MotionImportService extends BaseImportService<ViewMotion> {
* @param line
* @returns a new Entry representing a Motion
*/
public mapData(line: string): NewEntry<ViewMotion> {
const newEntry = new ViewCsvCreateMotion(new CreateMotion());
public mapData(line: string): NewEntry<Motion> {
const newEntry = new ImportCreateMotion(new CreateMotion());
const headerLength = Math.min(this.expectedHeader.length, line.length);
for (let idx = 0; idx < headerLength; idx++) {
switch (this.expectedHeader[idx]) {
@ -122,11 +122,11 @@ export class MotionImportService extends BaseImportService<ViewMotion> {
newEntry.csvTags = this.getTags(line[idx]);
break;
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 entry: NewEntry<ViewMotion> = {
const entry: NewEntry<Motion> = {
newEntry: newEntry,
hasDuplicates: hasDuplicates,
status: hasDuplicates ? 'error' : 'new',
@ -157,31 +157,31 @@ export class MotionImportService extends BaseImportService<ViewMotion> {
if (entry.status !== 'new') {
continue;
}
const openBlocks = (entry.newEntry as ViewCsvCreateMotion).solveMotionBlocks(this.newMotionBlocks);
const openBlocks = (entry.newEntry as ImportCreateMotion).solveMotionBlocks(this.newMotionBlocks);
if (openBlocks) {
this.setError(entry, 'MotionBlock');
this.updatePreview();
continue;
}
const openCategories = (entry.newEntry as ViewCsvCreateMotion).solveCategory(this.newCategories);
const openCategories = (entry.newEntry as ImportCreateMotion).solveCategory(this.newCategories);
if (openCategories) {
this.setError(entry, 'Category');
this.updatePreview();
continue;
}
const openUsers = (entry.newEntry as ViewCsvCreateMotion).solveSubmitters(this.newSubmitters);
const openUsers = (entry.newEntry as ImportCreateMotion).solveSubmitters(this.newSubmitters);
if (openUsers) {
this.setError(entry, 'Submitters');
this.updatePreview();
continue;
}
const openTags = (entry.newEntry as ViewCsvCreateMotion).solveTags(this.newTags);
const openTags = (entry.newEntry as ImportCreateMotion).solveTags(this.newTags);
if (openTags) {
this.setError(entry, 'Tags');
this.updatePreview();
continue;
}
await this.repo.create((entry.newEntry as ViewCsvCreateMotion).motion);
await this.repo.create(entry.newEntry as ImportCreateMotion);
entry.status = 'done';
}
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 { BaseImportService, NewEntry } from 'app/core/ui-services/base-import.service';
import { StatuteParagraph } from 'app/shared/models/motions/statute-paragraph';
import { ViewStatuteParagraph } from '../models/view-statute-paragraph';
/**
* Service for motion imports
@ -15,7 +14,7 @@ import { ViewStatuteParagraph } from '../models/view-statute-paragraph';
@Injectable({
providedIn: 'root'
})
export class StatuteImportService extends BaseImportService<ViewStatuteParagraph> {
export class StatuteImportService extends BaseImportService<StatuteParagraph> {
/**
* List of possible errors and their verbose explanation
*/
@ -60,16 +59,16 @@ export class StatuteImportService extends BaseImportService<ViewStatuteParagraph
* @param line
* @returns a new Entry representing a Motion
*/
public mapData(line: string): NewEntry<ViewStatuteParagraph> {
const newEntry = new ViewStatuteParagraph(new StatuteParagraph());
public mapData(line: string): NewEntry<StatuteParagraph> {
const newEntry = new StatuteParagraph(new StatuteParagraph());
const headerLength = Math.min(this.expectedHeader.length, line.length);
for (let idx = 0; idx < headerLength; idx++) {
switch (this.expectedHeader[idx]) {
case 'title':
newEntry.statuteParagraph.title = line[idx];
newEntry.title = line[idx];
break;
case 'text':
newEntry.statuteParagraph.text = line[idx];
newEntry.text = line[idx];
break;
}
}
@ -91,7 +90,7 @@ export class StatuteImportService extends BaseImportService<ViewStatuteParagraph
if (entry.status !== 'new') {
continue;
}
await this.repo.create(entry.newEntry.statuteParagraph);
await this.repo.create(entry.newEntry);
entry.status = 'done';
}
this.updatePreview();

View File

@ -197,21 +197,21 @@
</ng-container>
<!-- duration column -->
<ng-container matColumnDef="duration">
<ng-container matColumnDef="agenda_duration">
<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>
<!-- comment column-->
<ng-container matColumnDef="comment">
<ng-container matColumnDef="agenda_comment">
<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>
<!-- type column -->
<ng-container matColumnDef="type">
<ng-container matColumnDef="agenda_type">
<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>
<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 { AgendaImportListComponent } from './agenda-import-list.component';
import { TopicImportListComponent } from './topic-import-list.component';
describe('AgendaImportListComponent', () => {
let component: AgendaImportListComponent;
let fixture: ComponentFixture<AgendaImportListComponent>;
describe('TopicImportListComponent', () => {
let component: TopicImportListComponent;
let fixture: ComponentFixture<TopicImportListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AgendaImportListComponent],
declarations: [TopicImportListComponent],
imports: [E2EImportsModule]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AgendaImportListComponent);
fixture = TestBed.createComponent(TopicImportListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -5,21 +5,21 @@ import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { AgendaImportService } from '../../services/agenda-import.service';
import { CsvExportService } from 'app/core/ui-services/csv-export.service';
import { DurationService } from 'app/core/ui-services/duration.service';
import { ItemVisibilityChoices } from 'app/shared/models/agenda/item';
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({
selector: 'os-agenda-import-list',
templateUrl: './agenda-import-list.component.html'
selector: 'os-topic-import-list',
templateUrl: './topic-import-list.component.html'
})
export class AgendaImportListComponent extends BaseImportListComponent<ViewCreateTopic> {
export class TopicImportListComponent extends BaseImportListComponent<CreateTopic> {
/**
* A form for text input
*/
@ -40,7 +40,7 @@ export class AgendaImportListComponent extends BaseImportListComponent<ViewCreat
titleService: Title,
matSnackBar: MatSnackBar,
translate: TranslateService,
importer: AgendaImportService,
importer: TopicImportService,
formBuilder: FormBuilder,
private exporter: CsvExportService,
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
*/
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) {
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 { AgendaImportService } from './agenda-import.service';
import { TopicImportService } from './topic-import.service';
describe('AgendaImportService', () => {
describe('TopicImportService', () => {
beforeEach(() =>
TestBed.configureTestingModule({
imports: [E2EImportsModule]
@ -12,7 +12,7 @@ describe('AgendaImportService', () => {
);
it('should be created', () => {
const service: AgendaImportService = TestBed.get(AgendaImportService);
const service: TopicImportService = TestBed.get(TopicImportService);
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 { DurationService } from 'app/core/ui-services/duration.service';
import { ItemVisibilityChoices } from 'app/shared/models/agenda/item';
import { ViewCreateTopic } from 'app/site/topics/models/view-create-topic';
import { CreateTopic } from '../../topics/models/create-topic';
import { CreateTopic } from '../models/create-topic';
@Injectable({
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
* {@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
@ -31,7 +30,7 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
*/
public errorList = {
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.'
};
@ -67,17 +66,17 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
* @param line a line extracted by the CSV (without the header)
* @returns a new entry for a Topic
*/
public mapData(line: string): NewEntry<ViewCreateTopic> {
const newEntry = new ViewCreateTopic(new CreateTopic());
public mapData(line: string): NewEntry<CreateTopic> {
const newEntry = new CreateTopic();
const headerLength = Math.min(this.expectedHeader.length, line.length);
let hasErrors = false;
for (let idx = 0; idx < headerLength; idx++) {
switch (this.expectedHeader[idx]) {
case 'duration':
case 'agenda_duration':
try {
const duration = this.parseDuration(line[idx]);
if (duration > 0) {
newEntry.duration = duration;
newEntry.agenda_duration = duration;
}
} catch (e) {
if (e instanceof TypeError) {
@ -86,9 +85,9 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
}
}
break;
case 'type':
case 'agenda_type':
try {
newEntry.type = this.parseType(line[idx]);
newEntry.agenda_type = this.parseType(line[idx]);
} catch (e) {
if (e instanceof TypeError) {
hasErrors = true;
@ -103,10 +102,10 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
const hasDuplicates = this.repo.getViewModelList().some(topic => topic.title === newEntry.title);
// set type to 'public' if none is given in import
if (!newEntry.type) {
newEntry.type = 1;
if (!newEntry.agenda_type) {
newEntry.agenda_type = 1;
}
const mappedEntry: NewEntry<ViewCreateTopic> = {
const mappedEntry: NewEntry<CreateTopic> = {
newEntry: newEntry,
hasDuplicates: hasDuplicates,
status: 'new',
@ -133,7 +132,7 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
if (entry.status !== 'new') {
continue;
}
await this.repo.create(entry.newEntry.topic);
await this.repo.create(entry.newEntry);
entry.status = 'done';
}
this.updatePreview();
@ -183,7 +182,7 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
* @param data a string as produced by textArea input
*/
public parseTextArea(data: string): void {
const newEntries: NewEntry<ViewCreateTopic>[] = [];
const newEntries: NewEntry<CreateTopic>[] = [];
this.clearData();
this.clearPreview();
const lines = data.split('\n');
@ -191,14 +190,14 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
if (!line.length) {
return;
}
const newTopic = new ViewCreateTopic(
const newTopic = new CreateTopic(
new CreateTopic({
title: line,
agenda_type: 1 // set type to 'public item' by default
})
);
const hasDuplicates = this.repo.getViewModelList().some(topic => topic.title === newTopic.title);
const newEntry: NewEntry<ViewCreateTopic> = {
const newEntry: NewEntry<CreateTopic> = {
newEntry: newTopic,
hasDuplicates: hasDuplicates,
status: 'new',

View File

@ -12,6 +12,7 @@
<mat-tab-group (selectedTabChange)="onTabChange()">
<!-- textarea import tab -->
<mat-tab label="{{ 'Text import' | translate }}">
<br>
<div [formGroup]="textAreaForm">
<div>
<span translate> Copy and paste your participant names in this textbox.</span>
@ -36,19 +37,20 @@
</mat-tab>
<!-- CSV import tab -->
<mat-tab label="{{ 'CSV import' | translate }}">
<br>
<span translate
>Required comma or semicolon separated values with these column header names in the first row:</span
>: <br />
><br />
<div class="code red-warning-text">
<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>Groups</span>, <span translate>Comment</span>, <span translate>Is active</span>,
<span translate>Is present</span> , <span translate>Is committee</span>,
<span translate>Initial password</span>, <span translate>Email</span>
<span translate>Is present</span>, <span translate>Is committee</span>, <span translate>Initial password</span>,
<span translate>Email</span>, <span translate>Username</span>, <span translate>Gender</span>
</div>
<ul>
<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 translate>
Additional columns after the required ones may be present and won't affect the import.
@ -224,7 +226,7 @@
<ng-container matColumnDef="number">
<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>
<!-- groups column -->
@ -277,6 +279,14 @@
<mat-header-cell *matHeaderCellDef translate>Email</mat-header-cell>
<mat-cell *matCellDef="let entry"> {{ entry.newEntry.email }} </mat-cell>
</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-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 { 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 { UserImportService } from '../../services/user-import.service';
import { ViewUser } from '../../models/view-user';
/**
* Component for the user import list view.
@ -18,7 +18,7 @@ import { ViewUser } from '../../models/view-user';
selector: 'os-user-import-list',
templateUrl: './user-import-list.component.html'
})
export class UserImportListComponent extends BaseImportListComponent<ViewUser> {
export class UserImportListComponent extends BaseImportListComponent<User> {
public textAreaForm: FormGroup;
/**
@ -59,7 +59,9 @@ export class UserImportListComponent extends BaseImportListComponent<ViewUser> {
'Is present',
'Is a committee',
'Initial password',
'Email'
'Email',
'Username',
'Gender'
];
const rows = [
[
@ -74,7 +76,9 @@ export class UserImportListComponent extends BaseImportListComponent<ViewUser> {
1,
,
'initialPassword',
null
null,
'mmustermann',
'm'
],
[
null,
@ -88,10 +92,12 @@ export class UserImportListComponent extends BaseImportListComponent<ViewUser> {
1,
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, null, 'Executive Board', null, null, null, null, null, null, 1, 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, 'executive', null]
];
this.exporter.dummyCSVExport(headerRow, rows, `${this.translate.instant('participants-example')}.csv`);
}
@ -102,7 +108,7 @@ export class UserImportListComponent extends BaseImportListComponent<ViewUser> {
* @param row
* @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']) {
if (this.importer.hasError(row, name)) {
return this.importer.verbose(name);

View File

@ -1,5 +1,4 @@
import { User } from 'app/shared/models/users/user';
import { ViewUser } from './view-user';
/**
* Interface for correlating between strings representing BaseModels and existing
@ -17,7 +16,7 @@ export interface CsvMapping {
*
* @ignore
*/
export class ViewCsvCreateUser extends ViewUser {
export class ImportCreateUser extends User {
/**
* 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
*
* @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 {
if (this.user && (this.first_name || this.last_name)) {
return true;
}
return false;
}
public constructor(user?: User) {
super(user);
return !!(this.first_name || this.last_name || this.username);
}
/**
@ -67,7 +59,7 @@ export class ViewCsvCreateUser extends ViewUser {
open += 1;
}
});
this.user.groups_id = ids;
this.groups_id = ids;
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 { Group } from 'app/shared/models/users/group';
import { User } from 'app/shared/models/users/user';
import { CsvMapping, ViewCsvCreateUser } from '../models/view-csv-create-user';
import { ViewUser } from '../models/view-user';
import { CsvMapping, ImportCreateUser } from '../models/import-create-user';
@Injectable({
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
* will be passed to {@link expectedHeader}
*/
public headerMap: (keyof ViewCsvCreateUser)[] = [
public headerMap: (keyof ImportCreateUser)[] = [
'title',
'first_name',
'last_name',
@ -32,7 +31,9 @@ export class UserImportService extends BaseImportService<ViewUser> {
'is_present',
'is_committee',
'default_password',
'email'
'email',
'username',
'gender'
];
/**
@ -91,8 +92,8 @@ export class UserImportService extends BaseImportService<ViewUser> {
* @param line
* @returns a new entry representing an User
*/
public mapData(line: string): NewEntry<ViewUser> {
const newViewUser = new ViewCsvCreateUser(new User());
public mapData(line: string): NewEntry<User> {
const newViewUser = new ImportCreateUser();
const headerLength = Math.min(this.expectedHeader.length, line.length);
let hasErrors = false;
for (let idx = 0; idx < headerLength; idx++) {
@ -104,7 +105,7 @@ export class UserImportService extends BaseImportService<ViewUser> {
case 'is_committee':
case 'is_present':
try {
newViewUser.user[this.expectedHeader[idx]] = this.toBoolean(line[idx]);
newViewUser[this.expectedHeader[idx]] = this.toBoolean(line[idx]);
} catch (e) {
if (e instanceof TypeError) {
console.log(e);
@ -114,10 +115,10 @@ export class UserImportService extends BaseImportService<ViewUser> {
}
break;
case 'number':
newViewUser.user.number = line[idx];
newViewUser.number = line[idx];
break;
default:
newViewUser.user[this.expectedHeader[idx]] = line[idx];
newViewUser[this.expectedHeader[idx]] = line[idx];
break;
}
}
@ -136,13 +137,13 @@ export class UserImportService extends BaseImportService<ViewUser> {
*/
public async doImport(): Promise<void> {
this.newGroups = await this.createNewGroups();
const importUsers: NewEntry<ViewUser>[] = [];
const importUsers: NewEntry<User>[] = [];
let trackId = 1;
for (const entry of this.entries) {
if (entry.status !== 'new') {
continue;
}
const openBlocks = (entry.newEntry as ViewCsvCreateUser).solveGroups(this.newGroups);
const openBlocks = (entry.newEntry as ImportCreateUser).solveGroups(this.newGroups);
if (openBlocks) {
this.setError(entry, 'Group');
this.updatePreview();
@ -245,7 +246,7 @@ export class UserImportService extends BaseImportService<ViewUser> {
* @param data a string as produced by textArea input
*/
public parseTextArea(data: string): void {
const newEntries: NewEntry<ViewUser>[] = [];
const newEntries: NewEntry<User>[] = [];
this.clearData();
this.clearPreview();
const lines = data.split('\n');
@ -254,7 +255,7 @@ export class UserImportService extends BaseImportService<ViewUser> {
return;
}
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);
newEntries.push(newEntry);
});
@ -267,15 +268,17 @@ export class UserImportService extends BaseImportService<ViewUser> {
* @param newUser
* @returns a NewEntry with duplicate/error information
*/
private userToEntry(newUser: ViewCsvCreateUser): NewEntry<ViewUser> {
const newEntry: NewEntry<ViewUser> = {
private userToEntry(newUser: ImportCreateUser): NewEntry<User> {
const newEntry: NewEntry<User> = {
newEntry: newUser,
hasDuplicates: false,
status: 'new',
errors: []
};
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) {
this.setError(newEntry, 'Duplicates');
}

View File

@ -352,6 +352,7 @@ class UserViewSet(ModelViewSet):
serializer.is_valid(raise_exception=True)
except ValidationError:
# Skip invalid users.
continue
data = serializer.prepare_password(serializer.data)
groups = data["groups_id"]
@ -364,7 +365,7 @@ class UserViewSet(ModelViewSet):
if "importTrackId" in user:
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)
return Response(
{