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 ' - '