From 0922f0de3fdd39b01ca75cbfafbf1d6a2cb6dfa5 Mon Sep 17 00:00:00 2001 From: jsangmeister Date: Fri, 1 Nov 2019 15:19:05 +0100 Subject: [PATCH] fixed csv import --- .../users/user-repository.service.ts | 4 +- .../core/ui-services/base-import.service.ts | 20 +-- .../app/site/agenda/agenda-routing.module.ts | 4 +- client/src/app/site/agenda/agenda.module.ts | 4 +- client/src/app/site/base/base-import-list.ts | 16 +-- .../app/site/motions/models/create-motion.ts | 10 -- ...eate-motion.ts => import-create-motion.ts} | 21 ++-- .../motion-import-list.component.ts | 4 +- .../statute-import-list.component.ts | 4 +- .../motions/services/motion-import.service.ts | 24 ++-- .../services/statute-import.service.ts | 13 +- .../topic-import-list.component.html} | 12 +- .../topic-import-list.component.spec.ts} | 12 +- .../topic-import-list.component.ts} | 14 +-- .../app/site/topics/models/create-topic.ts | 9 ++ .../site/topics/models/view-create-topic.ts | 114 ------------------ .../services/topic-import.service.spec.ts} | 6 +- .../services/topic-import.service.ts} | 35 +++--- .../user-import-list.component.html | 24 ++-- .../user-import/user-import-list.component.ts | 22 ++-- ...v-create-user.ts => import-create-user.ts} | 16 +-- .../users/services/user-import.service.ts | 37 +++--- openslides/users/views.py | 3 +- 23 files changed, 158 insertions(+), 270 deletions(-) rename client/src/app/site/motions/models/{view-csv-create-motion.ts => import-create-motion.ts} (87%) rename client/src/app/site/{agenda/components/agenda-import-list/agenda-import-list.component.html => topics/components/topic-import-list/topic-import-list.component.html} (96%) rename client/src/app/site/{agenda/components/agenda-import-list/agenda-import-list.component.spec.ts => topics/components/topic-import-list/topic-import-list.component.spec.ts} (57%) rename client/src/app/site/{agenda/components/agenda-import-list/agenda-import-list.component.ts => topics/components/topic-import-list/topic-import-list.component.ts} (87%) delete mode 100644 client/src/app/site/topics/models/view-create-topic.ts rename client/src/app/site/{agenda/services/agenda-import.service.spec.ts => topics/services/topic-import.service.spec.ts} (63%) rename client/src/app/site/{agenda/services/agenda-import.service.ts => topics/services/topic-import.service.ts} (86%) rename client/src/app/site/users/models/{view-csv-create-user.ts => import-create-user.ts} (82%) diff --git a/client/src/app/core/repositories/users/user-repository.service.ts b/client/src/app/core/repositories/users/user-repository.service.ts index c0df75b8b..35f74ed29 100644 --- a/client/src/app/core/repositories/users/user-repository.service.ts +++ b/client/src/app/core/repositories/users/user-repository.service.ts @@ -209,9 +209,9 @@ export class UserRepositoryService extends BaseRepository[]): Promise { + public async bulkCreate(newEntries: NewEntry[]): Promise { 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; diff --git a/client/src/app/core/ui-services/base-import.service.ts b/client/src/app/core/ui-services/base-import.service.ts index c0884f32e..af2c875ce 100644 --- a/client/src/app/core/ui-services/base-import.service.ts +++ b/client/src/app/core/ui-services/base-import.service.ts @@ -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 { +export abstract class BaseImportService { /** * List of possible errors and their verbose explanation */ @@ -127,12 +127,12 @@ export abstract class BaseImportService { /** * the list of parsed models that have been extracted from the opened file */ - private _entries: NewEntry[] = []; + private _entries: NewEntry[] = []; /** * BehaviorSubject for displaying a preview for the currently selected entries */ - public newEntries = new BehaviorSubject[]>([]); + public newEntries = new BehaviorSubject[]>([]); /** * Emits an error string to display if a file import cannot be done @@ -159,7 +159,7 @@ export abstract class BaseImportService { * 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[] { + protected get entries(): NewEntry[] { return this._entries; } @@ -220,7 +220,7 @@ export abstract class BaseImportService { * * @param entries: an array of prepared newEntry objects */ - public setParsedEntries(entries: NewEntry[]): void { + public setParsedEntries(entries: NewEntry[]): void { this.clearData(); this.clearPreview(); if (!entries) { @@ -236,7 +236,7 @@ export abstract class BaseImportService { * returning a new entry object * @param line a line extracted by the CSV (not including the header) */ - public abstract mapData(line: string): NewEntry; + public abstract mapData(line: string): NewEntry; /** * Trigger for executing the import. @@ -279,7 +279,7 @@ export abstract class BaseImportService { * * @returns an observable BehaviorSubject */ - public getNewEntries(): Observable[]> { + public getNewEntries(): Observable[]> { return this.newEntries.asObservable(); } @@ -357,7 +357,7 @@ export abstract class BaseImportService { /** * set a list of short names for error, indicating which column failed */ - public setError(entry: NewEntry, error: string): void { + public setError(entry: NewEntry, error: string): void { if (this.errorList.hasOwnProperty(error)) { if (!entry.errors) { entry.errors = [error]; @@ -385,7 +385,7 @@ export abstract class BaseImportService { * @param error The error to check for * @returns true if the error is present */ - public hasError(entry: NewEntry, error: string): boolean { + public hasError(entry: NewEntry, error: string): boolean { return entry.errors.includes(error); } } diff --git a/client/src/app/site/agenda/agenda-routing.module.ts b/client/src/app/site/agenda/agenda-routing.module.ts index b368c248e..b2ad36c3a 100644 --- a/client/src/app/site/agenda/agenda-routing.module.ts +++ b/client/src/app/site/agenda/agenda-routing.module.ts @@ -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, diff --git a/client/src/app/site/agenda/agenda.module.ts b/client/src/app/site/agenda/agenda.module.ts index aaaa04468..7f46ef30f 100644 --- a/client/src/app/site/agenda/agenda.module.ts +++ b/client/src/app/site/agenda/agenda.module.ts @@ -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 ] diff --git a/client/src/app/site/base/base-import-list.ts b/client/src/app/site/base/base-import-list.ts index ca67876a8..9318de142 100644 --- a/client/src/app/site/base/base-import-list.ts +++ b/client/src/app/site/base/base-import-list.ts @@ -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 extends BaseViewComponent implements OnInit { +export abstract class BaseImportListComponent extends BaseViewComponent implements OnInit { /** * The data source for a table. Requires to be initialised with a BaseViewModel */ - public dataSource: MatTableDataSource>; + public dataSource: MatTableDataSource>; /** * Helper function for previews @@ -48,7 +48,7 @@ export abstract class BaseImportListComponent extends B * The table itself */ @ViewChild(MatTable, { static: false }) - protected table: MatTable>; + protected table: MatTable>; /** * @returns the amount of total item successfully parsed @@ -112,7 +112,7 @@ export abstract class BaseImportListComponent extends B */ public constructor( - protected importer: BaseImportService, + protected importer: BaseImportService, titleService: Title, translate: TranslateService, matSnackBar: MatSnackBar @@ -204,7 +204,7 @@ export abstract class BaseImportListComponent extends B * @param row a newEntry object with a current status * @returns a css class name */ - public getStateClass(row: NewEntry): string { + public getStateClass(row: NewEntry): string { switch (row.status) { case 'done': return 'import-done import-decided'; @@ -220,7 +220,7 @@ export abstract class BaseImportListComponent extends B * @param entry a newEntry object with a current status * @eturn the icon for the action of the item */ - public getActionIcon(entry: NewEntry): string { + public getActionIcon(entry: NewEntry): string { switch (entry.status) { case 'error': // no import possible return 'block'; @@ -286,7 +286,7 @@ export abstract class BaseImportListComponent 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, error: string): boolean { + public hasError(row: NewEntry, error: string): boolean { return this.importer.hasError(row, error); } } diff --git a/client/src/app/site/motions/models/create-motion.ts b/client/src/app/site/motions/models/create-motion.ts index 067b83359..9032c11eb 100644 --- a/client/src/app/site/motions/models/create-motion.ts +++ b/client/src/app/site/motions/models/create-motion.ts @@ -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); - } } diff --git a/client/src/app/site/motions/models/view-csv-create-motion.ts b/client/src/app/site/motions/models/import-create-motion.ts similarity index 87% rename from client/src/app/site/motions/models/view-csv-create-motion.ts rename to client/src/app/site/motions/models/import-create-motion.ts index 6f1d0c1c6..78a167d78 100644 --- a/client/src/app/site/motions/models/view-csv-create-motion.ts +++ b/client/src/app/site/motions/models/import-create-motion.ts @@ -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; } } diff --git a/client/src/app/site/motions/modules/motion-import/motion-import-list.component.ts b/client/src/app/site/motions/modules/motion-import/motion-import-list.component.ts index 61c0facd7..22164b495 100644 --- a/client/src/app/site/motions/modules/motion-import/motion-import-list.component.ts +++ b/client/src/app/site/motions/modules/motion-import/motion-import-list.component.ts @@ -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 { +export class MotionImportListComponent extends BaseImportListComponent { /** * Fetach a list of the headers expected by the importer, and prepare them * to be translateable (upper case) diff --git a/client/src/app/site/motions/modules/statute-paragraph/components/statute-import-list/statute-import-list.component.ts b/client/src/app/site/motions/modules/statute-paragraph/components/statute-import-list/statute-import-list.component.ts index a0652a18c..9215a4b4f 100644 --- a/client/src/app/site/motions/modules/statute-paragraph/components/statute-import-list/statute-import-list.component.ts +++ b/client/src/app/site/motions/modules/statute-paragraph/components/statute-import-list/statute-import-list.component.ts @@ -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 { +export class StatuteImportListComponent extends BaseImportListComponent { /** * Constructor for list view bases * diff --git a/client/src/app/site/motions/services/motion-import.service.ts b/client/src/app/site/motions/services/motion-import.service.ts index c40af7e62..afa0eb557 100644 --- a/client/src/app/site/motions/services/motion-import.service.ts +++ b/client/src/app/site/motions/services/motion-import.service.ts @@ -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 { +export class MotionImportService extends BaseImportService { /** * List of possible errors and their verbose explanation */ @@ -104,8 +104,8 @@ export class MotionImportService extends BaseImportService { * @param line * @returns a new Entry representing a Motion */ - public mapData(line: string): NewEntry { - const newEntry = new ViewCsvCreateMotion(new CreateMotion()); + public mapData(line: string): NewEntry { + 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 { 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 = { + const entry: NewEntry = { newEntry: newEntry, hasDuplicates: hasDuplicates, status: hasDuplicates ? 'error' : 'new', @@ -157,31 +157,31 @@ export class MotionImportService extends BaseImportService { 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(); diff --git a/client/src/app/site/motions/services/statute-import.service.ts b/client/src/app/site/motions/services/statute-import.service.ts index 33e69a190..7f776166c 100644 --- a/client/src/app/site/motions/services/statute-import.service.ts +++ b/client/src/app/site/motions/services/statute-import.service.ts @@ -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 { +export class StatuteImportService extends BaseImportService { /** * List of possible errors and their verbose explanation */ @@ -60,16 +59,16 @@ export class StatuteImportService extends BaseImportService { - const newEntry = new ViewStatuteParagraph(new StatuteParagraph()); + public mapData(line: string): NewEntry { + 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 - + Duration - {{ getDuration(entry.newEntry.duration) }} + {{ getDuration(entry.newEntry.agenda_duration) }} - + Comment - {{ entry.newEntry.comment }} + {{ entry.newEntry.agenda_comment }} - + Type - {{ getTypeString(entry.newEntry.type) | translate }} + {{ getTypeString(entry.newEntry.agenda_type) | translate }} diff --git a/client/src/app/site/agenda/components/agenda-import-list/agenda-import-list.component.spec.ts b/client/src/app/site/topics/components/topic-import-list/topic-import-list.component.spec.ts similarity index 57% rename from client/src/app/site/agenda/components/agenda-import-list/agenda-import-list.component.spec.ts rename to client/src/app/site/topics/components/topic-import-list/topic-import-list.component.spec.ts index 681799867..2df0df914 100644 --- a/client/src/app/site/agenda/components/agenda-import-list/agenda-import-list.component.spec.ts +++ b/client/src/app/site/topics/components/topic-import-list/topic-import-list.component.spec.ts @@ -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; +describe('TopicImportListComponent', () => { + let component: TopicImportListComponent; + let fixture: ComponentFixture; 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(); }); diff --git a/client/src/app/site/agenda/components/agenda-import-list/agenda-import-list.component.ts b/client/src/app/site/topics/components/topic-import-list/topic-import-list.component.ts similarity index 87% rename from client/src/app/site/agenda/components/agenda-import-list/agenda-import-list.component.ts rename to client/src/app/site/topics/components/topic-import-list/topic-import-list.component.ts index 7092aa1e9..00b2e8330 100644 --- a/client/src/app/site/agenda/components/agenda-import-list/agenda-import-list.component.ts +++ b/client/src/app/site/topics/components/topic-import-list/topic-import-list.component.ts @@ -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 { +export class TopicImportListComponent extends BaseImportListComponent { /** * A form for text input */ @@ -40,7 +40,7 @@ export class AgendaImportListComponent extends BaseImportListComponent { - throw new Error('This should not be used'); - }; -} diff --git a/client/src/app/site/agenda/services/agenda-import.service.spec.ts b/client/src/app/site/topics/services/topic-import.service.spec.ts similarity index 63% rename from client/src/app/site/agenda/services/agenda-import.service.spec.ts rename to client/src/app/site/topics/services/topic-import.service.spec.ts index 39b05fc23..aa31de0aa 100644 --- a/client/src/app/site/agenda/services/agenda-import.service.spec.ts +++ b/client/src/app/site/topics/services/topic-import.service.spec.ts @@ -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(); }); }); diff --git a/client/src/app/site/agenda/services/agenda-import.service.ts b/client/src/app/site/topics/services/topic-import.service.ts similarity index 86% rename from client/src/app/site/agenda/services/agenda-import.service.ts rename to client/src/app/site/topics/services/topic-import.service.ts index 6bb51e07c..a0cc672c8 100644 --- a/client/src/app/site/agenda/services/agenda-import.service.ts +++ b/client/src/app/site/topics/services/topic-import.service.ts @@ -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 { +export class TopicImportService extends BaseImportService { /** * 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 { */ 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 { * @param line a line extracted by the CSV (without the header) * @returns a new entry for a Topic */ - public mapData(line: string): NewEntry { - const newEntry = new ViewCreateTopic(new CreateTopic()); + public mapData(line: string): NewEntry { + 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 { } } 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 { 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 = { + const mappedEntry: NewEntry = { newEntry: newEntry, hasDuplicates: hasDuplicates, status: 'new', @@ -133,7 +132,7 @@ export class AgendaImportService extends BaseImportService { 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 { * @param data a string as produced by textArea input */ public parseTextArea(data: string): void { - const newEntries: NewEntry[] = []; + const newEntries: NewEntry[] = []; this.clearData(); this.clearPreview(); const lines = data.split('\n'); @@ -191,14 +190,14 @@ export class AgendaImportService extends BaseImportService { 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 = { + const newEntry: NewEntry = { newEntry: newTopic, hasDuplicates: hasDuplicates, status: 'new', diff --git a/client/src/app/site/users/components/user-import/user-import-list.component.html b/client/src/app/site/users/components/user-import/user-import-list.component.html index fc13649cc..9e39d0dbb 100644 --- a/client/src/app/site/users/components/user-import/user-import-list.component.html +++ b/client/src/app/site/users/components/user-import/user-import-list.component.html @@ -12,6 +12,7 @@ +
Copy and paste your participant names in this textbox. @@ -36,19 +37,20 @@ +
Required comma or semicolon separated values with these column header names in the first row::
+ >
- Title, Given name, Surname , + Title, Given name, Surname, Structure level, Participant number, - Groups , Comment, Is active, - Is present , Is committee, - Initial password, Email + Groups, Comment, Is active, + Is present, Is committee, Initial password, + Email, Username, Gender
  • - 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.
  • Additional columns after the required ones may be present and won't affect the import. @@ -224,7 +226,7 @@ Participant number - {{ entry.newEntry.user.number }} + {{ entry.newEntry.number }} @@ -277,6 +279,14 @@ Email {{ entry.newEntry.email }} + + Username + {{ entry.newEntry.username }} + + + Gender + {{ entry.newEntry.gender }} + diff --git a/client/src/app/site/users/components/user-import/user-import-list.component.ts b/client/src/app/site/users/components/user-import/user-import-list.component.ts index 34de72457..41ddd1368 100644 --- a/client/src/app/site/users/components/user-import/user-import-list.component.ts +++ b/client/src/app/site/users/components/user-import/user-import-list.component.ts @@ -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 { +export class UserImportListComponent extends BaseImportListComponent { public textAreaForm: FormGroup; /** @@ -59,7 +59,9 @@ export class UserImportListComponent extends BaseImportListComponent { 'Is present', 'Is a committee', 'Initial password', - 'Email' + 'Email', + 'Username', + 'Gender' ]; const rows = [ [ @@ -74,7 +76,9 @@ export class UserImportListComponent extends BaseImportListComponent { 1, , 'initialPassword', - null + null, + 'mmustermann', + 'm' ], [ null, @@ -88,10 +92,12 @@ export class UserImportListComponent extends BaseImportListComponent { 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 { * @param row * @returns an error string similar to getVerboseError */ - public nameErrors(row: NewEntry): string { + public nameErrors(row: NewEntry): string { for (const name of ['NoName', 'Duplicates', 'DuplicateImport']) { if (this.importer.hasError(row, name)) { return this.importer.verbose(name); diff --git a/client/src/app/site/users/models/view-csv-create-user.ts b/client/src/app/site/users/models/import-create-user.ts similarity index 82% rename from client/src/app/site/users/models/view-csv-create-user.ts rename to client/src/app/site/users/models/import-create-user.ts index 91f9f383c..e5ed1e1ca 100644 --- a/client/src/app/site/users/models/view-csv-create-user.ts +++ b/client/src/app/site/users/models/import-create-user.ts @@ -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; } } diff --git a/client/src/app/site/users/services/user-import.service.ts b/client/src/app/site/users/services/user-import.service.ts index 8722e19ba..dcdfbeb85 100644 --- a/client/src/app/site/users/services/user-import.service.ts +++ b/client/src/app/site/users/services/user-import.service.ts @@ -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 { +export class UserImportService extends BaseImportService { /** * 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 { 'is_present', 'is_committee', 'default_password', - 'email' + 'email', + 'username', + 'gender' ]; /** @@ -91,8 +92,8 @@ export class UserImportService extends BaseImportService { * @param line * @returns a new entry representing an User */ - public mapData(line: string): NewEntry { - const newViewUser = new ViewCsvCreateUser(new User()); + public mapData(line: string): NewEntry { + 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 { 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 { } 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 { */ public async doImport(): Promise { this.newGroups = await this.createNewGroups(); - const importUsers: NewEntry[] = []; + const importUsers: NewEntry[] = []; 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 { * @param data a string as produced by textArea input */ public parseTextArea(data: string): void { - const newEntries: NewEntry[] = []; + const newEntries: NewEntry[] = []; this.clearData(); this.clearPreview(); const lines = data.split('\n'); @@ -254,7 +255,7 @@ export class UserImportService extends BaseImportService { 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 { * @param newUser * @returns a NewEntry with duplicate/error information */ - private userToEntry(newUser: ViewCsvCreateUser): NewEntry { - const newEntry: NewEntry = { + private userToEntry(newUser: ImportCreateUser): NewEntry { + const newEntry: NewEntry = { 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'); } diff --git a/openslides/users/views.py b/openslides/users/views.py index fa1cf306c..2ef30d62a 100644 --- a/openslides/users/views.py +++ b/openslides/users/views.py @@ -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( {