diff --git a/client/src/app/site/motions/models/create-motion.ts b/client/src/app/site/motions/models/create-motion.ts index 816ed578a..067b83359 100644 --- a/client/src/app/site/motions/models/create-motion.ts +++ b/client/src/app/site/motions/models/create-motion.ts @@ -11,6 +11,8 @@ export class CreateMotion extends Motion { 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/view-csv-create-motion.ts index 12c794856..4d18fe13f 100644 --- a/client/src/app/site/motions/models/view-csv-create-motion.ts +++ b/client/src/app/site/motions/models/view-csv-create-motion.ts @@ -35,6 +35,11 @@ export class ViewCsvCreateMotion extends ViewCreateMotion { */ public csvSubmitters: CsvMapping[]; + /** + * Mapping for new/existing tags. + */ + public csvTags: CsvMapping[]; + public constructor(motion?: CreateMotion) { super(motion); } @@ -116,4 +121,35 @@ export class ViewCsvCreateMotion extends ViewCreateMotion { this.motion.submitters_id = ids; return open; } + + /** + * Function to iterate over the found tags. + * + * @param tags The mapping of the read tags. + * + * @returns {number} the number of open tags. + */ + public solveTags(tags: CsvMapping[]): number { + let open = 0; + const ids: number[] = []; + for (const tag of this.csvTags) { + if (tag.id) { + ids.push(tag.id); + continue; + } + if (!tags.length) { + ++open; + continue; + } + const mapped = tags.find(_tag => _tag.name === tag.name); + if (mapped) { + tag.id = mapped.id; + ids.push(mapped.id); + } else { + ++open; + } + } + this.motion.tags_id = ids; + return open; + } } diff --git a/client/src/app/site/motions/modules/motion-import/motion-import-list.component.html b/client/src/app/site/motions/modules/motion-import/motion-import-list.component.html index ffb631a93..ee3459a28 100644 --- a/client/src/app/site/motions/modules/motion-import/motion-import-list.component.html +++ b/client/src/app/site/motions/modules/motion-import/motion-import-list.component.html @@ -228,6 +228,26 @@ + + + Tags + +
+ + warning + +
+ {{ tag.name }} + add +
+
+
+
+ Origin diff --git a/client/src/app/site/motions/motion-import-export-order.ts b/client/src/app/site/motions/motion-import-export-order.ts index e1fdec61d..9292f2259 100644 --- a/client/src/app/site/motions/motion-import-export-order.ts +++ b/client/src/app/site/motions/motion-import-export-order.ts @@ -26,7 +26,7 @@ export const noMetaData: string[] = ['identifier', 'title', 'text', 'reason']; * Subset of {@link motionImportExportHeaderOrder} properties that are * restricted to export only due to database or workflow limitations */ -export const motionExportOnly: string[] = ['id', 'recommendation', 'state', 'tags']; +export const motionExportOnly: string[] = ['id', 'recommendation', 'state']; /** * reorders the exported properties according to motionImportExportHeaderOrder diff --git a/client/src/app/site/motions/services/motion-csv-export.service.ts b/client/src/app/site/motions/services/motion-csv-export.service.ts index d5600ba33..51cd617d7 100644 --- a/client/src/app/site/motions/services/motion-csv-export.service.ts +++ b/client/src/app/site/motions/services/motion-csv-export.service.ts @@ -145,11 +145,31 @@ export class MotionCsvExportService { // TODO does not reflect updated export order. any more. Hard coded for now public exportDummyMotion(): void { - const headerRow = ['Identifier', 'Title', 'Text', 'Reason', 'Submitters', 'Category', 'Origin', 'Motion block']; + const headerRow = [ + 'Identifier', + 'Title', + 'Text', + 'Reason', + 'Submitters', + 'Category', + 'Tags', + 'Origin', + 'Motion block' + ]; const rows = [ - ['A1', 'Title 1', 'Text 1', 'Reason 1', 'Submitter A', 'Category A', 'Last Year Conference A', 'Block A'], - ['B1', 'Title 2', 'Text 2', 'Reason 2', 'Submitter B', 'Category B', null, 'Block A'], - [null, 'Title 3', 'Text 3', null, null, null, null, null] + [ + 'A1', + 'Title 1', + 'Text 1', + 'Reason 1', + 'Submitter A', + 'Category A', + 'Tag 1, Tag 2', + 'Last Year Conference A', + 'Block A' + ], + ['B1', 'Title 2', 'Text 2', 'Reason 2', 'Submitter B', 'Category B', null, null, 'Block A'], + [null, 'Title 3', 'Text 3', null, null, null, null, null, null] ]; this.csvExport.dummyCSVExport(headerRow, rows, `${this.translate.instant('motions-example')}.csv`); } 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 3ec2c0dce..225c8b982 100644 --- a/client/src/app/site/motions/services/motion-import.service.ts +++ b/client/src/app/site/motions/services/motion-import.service.ts @@ -15,6 +15,8 @@ import { MotionRepositoryService } from 'app/core/repositories/motions/motion-re import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service'; import { ViewCsvCreateMotion, CsvMapping } from '../models/view-csv-create-motion'; import { ViewMotion } from '../models/view-motion'; +import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service'; +import { Tag } from 'app/shared/models/core/tag'; /** * Service for motion imports @@ -30,6 +32,7 @@ export class MotionImportService extends BaseImportService { MotionBlock: 'Could not resolve the motion block', Category: 'Could not resolve the category', Submitters: 'Could not resolve the submitters', + Tags: 'Could not resolve the tags', Title: 'A title is required', Text: "A content in the 'text' column is required", Duplicates: 'A motion with this identifier already exists.' @@ -55,6 +58,11 @@ export class MotionImportService extends BaseImportService { */ public newMotionBlocks: CsvMapping[] = []; + /** + * Mapping of the new tags for the imported motion. + */ + public newTags: CsvMapping[] = []; + /** * Constructor. Defines the headers expected and calls the abstract class * @param repo: The repository for motions. @@ -70,6 +78,7 @@ export class MotionImportService extends BaseImportService { private categoryRepo: CategoryRepositoryService, private motionBlockRepo: MotionBlockRepositoryService, private userRepo: UserRepositoryService, + private tagRepo: TagRepositoryService, translate: TranslateService, papa: Papa, matSnackbar: MatSnackBar @@ -85,6 +94,7 @@ export class MotionImportService extends BaseImportService { this.newSubmitters = []; this.newCategories = []; this.newMotionBlocks = []; + this.newTags = []; } /** @@ -108,6 +118,9 @@ export class MotionImportService extends BaseImportService { case 'motion_block': newEntry.csvMotionblock = this.getMotionBlock(line[idx]); break; + case 'tags': + newEntry.csvTags = this.getTags(line[idx]); + break; default: newEntry.motion[this.expectedHeader[idx]] = line[idx]; } @@ -138,6 +151,7 @@ export class MotionImportService extends BaseImportService { this.newMotionBlocks = await this.createNewMotionBlocks(); this.newCategories = await this.createNewCategories(); this.newSubmitters = await this.createNewUsers(); + this.newTags = await this.createNewTags(); for (const entry of this.entries) { if (entry.status !== 'new') { @@ -161,6 +175,12 @@ export class MotionImportService extends BaseImportService { this.updatePreview(); continue; } + const openTags = (entry.newEntry as ViewCsvCreateMotion).solveTags(this.newTags); + if (openTags) { + this.setError(entry, 'Tags'); + this.updatePreview(); + continue; + } await this.repo.create((entry.newEntry as ViewCsvCreateMotion).motion); entry.status = 'done'; } @@ -276,6 +296,36 @@ export class MotionImportService extends BaseImportService { } } + /** + * Iterates over the given string separated by ',' + * Creates for every found string a tag. + * + * @param tagList The list of tags as string. + * + * @returns {CsvMapping[]} The list of tags as csv-mapping. + */ + public getTags(tagList: string): CsvMapping[] { + const result: CsvMapping[] = []; + if (!tagList) { + return result; + } + + const tagArray = tagList.split(','); + for (let tag of tagArray) { + tag = tag.trim(); + const existingTag = this.tagRepo.getViewModelList().find(tagInRepo => tagInRepo.name === tag); + if (existingTag) { + result.push({ id: existingTag.id, name: existingTag.name }); + } else { + if (!this.newTags.find(entry => entry.name === tag)) { + this.newTags.push({ name: tag }); + } + result.push({ name: tag }); + } + } + return result; + } + /** * Creates all new Users needed for the import. * @@ -331,6 +381,23 @@ export class MotionImportService extends BaseImportService { return await Promise.all(promises); } + /** + * Combines all tags which are new created to one promise. + * + * @returns {Promise} One promise containing all promises to create a new tag. + */ + private async createNewTags(): Promise { + const promises: Promise[] = []; + for (const tag of this.newTags) { + promises.push( + this.tagRepo + .create(new Tag({ name: tag.name })) + .then(identifiable => ({ name: tag.name, id: identifiable.id })) + ); + } + return await Promise.all(promises); + } + /** * Helper to separate a category string from its' prefix. Assumes that a prefix is no longer * than 5 chars and separated by a ' - '