From dd66244d8457fc62dcbfe01bbd49c4382bfa1f1a Mon Sep 17 00:00:00 2001 From: FinnStutzenstein Date: Wed, 17 Jul 2019 16:13:49 +0200 Subject: [PATCH] Generic relations for the repos --- .../collection-string-mapper.service.ts | 4 +- .../agenda/item-repository.service.ts | 56 +--- .../list-of-speakers-repository.service.ts | 91 ++---- .../assignment-repository.service.ts | 105 +++--- .../base-has-content-object-repository.ts | 112 ++++++- ...t-of-speakers-content-object-repository.ts | 31 +- ...s-agenda-item-content-object-repository.ts | 22 +- ...t-of-speakers-content-object-repository.ts | 22 +- .../app/core/repositories/base-repository.ts | 302 ++++++++++++++++-- .../config/config-repository.service.ts | 16 - .../mediafile-repository.service.ts | 45 +-- .../motions/category-repository.service.ts | 18 +- ...hange-recommendation-repository.service.ts | 18 +- .../motion-block-repository.service.ts | 14 - ...tion-comment-section-repository.service.ts | 40 ++- .../motions/motion-repository.service.ts | 253 +++++++-------- .../motions/state-repository.service.spec.ts | 17 + .../motions/state-repository.service.ts | 69 ++++ .../statute-paragraph-repository.service.ts | 4 - .../motions/workflow-repository.service.ts | 114 ++----- .../projector/countdown-repository.service.ts | 4 - .../projection-default-repository.service.ts | 4 - .../projector-message-repository.service.ts | 4 - .../projector/projector-repository.service.ts | 17 +- .../tags/tag-repository.service.ts | 4 - .../topics/topic-repository.service.ts | 40 +-- .../users/group-repository.service.ts | 4 - .../users/personal-note-repository.service.ts | 4 - .../users/user-repository.service.ts | 28 +- .../core/ui-services/base-import.service.ts | 6 +- .../src/app/shared/models/agenda/speaker.ts | 23 +- .../assignments/assignment-poll-option.ts | 10 +- .../models/assignments/assignment-poll.ts | 16 +- .../assignments/assignment-related-user.ts | 33 +- .../shared/models/motions/motion-submitter.ts | 27 -- .../src/app/shared/models/motions/motion.ts | 9 +- .../motions/{workflow-state.ts => state.ts} | 25 +- .../app/shared/models/motions/submitter.ts | 19 ++ .../src/app/shared/models/motions/workflow.ts | 33 +- .../src/app/site/agenda/models/view-item.ts | 9 +- .../agenda/models/view-list-of-speakers.ts | 32 +- .../app/site/agenda/models/view-speaker.ts | 23 +- .../agenda/services/agenda-import.service.ts | 16 +- .../assignment-detail.component.html | 4 +- .../assignment-detail.component.ts | 2 +- .../assignment-list.component.ts | 2 +- .../assignment-poll.component.html | 3 +- .../assignment-poll.component.ts | 2 +- .../models/view-assignment-poll-option.ts | 21 +- .../models/view-assignment-poll.ts | 59 ++-- .../models/view-assignment-related-user.ts | 33 +- .../assignments/models/view-assignment.ts | 64 +--- .../services/assignment-pdf.service.ts | 2 +- client/src/app/site/base/base-import-list.ts | 5 +- ...l-with-agenda-item-and-list-of-speakers.ts | 18 +- .../base/base-view-model-with-agenda-item.ts | 10 +- .../base-view-model-with-content-object.ts | 32 +- .../base-view-model-with-list-of-speakers.ts | 15 +- client/src/app/site/base/base-view-model.ts | 6 +- .../src/app/site/config/models/view-config.ts | 2 - .../mediafile-list.component.html | 14 +- .../site/mediafiles/models/view-mediafile.ts | 40 +-- .../app/site/motions/models/view-category.ts | 13 +- .../site/motions/models/view-create-motion.ts | 69 +--- .../models/view-motion-amended-paragraph.ts | 2 +- .../site/motions/models/view-motion-block.ts | 6 +- .../view-motion-change-recommendation.ts | 2 - .../models/view-motion-comment-section.ts | 12 +- .../app/site/motions/models/view-motion.ts | 254 ++------------- .../src/app/site/motions/models/view-state.ts | 99 ++++++ .../motions/models/view-statute-paragraph.ts | 6 - .../app/site/motions/models/view-submitter.ts | 44 +++ .../app/site/motions/models/view-workflow.ts | 32 +- .../manage-submitters.component.html | 4 +- .../manage-submitters.component.ts | 2 +- .../motion-detail.component.html | 8 +- .../motion-detail/motion-detail.component.ts | 35 +- .../workflow-detail.component.html | 27 +- .../workflow-detail.component.ts | 43 +-- .../statute-paragraph-list.component.html | 4 +- ...-notify.ts => motion-edit-notification.ts} | 6 +- client/src/app/site/motions/motions.config.ts | 9 + .../services/local-permissions.service.ts | 4 +- .../services/motion-csv-export.service.ts | 2 +- .../motions/services/motion-import.service.ts | 8 +- .../motions/services/motion-pdf.service.ts | 8 +- .../services/statute-import.service.ts | 8 +- .../site/projector/models/view-countdown.ts | 3 - .../models/view-projection-default.ts | 2 - .../models/view-projector-message.ts | 3 - .../site/projector/models/view-projector.ts | 9 +- client/src/app/site/tags/models/view-tag.ts | 6 - .../src/app/site/topics/models/view-topic.ts | 25 +- .../src/app/site/users/models/view-group.ts | 2 - .../site/users/models/view-personal-note.ts | 2 - client/src/app/site/users/models/view-user.ts | 15 +- .../users/services/user-import.service.ts | 7 +- openslides/agenda/views.py | 2 +- openslides/motions/access_permissions.py | 8 + openslides/motions/apps.py | 1 + openslides/motions/models.py | 9 +- openslides/motions/projector.py | 44 ++- openslides/motions/serializers.py | 7 +- openslides/motions/views.py | 43 ++- tests/unit/motions/test_projector.py | 187 +++++------ 105 files changed, 1489 insertions(+), 1670 deletions(-) create mode 100644 client/src/app/core/repositories/motions/state-repository.service.spec.ts create mode 100644 client/src/app/core/repositories/motions/state-repository.service.ts delete mode 100644 client/src/app/shared/models/motions/motion-submitter.ts rename client/src/app/shared/models/motions/{workflow-state.ts => state.ts} (69%) create mode 100644 client/src/app/shared/models/motions/submitter.ts create mode 100644 client/src/app/site/motions/models/view-state.ts create mode 100644 client/src/app/site/motions/models/view-submitter.ts rename client/src/app/site/motions/{models/view-motion-notify.ts => motion-edit-notification.ts} (89%) diff --git a/client/src/app/core/core-services/collection-string-mapper.service.ts b/client/src/app/core/core-services/collection-string-mapper.service.ts index f1c5169be..20f2f7304 100644 --- a/client/src/app/core/core-services/collection-string-mapper.service.ts +++ b/client/src/app/core/core-services/collection-string-mapper.service.ts @@ -20,7 +20,7 @@ type TypeIdentifier = UnifiedConstructors | BaseRepository | stri type CollectionStringMappedTypes = [ ModelConstructor, ViewModelConstructor, - BaseRepository + BaseRepository, BaseModel, TitleInformation> ]; /** @@ -46,7 +46,7 @@ export class CollectionStringMapperService { * @param collectionString * @param model */ - public registerCollectionElement( + public registerCollectionElement, M extends BaseModel>( collectionString: string, model: ModelConstructor, viewModel: ViewModelConstructor, diff --git a/client/src/app/core/repositories/agenda/item-repository.service.ts b/client/src/app/core/repositories/agenda/item-repository.service.ts index 818869af6..a522e6571 100644 --- a/client/src/app/core/repositories/agenda/item-repository.service.ts +++ b/client/src/app/core/repositories/agenda/item-repository.service.ts @@ -16,14 +16,23 @@ import { IBaseViewModelWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item'; import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; -import { BaseViewModel } from 'app/site/base/base-view-model'; -import { Motion } from 'app/shared/models/motions/motion'; -import { MotionBlock } from 'app/shared/models/motions/motion-block'; -import { Topic } from 'app/shared/models/topics/topic'; -import { Assignment } from 'app/shared/models/assignments/assignment'; import { BaseIsAgendaItemContentObjectRepository } from '../base-is-agenda-item-content-object-repository'; -import { BaseHasContentObjectRepository } from '../base-has-content-object-repository'; +import { BaseHasContentObjectRepository, GenericRelationDefinition } from '../base-has-content-object-repository'; import { Identifiable } from 'app/shared/models/base/identifiable'; +import { RelationDefinition } from '../base-repository'; +import { ViewMotion } from 'app/site/motions/models/view-motion'; +import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block'; +import { ViewTopic } from 'app/site/topics/models/view-topic'; +import { ViewAssignment } from 'app/site/assignments/models/view-assignment'; + +const ItemRelations: (RelationDefinition | GenericRelationDefinition)[] = [ + { + type: 'generic', + possibleModels: [ViewMotion, ViewMotionBlock, ViewTopic, ViewAssignment], + isVForeign: isBaseViewModelWithAgendaItem, + VForeignVerbose: 'BaseViewModelWithAgendaItem' + } +]; /** * Repository service for items @@ -58,12 +67,7 @@ export class ItemRepositoryService extends BaseHasContentObjectRepository< private httpService: HttpService, private config: ConfigService ) { - super(DS, dataSend, mapperService, viewModelStoreService, translate, Item, [ - Topic, - Assignment, - Motion, - MotionBlock - ]); + super(DS, dataSend, mapperService, viewModelStoreService, translate, Item, ItemRelations); this.setSortFunction((a, b) => a.weight - b.weight); } @@ -83,34 +87,6 @@ export class ItemRepositoryService extends BaseHasContentObjectRepository< } }; - /** - * Creates the viewItem out of a given item - * - * @param item the item that should be converted to view item - * @returns a new view item - */ - public createViewModel(item: Item): ViewItem { - const contentObject = this.getContentObject(item); - return new ViewItem(item, contentObject); - } - - /** - * Returns the corresponding content object to a given {@link Item} as an {@link BaseAgendaItemViewModel} - * - * @param agendaItem the target agenda Item - * @returns the content object of the given item. Might be null if it was not found. - */ - public getContentObject(agendaItem: Item): BaseViewModelWithAgendaItem { - const contentObject = this.viewModelStoreService.get( - agendaItem.content_object.collection, - agendaItem.content_object.id - ); - if (!contentObject || !isBaseViewModelWithAgendaItem(contentObject)) { - return null; - } - return contentObject; - } - /** * Trigger the automatic numbering sequence on the server */ diff --git a/client/src/app/core/repositories/agenda/list-of-speakers-repository.service.ts b/client/src/app/core/repositories/agenda/list-of-speakers-repository.service.ts index 47301e3b2..316cf2839 100644 --- a/client/src/app/core/repositories/agenda/list-of-speakers-repository.service.ts +++ b/client/src/app/core/repositories/agenda/list-of-speakers-repository.service.ts @@ -12,20 +12,42 @@ import { BaseViewModelWithListOfSpeakers, isBaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers'; -import { BaseViewModel } from 'app/site/base/base-view-model'; import { ViewSpeaker } from 'app/site/agenda/models/view-speaker'; -import { ViewUser } from 'app/site/users/models/view-user'; import { Identifiable } from 'app/shared/models/base/identifiable'; import { HttpService } from 'app/core/core-services/http.service'; import { BaseIsListOfSpeakersContentObjectRepository } from '../base-is-list-of-speakers-content-object-repository'; -import { BaseHasContentObjectRepository } from '../base-has-content-object-repository'; -import { Topic } from 'app/shared/models/topics/topic'; -import { Assignment } from 'app/shared/models/assignments/assignment'; -import { Motion } from 'app/shared/models/motions/motion'; -import { MotionBlock } from 'app/shared/models/motions/motion-block'; -import { Mediafile } from 'app/shared/models/mediafiles/mediafile'; +import { BaseHasContentObjectRepository, GenericRelationDefinition } from '../base-has-content-object-repository'; import { ItemRepositoryService } from './item-repository.service'; -import { User } from 'app/shared/models/users/user'; +import { ViewMotion } from 'app/site/motions/models/view-motion'; +import { RelationDefinition } from '../base-repository'; +import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block'; +import { ViewTopic } from 'app/site/topics/models/view-topic'; +import { ViewAssignment } from 'app/site/assignments/models/view-assignment'; +import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; +import { ViewUser } from 'app/site/users/models/view-user'; + +const ListOfSpeakersRelations: (RelationDefinition | GenericRelationDefinition)[] = [ + { + type: 'generic', + possibleModels: [ViewMotion, ViewMotionBlock, ViewTopic, ViewAssignment, ViewMediafile], + isVForeign: isBaseViewModelWithListOfSpeakers, + VForeignVerbose: 'BaseViewModelWithListOfSpeakers' + }, + { + type: 'nested', + ownKey: 'speakers', + foreignModel: ViewSpeaker, + order: 'weight', + relationDefinition: [ + { + type: 'O2M', + ownIdKey: 'user_id', + ownKey: 'user', + foreignModel: ViewUser + } + ] + } +]; /** * Repository service for lists of speakers @@ -60,14 +82,7 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit private httpService: HttpService, private itemRepo: ItemRepositoryService ) { - super(DS, dataSend, mapperService, viewModelStoreService, translate, ListOfSpeakers, [ - Topic, - Assignment, - Motion, - MotionBlock, - Mediafile, - User - ]); + super(DS, dataSend, mapperService, viewModelStoreService, translate, ListOfSpeakers, ListOfSpeakersRelations); } public getVerboseName = (plural: boolean = false) => { @@ -93,48 +108,6 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit } }; - /** - * Creates the viewListOfSpeakers out of a given list of speakers - * - * @param listOfSpeakers the list fo speakers that should be converted to view item - * @returns a new view list fo speakers - */ - public createViewModel(listOfSpeakers: ListOfSpeakers): ViewListOfSpeakers { - const contentObject = this.getContentObject(listOfSpeakers); - const speakers = this.getSpeakers(listOfSpeakers); - return new ViewListOfSpeakers(listOfSpeakers, speakers, contentObject); - } - - private getSpeakers(listOfSpeakers: ListOfSpeakers): ViewSpeaker[] { - return listOfSpeakers.speakers.map(speaker => { - const user = this.viewModelStoreService.get(ViewUser, speaker.user_id); - return new ViewSpeaker(speaker, user); - }); - } - - /** - * Returns the corresponding content object to a given {@link ListOfSpeakers} as an {@link BaseListOfSpeakersViewModel} - * - * @param listOfSpeakers the target list fo speakers - * @returns the content object of the given list of sepakers. Might be null if it was not found. - */ - public getContentObject(listOfSpeakers: ListOfSpeakers): BaseViewModelWithListOfSpeakers { - const contentObject = this.viewModelStoreService.get( - listOfSpeakers.content_object.collection, - listOfSpeakers.content_object.id - ); - if (!contentObject) { - return null; - } - if (isBaseViewModelWithListOfSpeakers(contentObject)) { - return contentObject; - } else { - throw new Error( - `The content object (${listOfSpeakers.content_object.collection}, ${listOfSpeakers.content_object.id}) of list of speakers ${listOfSpeakers.id} is not a BaseListOfSpeakersViewModel.` - ); - } - } - /** * Add a new speaker to a list of speakers. * Sends the users id to the server diff --git a/client/src/app/core/repositories/assignments/assignment-repository.service.ts b/client/src/app/core/repositories/assignments/assignment-repository.service.ts index 5126d62c5..c18e8593d 100644 --- a/client/src/app/core/repositories/assignments/assignment-repository.service.ts +++ b/client/src/app/core/repositories/assignments/assignment-repository.service.ts @@ -3,26 +3,71 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { Assignment } from 'app/shared/models/assignments/assignment'; -import { AssignmentRelatedUser } from 'app/shared/models/assignments/assignment-related-user'; import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service'; import { DataSendService } from 'app/core/core-services/data-send.service'; import { DataStoreService } from '../../core-services/data-store.service'; import { HttpService } from 'app/core/core-services/http.service'; import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll'; -import { Tag } from 'app/shared/models/core/tag'; -import { User } from 'app/shared/models/users/user'; import { ViewAssignment, AssignmentTitleInformation } from 'app/site/assignments/models/view-assignment'; -import { ViewItem } from 'app/site/agenda/models/view-item'; import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; import { ViewTag } from 'app/site/tags/models/view-tag'; import { ViewUser } from 'app/site/users/models/view-user'; import { ViewAssignmentRelatedUser } from 'app/site/assignments/models/view-assignment-related-user'; import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll'; -import { ViewAssignmentPollOption } from 'app/site/assignments/models/view-assignment-poll-option'; -import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; -import { Mediafile } from 'app/shared/models/mediafiles/mediafile'; -import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers'; import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../base-is-agenda-item-and-list-of-speakers-content-object-repository'; +import { RelationDefinition } from '../base-repository'; +import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; +import { ViewAssignmentPollOption } from 'app/site/assignments/models/view-assignment-poll-option'; + +const AssignmentRelations: RelationDefinition[] = [ + { + type: 'M2M', + ownIdKey: 'tags_id', + ownKey: 'tags', + foreignModel: ViewTag + }, + { + type: 'M2M', + ownIdKey: 'attachments_id', + ownKey: 'attachments', + foreignModel: ViewMediafile + }, + { + type: 'nested', + ownKey: 'assignment_related_users', + foreignModel: ViewAssignmentRelatedUser, + order: 'weight', + relationDefinition: [ + { + type: 'O2M', + ownIdKey: 'user_id', + ownKey: 'user', + foreignModel: ViewUser + } + ] + }, + { + type: 'nested', + ownKey: 'polls', + foreignModel: ViewAssignmentPoll, + relationDefinition: [ + { + type: 'nested', + ownKey: 'options', + foreignModel: ViewAssignmentPollOption, + order: 'weight', + relationDefinition: [ + { + type: 'O2M', + ownIdKey: 'user_id', + ownKey: 'user', + foreignModel: ViewUser + } + ] + } + ] + } +]; /** * Repository Service for Assignments. @@ -62,7 +107,7 @@ export class AssignmentRepositoryService extends BaseIsAgendaItemAndListOfSpeake protected translate: TranslateService, private httpService: HttpService ) { - super(DS, dataSend, mapperService, viewModelStoreService, translate, Assignment, [User, Tag, Mediafile]); + super(DS, dataSend, mapperService, viewModelStoreService, translate, Assignment, AssignmentRelations); } public getTitle = (titleInformation: AssignmentTitleInformation) => { @@ -73,48 +118,6 @@ export class AssignmentRepositoryService extends BaseIsAgendaItemAndListOfSpeake return this.translate.instant(plural ? 'Elections' : 'Election'); }; - public createViewModel(assignment: Assignment): ViewAssignment { - const item = this.viewModelStoreService.get(ViewItem, assignment.agenda_item_id); - const listOfSpeakers = this.viewModelStoreService.get(ViewListOfSpeakers, assignment.list_of_speakers_id); - const tags = this.viewModelStoreService.getMany(ViewTag, assignment.tags_id); - const attachments = this.viewModelStoreService.getMany(ViewMediafile, assignment.attachments_id); - const assignmentRelatedUsers = this.createViewAssignmentRelatedUsers(assignment.assignment_related_users); - const assignmentPolls = this.createViewAssignmentPolls(assignment.polls); - - return new ViewAssignment( - assignment, - assignmentRelatedUsers, - assignmentPolls, - item, - listOfSpeakers, - tags, - attachments - ); - } - - private createViewAssignmentRelatedUsers( - assignmentRelatedUsers: AssignmentRelatedUser[] - ): ViewAssignmentRelatedUser[] { - return assignmentRelatedUsers - .map(aru => { - const user = this.viewModelStoreService.get(ViewUser, aru.user_id); - return new ViewAssignmentRelatedUser(aru, user); - }) - .sort((a, b) => a.weight - b.weight); - } - - private createViewAssignmentPolls(assignmentPolls: AssignmentPoll[]): ViewAssignmentPoll[] { - return assignmentPolls.map(poll => { - const options = poll.options - .map(option => { - const user = this.viewModelStoreService.get(ViewUser, option.candidate_id); - return new ViewAssignmentPollOption(option, user); - }) - .sort((a, b) => a.weight - b.weight); - return new ViewAssignmentPoll(poll, options); - }); - } - /** * Adds/removes another user to/from the candidates list of an assignment * diff --git a/client/src/app/core/repositories/base-has-content-object-repository.ts b/client/src/app/core/repositories/base-has-content-object-repository.ts index 467cf9ee4..e3ff9845e 100644 --- a/client/src/app/core/repositories/base-has-content-object-repository.ts +++ b/client/src/app/core/repositories/base-has-content-object-repository.ts @@ -1,8 +1,24 @@ -import { BaseRepository } from './base-repository'; +import { BaseRepository, RelationDefinition } from './base-repository'; import { BaseModelWithContentObject } from 'app/shared/models/base/base-model-with-content-object'; import { BaseViewModelWithContentObject } from 'app/site/base/base-view-model-with-content-object'; import { ContentObject } from 'app/shared/models/base/content-object'; -import { BaseViewModel, TitleInformation } from 'app/site/base/base-view-model'; +import { BaseViewModel, TitleInformation, ViewModelConstructor } from 'app/site/base/base-view-model'; +import { DataStoreService } from '../core-services/data-store.service'; +import { DataSendService } from '../core-services/data-send.service'; +import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service'; +import { ViewModelStoreService } from '../core-services/view-model-store.service'; +import { TranslateService } from '@ngx-translate/core'; +import { ModelConstructor } from 'app/shared/models/base/base-model'; + +/** + * A generic relation for models with a content_object + */ +export interface GenericRelationDefinition { + type: 'generic'; + possibleModels: ViewModelConstructor[]; + isVForeign: (obj: any) => obj is VForeign; + VForeignVerbose: string; +} /** * A base repository for objects that *have* content objects, e.g. items and lists of speakers. @@ -23,6 +39,98 @@ export abstract class BaseHasContentObjectRepository< }; } = {}; + public constructor( + DS: DataStoreService, + dataSend: DataSendService, + collectionStringMapperService: CollectionStringMapperService, + viewModelStoreService: ViewModelStoreService, + translate: TranslateService, + baseModelCtor: ModelConstructor, + relationDefinitions: (RelationDefinition | GenericRelationDefinition)[] = [] + ) { + super(DS, dataSend, collectionStringMapperService, viewModelStoreService, translate, baseModelCtor, < + RelationDefinition[] + >relationDefinitions); // This cast "hides" the new generic relation from the base repository. Typescript can't handle this... + } + + protected _groupRelationsByCollections( + relation: RelationDefinition | GenericRelationDefinition, + baseRelation: RelationDefinition + ): void { + if (relation.type === 'generic') { + relation.possibleModels.forEach(ctor => { + const collection = ctor.COLLECTIONSTRING; + if (!this.relationsByCollection[collection]) { + this.relationsByCollection[collection] = []; + } + // The cast to any is needed to convince Typescript, that a GenericRelationDefinition can also + // be used as a RelationDefinition + this.relationsByCollection[collection].push(baseRelation); + }); + } else { + super._groupRelationsByCollections(relation, baseRelation); + } + } + + /** + * Adds the generic relation. + */ + protected updateSingleDependency( + ownViewModel: V, + relation: RelationDefinition | GenericRelationDefinition, + collection: string, + changedId: number + ): boolean { + if (relation.type === 'generic') { + const foreignModel = this.viewModelStoreService.get(collection, changedId); + if ( + foreignModel && + foreignModel.collectionString === ownViewModel.contentObjectData.collection && + foreignModel.id === ownViewModel.contentObjectData.id + ) { + if (relation.isVForeign(foreignModel)) { + (ownViewModel)._contentObject = foreignModel; + return true; + } else { + throw new Error(`The object is not an ${relation.VForeignVerbose}:` + foreignModel); + } + + // TODO: set reverse + } + } else { + super.updateSingleDependency(ownViewModel, relation, collection, changedId); + } + } + + /** + * Adds the generic relation. + */ + protected setRelationsInViewModel( + model: M, + viewModel: K, + relation: RelationDefinition | GenericRelationDefinition + ): void { + if (relation.type === 'generic') { + (viewModel)._contentObject = this.getContentObject(model, relation); + } else { + super.setRelationsInViewModel(model, viewModel, relation); + } + } + + /** + * Tries to get the content object (as a view model) from the given model and relation. + */ + protected getContentObject(model: M, relation: GenericRelationDefinition): BaseViewModel { + const contentObject = this.viewModelStoreService.get( + model.content_object.collection, + model.content_object.id + ); + if (!contentObject || !relation.isVForeign(contentObject)) { + return null; + } + return contentObject; + } + /** * Returns the object with has the given content object as the content object. * diff --git a/client/src/app/core/repositories/base-is-agenda-item-and-list-of-speakers-content-object-repository.ts b/client/src/app/core/repositories/base-is-agenda-item-and-list-of-speakers-content-object-repository.ts index a460680d2..5e8fd43bb 100644 --- a/client/src/app/core/repositories/base-is-agenda-item-and-list-of-speakers-content-object-repository.ts +++ b/client/src/app/core/repositories/base-is-agenda-item-and-list-of-speakers-content-object-repository.ts @@ -1,7 +1,7 @@ import { TranslateService } from '@ngx-translate/core'; import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model'; -import { BaseRepository } from './base-repository'; +import { BaseRepository, RelationDefinition } from './base-repository'; import { isBaseIsAgendaItemContentObjectRepository, IBaseIsAgendaItemContentObjectRepository @@ -13,8 +13,6 @@ import { import { DataStoreService } from '../core-services/data-store.service'; import { DataSendService } from '../core-services/data-send.service'; import { ViewModelStoreService } from '../core-services/view-model-store.service'; -import { Item } from 'app/shared/models/agenda/item'; -import { ListOfSpeakers } from 'app/shared/models/agenda/list-of-speakers'; import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service'; import { TitleInformationWithAgendaItem, @@ -22,6 +20,8 @@ import { } from 'app/site/base/base-view-model-with-agenda-item'; import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model'; import { IBaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers'; +import { ViewItem } from 'app/site/agenda/models/view-item'; +import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers'; export function isBaseIsAgendaItemAndListOfSpeakersContentObjectRepository( obj: any @@ -50,7 +50,7 @@ export abstract class BaseIsAgendaItemAndListOfSpeakersContentObjectRepository< viewModelStoreService: ViewModelStoreService, translate: TranslateService, baseModelCtor: ModelConstructor, - depsModelCtors?: ModelConstructor[] + relationDefinitions?: RelationDefinition[] ) { super( DS, @@ -59,13 +59,24 @@ export abstract class BaseIsAgendaItemAndListOfSpeakersContentObjectRepository< viewModelStoreService, translate, baseModelCtor, - depsModelCtors + relationDefinitions ); - if (!this.depsModelCtors) { - this.depsModelCtors = []; - } - this.depsModelCtors.push(Item); - this.depsModelCtors.push(ListOfSpeakers); + } + + protected groupRelationsByCollections(): void { + this.relationDefinitions.push({ + type: 'O2M', + ownIdKey: 'agenda_item_id', + ownKey: 'item', + foreignModel: ViewItem + }); + this.relationDefinitions.push({ + type: 'O2M', + ownIdKey: 'list_of_speakers_id', + ownKey: 'list_of_speakers', + foreignModel: ViewListOfSpeakers + }); + super.groupRelationsByCollections(); } public getAgendaListTitle(titleInformation: T): string { diff --git a/client/src/app/core/repositories/base-is-agenda-item-content-object-repository.ts b/client/src/app/core/repositories/base-is-agenda-item-content-object-repository.ts index b504b6ea3..5c90bef62 100644 --- a/client/src/app/core/repositories/base-is-agenda-item-content-object-repository.ts +++ b/client/src/app/core/repositories/base-is-agenda-item-content-object-repository.ts @@ -3,14 +3,14 @@ import { TranslateService } from '@ngx-translate/core'; import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model'; import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service'; import { DataSendService } from '../core-services/data-send.service'; -import { BaseRepository } from './base-repository'; -import { Item } from 'app/shared/models/agenda/item'; +import { BaseRepository, RelationDefinition } from './base-repository'; import { DataStoreService } from '../core-services/data-store.service'; import { ViewModelStoreService } from '../core-services/view-model-store.service'; import { TitleInformationWithAgendaItem, BaseViewModelWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item'; +import { ViewItem } from 'app/site/agenda/models/view-item'; export function isBaseIsAgendaItemContentObjectRepository( obj: any @@ -46,7 +46,7 @@ export abstract class BaseIsAgendaItemContentObjectRepository< viewModelStoreService: ViewModelStoreService, translate: TranslateService, baseModelCtor: ModelConstructor, - depsModelCtors?: ModelConstructor[] + relationDefinitions?: RelationDefinition[] ) { super( DS, @@ -55,12 +55,18 @@ export abstract class BaseIsAgendaItemContentObjectRepository< viewModelStoreService, translate, baseModelCtor, - depsModelCtors + relationDefinitions ); - if (!this.depsModelCtors) { - this.depsModelCtors = []; - } - this.depsModelCtors.push(Item); + } + + protected groupRelationsByCollections(): void { + this.relationDefinitions.push({ + type: 'O2M', + ownIdKey: 'agenda_item_id', + ownKey: 'item', + foreignModel: ViewItem + }); + super.groupRelationsByCollections(); } /** diff --git a/client/src/app/core/repositories/base-is-list-of-speakers-content-object-repository.ts b/client/src/app/core/repositories/base-is-list-of-speakers-content-object-repository.ts index acc19083d..5a351a4d7 100644 --- a/client/src/app/core/repositories/base-is-list-of-speakers-content-object-repository.ts +++ b/client/src/app/core/repositories/base-is-list-of-speakers-content-object-repository.ts @@ -2,13 +2,13 @@ import { TranslateService } from '@ngx-translate/core'; import { TitleInformation } from '../../site/base/base-view-model'; import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model'; -import { BaseRepository } from './base-repository'; +import { BaseRepository, RelationDefinition } from './base-repository'; import { DataStoreService } from '../core-services/data-store.service'; import { DataSendService } from '../core-services/data-send.service'; import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service'; import { ViewModelStoreService } from '../core-services/view-model-store.service'; -import { ListOfSpeakers } from 'app/shared/models/agenda/list-of-speakers'; import { BaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers'; +import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers'; export function isBaseIsListOfSpeakersContentObjectRepository( obj: any @@ -44,7 +44,7 @@ export abstract class BaseIsListOfSpeakersContentObjectRepository< viewModelStoreService: ViewModelStoreService, translate: TranslateService, baseModelCtor: ModelConstructor, - depsModelCtors?: ModelConstructor[] + relationDefinitions?: RelationDefinition[] ) { super( DS, @@ -53,12 +53,18 @@ export abstract class BaseIsListOfSpeakersContentObjectRepository< viewModelStoreService, translate, baseModelCtor, - depsModelCtors + relationDefinitions ); - if (!this.depsModelCtors) { - this.depsModelCtors = []; - } - this.depsModelCtors.push(ListOfSpeakers); + } + + protected groupRelationsByCollections(): void { + this.relationDefinitions.push({ + type: 'O2M', + ownIdKey: 'list_of_speakers_id', + ownKey: 'list_of_speakers', + foreignModel: ViewListOfSpeakers + }); + super.groupRelationsByCollections(); } public getListOfSpeakersTitle(titleInformation: T): string { diff --git a/client/src/app/core/repositories/base-repository.ts b/client/src/app/core/repositories/base-repository.ts index 39264258f..ecdeb590b 100644 --- a/client/src/app/core/repositories/base-repository.ts +++ b/client/src/app/core/repositories/base-repository.ts @@ -2,7 +2,7 @@ import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { auditTime } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; -import { BaseViewModel, TitleInformation } from '../../site/base/base-view-model'; +import { BaseViewModel, TitleInformation, ViewModelConstructor } from '../../site/base/base-view-model'; import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model'; import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service'; import { DataSendService } from '../core-services/data-send.service'; @@ -12,6 +12,89 @@ import { ViewModelStoreService } from '../core-services/view-model-store.service import { OnAfterAppsLoaded } from '../onAfterAppsLoaded'; import { Collection } from 'app/shared/models/base/collection'; +// All "standard" relations. +export type RelationDefinition = + | NormalRelationDefinition + | NestedRelationDefinition + | CustomRelationDefinition; + +/** + * Normal relations. + */ +interface NormalRelationDefinition { + /** + * - O2M: From this model to another one, where this model is the One-side. + * E.g. motions<->categories: One motions has One category; One category has + * Many motions + * - M2M: M2M relation from this to another model. + */ + type: 'M2M' | 'O2M'; + + /** + * The key where the id(s) are given. Must be present in the model and view model. E.g. `category_id`. + */ + ownIdKey: string; + + /** + * The name of the property, where the foreign view model should be accessable. + * Note, that this must be a getter to a private variable `_; + + /** + * TODO: reverse relations. + */ + foreignKey?: keyof VForeign; +} + +/** + * Nested relations in the REST-API. For the most values see + * `NormalRelationDefinition`. + */ +interface NestedRelationDefinition { + type: 'nested'; + ownKey: string; + foreignModel: ViewModelConstructor; + foreignKey?: keyof VForeign; + + /** + * The nested relations. + */ + relationDefinition?: RelationDefinition[]; + + /** + * Provide an extra key (holding a number) to order by. + * If the value is equal or no order key is given, the models + * will be sorted by id. + */ + order?: string; +} + +/** + * A custom relation with callbacks with things todo. + */ +interface CustomRelationDefinition { + type: 'custom'; + foreignModel: ViewModelConstructor; + + /** + * Called, when the view model is created from the model. + */ + setRelations: (model: BaseModel, viewModel: BaseViewModel) => void; + + /** + * Called, when the dependency was updated. + */ + updateDependency: (ownViewModel: BaseViewModel, foreignViewModel: VForeign) => boolean; +} + export abstract class BaseRepository implements OnAfterAppsLoaded, Collection { /** @@ -71,6 +154,20 @@ export abstract class BaseRepository string; public abstract getTitle: (titleInformation: T) => string; + /** + * Maps the given relations (`relationDefinitions`) to their affected collections. This means, + * if a model of the collection updates, the relation needs to be updated. + * + * Attention: Some inherited repos might put other relations than RelationDefinition in here, so + * *always* check the type of the relation. + */ + protected relationsByCollection: { [collection: string]: RelationDefinition[] } = {}; + + /** + * The view model ctor of the encapsulated view model. + */ + protected baseViewModelCtor: ViewModelConstructor; + /** * Construction routine for the base repository * @@ -87,10 +184,12 @@ export abstract class BaseRepository, - protected depsModelCtors?: ModelConstructor[] + protected relationDefinitions: RelationDefinition[] = [] ) { this._collectionString = baseModelCtor.COLLECTIONSTRING; + this.groupRelationsByCollections(); + // All data is piped through an auditTime of 1ms. This is to prevent massive // updates, if e.g. an autoupdate with a lot motions come in. The result is just one // update of the new list instead of many unnecessary updates. @@ -103,7 +202,34 @@ export abstract class BaseRepository { + this._groupRelationsByCollections(relation, relation); + }); + } + + /** + * Recursive function for reorderung the relations. + */ + protected _groupRelationsByCollections(relation: RelationDefinition, baseRelation: RelationDefinition): void { + if (relation.type === 'nested') { + (relation.relationDefinition || []).forEach(nestedRelation => { + this._groupRelationsByCollections(nestedRelation, baseRelation); + }); + } else if (relation.type === 'O2M' || relation.type === 'M2M' || relation.type === 'custom') { + const collection = relation.foreignModel.COLLECTIONSTRING; + if (!this.relationsByCollection[collection]) { + this.relationsByCollection[collection] = []; + } + this.relationsByCollection[collection].push(baseRelation); + } + } + public onAfterAppsLoaded(): void { + this.baseViewModelCtor = this.collectionStringMapperService.getViewModelConstructor(this.collectionString); this.DS.clearObservable.subscribe(() => this.clear()); this.translate.onLangChange.subscribe(change => { this.languageCollator = new Intl.Collator(change.lang); @@ -113,6 +239,10 @@ export abstract class BaseRepository string = (titleInformation: T) => { + return this.getTitle(titleInformation); + }; + /** * Deletes all models from the repository (internally, no requests). Changes need * to be committed via `commitUpdate()`. @@ -139,6 +269,73 @@ export abstract class BaseRepository this.getTitle(viewModel); + viewModel.getListTitle = () => this.getListTitle(viewModel); + viewModel.getVerboseName = this.getVerboseName; + return viewModel; + } + + /** + * Creates a view model from the given model and model ctor. All dependencies will be + * set accorting to relations. + */ + protected createViewModel( + model: M, + modelCtor: ViewModelConstructor, + relations: RelationDefinition[] + ): K { + const viewModel = new modelCtor(model) as K; + + // no reverse setting needed + relations.forEach(relation => { + this.setRelationsInViewModel(model, viewModel, relation); + }); + + return viewModel; + } + + /** + * Sets one foreign view model in the view model according to the relation and the information + * from the model. + */ + protected setRelationsInViewModel( + model: M, + viewModel: K, + relation: RelationDefinition + ): void { + if (relation.type === 'M2M' && model[relation.ownIdKey] instanceof Array) { + const foreignViewModels = this.viewModelStoreService.getMany( + relation.foreignModel, + model[relation.ownIdKey] + ); + viewModel['_' + relation.ownKey] = foreignViewModels; + } else if (relation.type === 'O2M') { + const foreignViewModel = this.viewModelStoreService.get(relation.foreignModel, model[relation.ownIdKey]); + viewModel['_' + relation.ownKey] = foreignViewModel; + } else if (relation.type === 'nested') { + const foreignViewModels: BaseViewModel[] = model[relation.ownKey].map(foreignModel => + this.createViewModel(foreignModel, relation.foreignModel, relation.relationDefinition || []) + ); + foreignViewModels.sort((a: BaseViewModel, b: BaseViewModel) => { + const order = relation.order; + if (!relation.order || a[order] === b[order]) { + return a.id - b.id; + } else { + return a[order] - b[order]; + } + }); + viewModel['_' + relation.ownKey] = foreignViewModels; + } else if (relation.type === 'custom') { + relation.setRelations(model, viewModel); + } + } + /** * Updates all models in this repository with all changed models. * @@ -146,7 +343,7 @@ export abstract class BaseRepository { - const dependencyChanged: boolean = this.depsModelCtors.some(ctor => { - return ctor.COLLECTIONSTRING === collection; - }); + const dependencyChanged: boolean = Object.keys(this.relationsByCollection).includes(collection); if (!dependencyChanged) { return; } // Ok, we are affected by this collection. Update all viewModels from this repo. viewModels.forEach(ownViewModel => { - changedModels[collection].forEach(id => { - ownViewModel.updateDependencies(this.viewModelStoreService.get(collection, id)); + const relations = this.relationsByCollection[collection]; + if (!relations || !relations.length) { + return; + } + relations.forEach(relation => { + changedModels[collection].forEach(id => { + if (this.updateSingleDependency(ownViewModel, relation, collection, id)) { + somethingUpdated = true; + } + }); }); }); - somethingUpdated = true; }); if (somethingUpdated) { viewModels.forEach(ownViewModel => { @@ -177,9 +379,64 @@ export abstract class BaseRepository string = (titleInformation: T) => { - return this.getTitle(titleInformation); - }; + /** + * Updates an own view model with an implicit given model by the collection and changedId. + * + * @return true, if something was updated. + */ + protected updateSingleDependency( + ownViewModel: BaseViewModel, + relation: RelationDefinition, + collection: string, + changedId: number + ): boolean { + if (relation.type === 'M2M') { + if ( + ownViewModel[relation.ownIdKey] && + ownViewModel[relation.ownIdKey] instanceof Array && + ownViewModel[relation.ownIdKey].includes(changedId) + ) { + const foreignViewModel = this.viewModelStoreService.get(collection, changedId); + let ownModelArray = ownViewModel['_' + relation.ownKey]; + if (!ownModelArray) { + ownViewModel['_' + relation.ownKey] = []; + ownModelArray = ownViewModel['_' + relation.ownKey]; + } + const index = ownModelArray.findIndex(user => user.id === changedId); + if (index < 0) { + ownModelArray.push(foreignViewModel); + } else { + ownModelArray[index] = foreignViewModel; + } + // TODO: set reverse + + return true; + } + } else if (relation.type === 'O2M') { + if (ownViewModel[relation.ownIdKey] === changedId) { + ownViewModel['_' + relation.ownKey] = this.viewModelStoreService.get(collection, changedId); + // TODO: set reverse + + return true; + } + } else if (relation.type === 'nested') { + let updated = false; + (relation.relationDefinition || []).forEach(nestedRelation => { + const nestedViewModels = ownViewModel[relation.ownKey] as BaseViewModel[]; + nestedViewModels.forEach(nestedViewModel => { + if (this.updateSingleDependency(nestedViewModel, nestedRelation, collection, changedId)) { + updated = true; + } + }); + }); + return updated; + } else if (relation.type === 'custom') { + const foreignViewModel = this.viewModelStoreService.get(collection, changedId); + return relation.updateDependency(ownViewModel, foreignViewModel); + } + + return false; + } /** * Saves the (full) update to an existing model. So called "update"-function @@ -242,27 +499,6 @@ export abstract class BaseRepository this.getTitle(viewModel); - viewModel.getListTitle = () => this.getListTitle(viewModel); - viewModel.getVerboseName = this.getVerboseName; - return viewModel; - } - /** * Clears the repository. */ diff --git a/client/src/app/core/repositories/config/config-repository.service.ts b/client/src/app/core/repositories/config/config-repository.service.ts index d2b719145..51e2d63d9 100644 --- a/client/src/app/core/repositories/config/config-repository.service.ts +++ b/client/src/app/core/repositories/config/config-repository.service.ts @@ -134,14 +134,6 @@ export class ConfigRepositoryService extends BaseRepository { - this.viewModelStore[config.id] = this.createViewModel(config); - this.updateConfigStructure(false, this.viewModelStore[config.id]); - }); - this.updateConfigListObservable(); - } - public changedModels(ids: number[]): void { super.changedModels(ids); diff --git a/client/src/app/core/repositories/mediafiles/mediafile-repository.service.ts b/client/src/app/core/repositories/mediafiles/mediafile-repository.service.ts index 1c715e65e..035b412b6 100644 --- a/client/src/app/core/repositories/mediafiles/mediafile-repository.service.ts +++ b/client/src/app/core/repositories/mediafiles/mediafile-repository.service.ts @@ -14,9 +14,29 @@ import { DataSendService } from 'app/core/core-services/data-send.service'; import { HttpService } from 'app/core/core-services/http.service'; import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; import { BaseIsListOfSpeakersContentObjectRepository } from '../base-is-list-of-speakers-content-object-repository'; -import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers'; import { ViewGroup } from 'app/site/users/models/view-group'; -import { Group } from 'app/shared/models/users/group'; +import { RelationDefinition } from '../base-repository'; + +const MediafileRelations: RelationDefinition[] = [ + { + type: 'O2M', + ownIdKey: 'parent_id', + ownKey: 'parent', + foreignModel: ViewMediafile + }, + { + type: 'M2M', + ownIdKey: 'access_groups_id', + ownKey: 'access_groups', + foreignModel: ViewGroup + }, + { + type: 'M2M', + ownIdKey: 'inherited_access_groups_id', + ownKey: 'inherited_access_groups', + foreignModel: ViewGroup + } +]; /** * Repository for MediaFiles @@ -46,7 +66,7 @@ export class MediafileRepositoryService extends BaseIsListOfSpeakersContentObjec dataSend: DataSendService, private httpService: HttpService ) { - super(DS, dataSend, mapperService, viewModelStoreService, translate, Mediafile, [Mediafile, Group]); + super(DS, dataSend, mapperService, viewModelStoreService, translate, Mediafile, MediafileRelations); this.directoryBehaviorSubject = new BehaviorSubject([]); this.getViewModelListObservable().subscribe(mediafiles => { if (mediafiles) { @@ -67,25 +87,6 @@ export class MediafileRepositoryService extends BaseIsListOfSpeakersContentObjec return this.translate.instant(plural ? 'Files' : 'File'); }; - /** - * Creates mediafile ViewModels out of given mediafile objects - * - * @param file mediafile to convert - * @returns a new mediafile ViewModel - */ - public createViewModel(file: Mediafile): ViewMediafile { - const listOfSpeakers = this.viewModelStoreService.get(ViewListOfSpeakers, file.list_of_speakers_id); - const parent = this.viewModelStoreService.get(ViewMediafile, file.parent_id); - const accessGroups = this.viewModelStoreService.getMany(ViewGroup, file.access_groups_id); - let inheritedAccessGroups; - if (file.has_inherited_access_groups) { - inheritedAccessGroups = this.viewModelStoreService.getMany(ViewGroup, ( - file.inherited_access_groups_id - )); - } - return new ViewMediafile(file, listOfSpeakers, parent, accessGroups, inheritedAccessGroups); - } - public async getDirectoryIdByPath(pathSegments: string[]): Promise { let parentId = null; diff --git a/client/src/app/core/repositories/motions/category-repository.service.ts b/client/src/app/core/repositories/motions/category-repository.service.ts index 0eb54a060..2a6cf55aa 100644 --- a/client/src/app/core/repositories/motions/category-repository.service.ts +++ b/client/src/app/core/repositories/motions/category-repository.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { BaseRepository } from '../base-repository'; +import { BaseRepository, RelationDefinition } from '../base-repository'; import { Category } from 'app/shared/models/motions/category'; import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service'; import { DataSendService } from '../../core-services/data-send.service'; @@ -13,6 +13,15 @@ import { ViewModelStoreService } from 'app/core/core-services/view-model-store.s import { Motion } from 'app/shared/models/motions/motion'; import { TreeIdNode } from 'app/core/ui-services/tree.service'; +const CategoryRelations: RelationDefinition[] = [ + { + type: 'O2M', + ownIdKey: 'parent_id', + ownKey: 'parent', + foreignModel: ViewCategory + } +]; + /** * Repository Services for Categories * @@ -47,7 +56,7 @@ export class CategoryRepositoryService extends BaseRepository a.weight - b.weight); } @@ -62,11 +71,6 @@ export class CategoryRepositoryService extends BaseRepository { @@ -76,15 +69,6 @@ export class ChangeRecommendationRepositoryService extends BaseRepository< return this.translate.instant(plural ? 'Change recommendations' : 'Change recommendation'); }; - /** - * Creates this view wrapper based on an actual Change Recommendation model - * - * @param {MotionChangeRecommendation} model - */ - protected createViewModel(model: MotionChangeRecommendation): ViewMotionChangeRecommendation { - return new ViewMotionChangeRecommendation(model); - } - /** * Given a change recommendation view object, a entry in the backend is created. * @param view diff --git a/client/src/app/core/repositories/motions/motion-block-repository.service.ts b/client/src/app/core/repositories/motions/motion-block-repository.service.ts index 1fa543d99..453fa28da 100644 --- a/client/src/app/core/repositories/motions/motion-block-repository.service.ts +++ b/client/src/app/core/repositories/motions/motion-block-repository.service.ts @@ -14,8 +14,6 @@ import { MotionRepositoryService } from './motion-repository.service'; import { ViewMotion } from 'app/site/motions/models/view-motion'; import { ViewMotionBlock, MotionBlockTitleInformation } from 'app/site/motions/models/view-motion-block'; import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; -import { ViewItem } from 'app/site/agenda/models/view-item'; -import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers'; import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../base-is-agenda-item-and-list-of-speakers-content-object-repository'; /** @@ -59,18 +57,6 @@ export class MotionBlockRepositoryService extends BaseIsAgendaItemAndListOfSpeak return this.translate.instant(plural ? 'Motion blocks' : 'Motion block'); }; - /** - * Converts a given motion block into a ViewModel - * - * @param block a motion block - * @returns a new ViewMotionBlock - */ - protected createViewModel(block: MotionBlock): ViewMotionBlock { - const item = this.viewModelStoreService.get(ViewItem, block.agenda_item_id); - const listOfSpeakers = this.viewModelStoreService.get(ViewListOfSpeakers, block.list_of_speakers_id); - return new ViewMotionBlock(block, item, listOfSpeakers); - } - /** * Removes the motion block id from the given motion * diff --git a/client/src/app/core/repositories/motions/motion-comment-section-repository.service.ts b/client/src/app/core/repositories/motions/motion-comment-section-repository.service.ts index dd17723b1..600d33650 100644 --- a/client/src/app/core/repositories/motions/motion-comment-section-repository.service.ts +++ b/client/src/app/core/repositories/motions/motion-comment-section-repository.service.ts @@ -4,19 +4,33 @@ import { TranslateService } from '@ngx-translate/core'; import { DataSendService } from '../../core-services/data-send.service'; import { DataStoreService } from '../../core-services/data-store.service'; -import { BaseRepository } from '../base-repository'; +import { BaseRepository, RelationDefinition } from '../base-repository'; import { ViewMotionCommentSection, MotionCommentSectionTitleInformation } from 'app/site/motions/models/view-motion-comment-section'; import { MotionCommentSection } from 'app/shared/models/motions/motion-comment-section'; -import { Group } from 'app/shared/models/users/group'; import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service'; import { HttpService } from 'app/core/core-services/http.service'; import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; import { ViewGroup } from 'app/site/users/models/view-group'; import { ViewMotion } from 'app/site/motions/models/view-motion'; +const MotionCommentSectionRelations: RelationDefinition[] = [ + { + type: 'M2M', + ownIdKey: 'read_groups_id', + ownKey: 'read_groups', + foreignModel: ViewGroup + }, + { + type: 'M2M', + ownIdKey: 'write_groups_id', + ownKey: 'write_groups', + foreignModel: ViewGroup + } +]; + /** * Repository Services for Categories * @@ -53,7 +67,15 @@ export class MotionCommentSectionRepositoryService extends BaseRepository< translate: TranslateService, private http: HttpService ) { - super(DS, dataSend, mapperService, viewModelStoreService, translate, MotionCommentSection, [Group]); + super( + DS, + dataSend, + mapperService, + viewModelStoreService, + translate, + MotionCommentSection, + MotionCommentSectionRelations + ); this.viewModelSortFn = (a: ViewMotionCommentSection, b: ViewMotionCommentSection) => { if (a.weight === b.weight) { @@ -72,18 +94,6 @@ export class MotionCommentSectionRepositoryService extends BaseRepository< return this.translate.instant(plural ? 'Comment sections' : 'Comment section'); }; - /** - * Creates the ViewModel for the MotionComment Section - * - * @param section the MotionCommentSection the View Model should be created of - * @returns the View Model representation of the MotionCommentSection - */ - protected createViewModel(section: MotionCommentSection): ViewMotionCommentSection { - const readGroups = this.viewModelStoreService.getMany(ViewGroup, section.read_groups_id); - const writeGroups = this.viewModelStoreService.getMany(ViewGroup, section.write_groups_id); - return new ViewMotionCommentSection(section, readGroups, writeGroups); - } - /** * Saves a comment made at a MotionCommentSection. Does an update, if * there is a comment text. Deletes the comment, if the text is empty. diff --git a/client/src/app/core/repositories/motions/motion-repository.service.ts b/client/src/app/core/repositories/motions/motion-repository.service.ts index 186fac8ac..7648c54b1 100644 --- a/client/src/app/core/repositories/motions/motion-repository.service.ts +++ b/client/src/app/core/repositories/motions/motion-repository.service.ts @@ -5,42 +5,35 @@ import { TranslateService } from '@ngx-translate/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { Category } from 'app/shared/models/motions/category'; import { ChangeRecoMode, MotionTitleInformation, ViewMotion } from 'app/site/motions/models/view-motion'; import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service'; import { ConfigService } from 'app/core/ui-services/config.service'; import { DataSendService } from '../../core-services/data-send.service'; -import { DataStoreService, CollectionIds } from 'app/core/core-services/data-store.service'; +import { DataStoreService } from 'app/core/core-services/data-store.service'; import { DiffService, DiffLinesInParagraph } from 'app/core/ui-services/diff.service'; import { HttpService } from 'app/core/core-services/http.service'; import { LinenumberingService, LineNumberRange } from '../../ui-services/linenumbering.service'; -import { Mediafile } from 'app/shared/models/mediafiles/mediafile'; import { Motion } from 'app/shared/models/motions/motion'; -import { MotionBlock } from 'app/shared/models/motions/motion-block'; -import { MotionChangeRecommendation } from 'app/shared/models/motions/motion-change-reco'; import { MotionPoll } from 'app/shared/models/motions/motion-poll'; import { TreeIdNode } from 'app/core/ui-services/tree.service'; -import { User } from 'app/shared/models/users/user'; import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation'; import { ViewMotionAmendedParagraph } from 'app/site/motions/models/view-motion-amended-paragraph'; import { ViewUnifiedChange } from 'app/shared/models/motions/view-unified-change'; import { ViewStatuteParagraph } from 'app/site/motions/models/view-statute-paragraph'; -import { Workflow } from 'app/shared/models/motions/workflow'; -import { WorkflowState } from 'app/shared/models/motions/workflow-state'; -import { Tag } from 'app/shared/models/core/tag'; import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; import { ViewCategory } from 'app/site/motions/models/view-category'; import { ViewUser } from 'app/site/users/models/view-user'; import { ViewWorkflow } from 'app/site/motions/models/view-workflow'; -import { ViewItem } from 'app/site/agenda/models/view-item'; import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block'; import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; import { ViewTag } from 'app/site/tags/models/view-tag'; import { ViewPersonalNote } from 'app/site/users/models/view-personal-note'; import { OperatorService } from 'app/core/core-services/operator.service'; -import { PersonalNote, PersonalNoteContent } from 'app/shared/models/users/personal-note'; -import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers'; +import { PersonalNoteContent } from 'app/shared/models/users/personal-note'; import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../base-is-agenda-item-and-list-of-speakers-content-object-repository'; +import { RelationDefinition } from '../base-repository'; +import { ViewState } from 'app/site/motions/models/view-state'; +import { ViewSubmitter } from 'app/site/motions/models/view-submitter'; type SortProperty = 'weight' | 'identifier'; @@ -74,6 +67,84 @@ export interface ParagraphToChoose { lineTo: number; } +const MotionRelations: RelationDefinition[] = [ + { + type: 'O2M', + ownIdKey: 'state_id', + ownKey: 'state', + foreignModel: ViewState + }, + { + type: 'O2M', + ownIdKey: 'recommendation_id', + ownKey: 'recommendation', + foreignModel: ViewState + }, + { + type: 'O2M', + ownIdKey: 'workflow_id', + ownKey: 'workflow', + foreignModel: ViewWorkflow + }, + { + type: 'O2M', + ownIdKey: 'category_id', + ownKey: 'category', + foreignModel: ViewCategory + }, + { + type: 'O2M', + ownIdKey: 'motion_block_id', + ownKey: 'motion_block', + foreignModel: ViewMotionBlock + }, + { + type: 'nested', + ownKey: 'submitters', + foreignModel: ViewSubmitter, + order: 'weight', + relationDefinition: [ + { + type: 'O2M', + ownIdKey: 'user_id', + ownKey: 'user', + foreignModel: ViewUser + } + ] + }, + { + type: 'M2M', + ownIdKey: 'supporters_id', + ownKey: 'supporters', + foreignModel: ViewUser + }, + { + type: 'M2M', + ownIdKey: 'attachments_id', + ownKey: 'attachments', + foreignModel: ViewMediafile + }, + { + type: 'M2M', + ownIdKey: 'tags_id', + ownKey: 'tags', + foreignModel: ViewTag + }, + { + type: 'O2M', + ownIdKey: 'parent_id', + ownKey: 'parent', + foreignModel: ViewMotion + }, + { + type: 'M2M', + ownIdKey: 'change_recommendations_id', + ownKey: 'changeRecommendations', + foreignModel: ViewMotionChangeRecommendation + } + // Personal notes are dynamically added in the repo. +]; + /** * Repository Services for motions (and potentially categories) * @@ -126,23 +197,36 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo private readonly diff: DiffService, private operator: OperatorService ) { - super(DS, dataSend, mapperService, viewModelStoreService, translate, Motion, [ - Category, - User, - Workflow, - MotionBlock, - Mediafile, - Tag, - MotionChangeRecommendation, - PersonalNote, - Motion - ]); + super(DS, dataSend, mapperService, viewModelStoreService, translate, Motion, MotionRelations); config.get('motions_motions_sorting').subscribe(conf => { this.sortProperty = conf; this.setConfigSortFn(); }); } + /** + * Adds the personal note custom relation to the relation definitions. + */ + protected groupRelationsByCollections(): void { + this.relationDefinitions.push({ + type: 'custom', + foreignModel: ViewPersonalNote, + setRelations: (motion: Motion, viewMotion: ViewMotion) => { + viewMotion.personalNote = this.getPersonalNoteForMotion(motion); + }, + updateDependency: (viewMotion: ViewMotion, viewPersonalNote: ViewPersonalNote) => { + const personalNoteContent = viewPersonalNote.getNoteContent(this.collectionString, viewMotion.id); + if (!personalNoteContent) { + return false; + } + + viewMotion.personalNote = personalNoteContent; + return true; + } + }); + super.groupRelationsByCollections(); + } + public getTitle = (titleInformation: MotionTitleInformation) => { if (titleInformation.identifier) { return titleInformation.identifier + ': ' + titleInformation.title; @@ -183,64 +267,20 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo return this.translate.instant(plural ? 'Motions' : 'Motion'); }; - /** - * Converts a motion to a ViewMotion and adds it to the store. - * - * Foreign references of the motion will be resolved (e.g submitters to users) - * Expandable to all (server side) changes that might occur on the motion object. - * - * @param motion blank motion domain object - */ - protected createViewModel(motion: Motion): ViewMotion { - const category = this.viewModelStoreService.get(ViewCategory, motion.category_id); - const submitters = this.viewModelStoreService.getMany(ViewUser, motion.sorted_submitters_id); - const supporters = this.viewModelStoreService.getMany(ViewUser, motion.supporters_id); - const workflow = this.viewModelStoreService.get(ViewWorkflow, motion.workflow_id); - const item = this.viewModelStoreService.get(ViewItem, motion.agenda_item_id); - const listOfSpeakers = this.viewModelStoreService.get(ViewListOfSpeakers, motion.list_of_speakers_id); - const block = this.viewModelStoreService.get(ViewMotionBlock, motion.motion_block_id); - const attachments = this.viewModelStoreService.getMany(ViewMediafile, motion.attachments_id); - const tags = this.viewModelStoreService.getMany(ViewTag, motion.tags_id); - const parent = this.viewModelStoreService.get(ViewMotion, motion.parent_id); - const amendments = this.viewModelStoreService.filter(ViewMotion, m => m.parent_id && m.parent_id === motion.id); - const changeRecommendations = this.viewModelStoreService.filter( - ViewMotionChangeRecommendation, - cr => cr.motion_id === motion.id - ); - let state: WorkflowState = null; - if (workflow) { - state = workflow.getStateById(motion.state_id); - } - const personalNote = this.getPersonalNoteForMotion(motion.id); - const viewMotion = new ViewMotion( - motion, - category, - submitters, - supporters, - workflow, - state, - item, - listOfSpeakers, - block, - attachments, - tags, - parent, - changeRecommendations, - amendments, - personalNote - ); - viewMotion.getIdentifierOrTitle = () => this.getIdentifierOrTitle(viewMotion); - viewMotion.getProjectorTitle = () => this.getAgendaSlideTitle(viewMotion); - return viewMotion; + protected createViewModelWithTitles(model: Motion): ViewMotion { + const viewModel = super.createViewModelWithTitles(model); + viewModel.getIdentifierOrTitle = () => this.getIdentifierOrTitle(viewModel); + viewModel.getProjectorTitle = () => this.getAgendaSlideTitle(viewModel); + return viewModel; } /** * Get the personal note content for one motion by their id * - * @param motionId the id of the motion + * @param motion the motion * @returns the personal note content for this motion or null */ - private getPersonalNoteForMotion(motionId: number): PersonalNoteContent | null { + private getPersonalNoteForMotion(motion: Motion): PersonalNoteContent | null { if (this.operator.isAnonymous) { return; } @@ -248,64 +288,17 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo const personalNote = this.viewModelStoreService.find(ViewPersonalNote, pn => { return pn.userId === this.operator.user.id; }); - if (!personalNote) { return; } const notes = personalNote.notes; const collection = Motion.COLLECTIONSTRING; - if (notes && notes[collection] && notes[collection][motionId]) { - return notes[collection][motionId]; + if (notes && notes[collection] && notes[collection][motion.id]) { + return notes[collection][motion.id]; } } - /** - * Special handling of updating personal notes. - * @override - */ - public updateDependencies(changedModels: CollectionIds): boolean { - if (!this.depsModelCtors || this.depsModelCtors.length === 0) { - return; - } - const viewModels = this.getViewModelList(); - let somethingUpdated = false; - Object.keys(changedModels).forEach(collection => { - const dependencyChanged: boolean = this.depsModelCtors.some(ctor => { - return ctor.COLLECTIONSTRING === collection; - }); - if (!dependencyChanged) { - return; - } - - // Do not update personal notes, if the operator is anonymous - if (collection === PersonalNote.COLLECTIONSTRING && this.operator.isAnonymous) { - return; - } - - viewModels.forEach(ownViewModel => { - changedModels[collection].forEach(id => { - const viewModel = this.viewModelStoreService.get(collection, id); - // Only update the personal note, if the operator is the right user. - if ( - collection === PersonalNote.COLLECTIONSTRING && - (viewModel).userId !== this.operator.user.id - ) { - return; - } - ownViewModel.updateDependencies(viewModel); - }); - }); - somethingUpdated = true; - }); - if (somethingUpdated) { - viewModels.forEach(ownViewModel => { - this.updateViewModelObservable(ownViewModel.id); - }); - } - return somethingUpdated; - } - /** * Set the state of a motion * @@ -757,20 +750,6 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo .filter((para: ViewMotionAmendedParagraph) => para !== null); } - /** - * Returns motion duplicates (sharing the identifier) - * - * @param viewMotion the ViewMotion to compare against the list of Motions - * in the data - * @returns An Array of ViewMotions with the same identifier of the input, or an empty array - */ - public getMotionDuplicates(motion: ViewMotion): ViewMotion[] { - const duplicates = this.DS.filter(Motion, item => motion.identifier === item.identifier); - const viewMotions: ViewMotion[] = []; - duplicates.forEach(item => viewMotions.push(this.createViewModel(item))); - return viewMotions; - } - /** * Sends a request to the server, creating a new poll for the motion */ diff --git a/client/src/app/core/repositories/motions/state-repository.service.spec.ts b/client/src/app/core/repositories/motions/state-repository.service.spec.ts new file mode 100644 index 000000000..f69a058ba --- /dev/null +++ b/client/src/app/core/repositories/motions/state-repository.service.spec.ts @@ -0,0 +1,17 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { E2EImportsModule } from '../../../../e2e-imports.module'; +import { StateRepositoryService } from './state-repository.service'; + +describe('StateRepositoryService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule], + providers: [StateRepositoryService] + }); + }); + + it('should be created', inject([StateRepositoryService], (service: StateRepositoryService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/client/src/app/core/repositories/motions/state-repository.service.ts b/client/src/app/core/repositories/motions/state-repository.service.ts new file mode 100644 index 000000000..66f05f21c --- /dev/null +++ b/client/src/app/core/repositories/motions/state-repository.service.ts @@ -0,0 +1,69 @@ +import { Injectable } from '@angular/core'; + +import { TranslateService } from '@ngx-translate/core'; + +import { WorkflowTitleInformation, ViewWorkflow } from 'app/site/motions/models/view-workflow'; +import { DataSendService } from '../../core-services/data-send.service'; +import { DataStoreService } from '../../core-services/data-store.service'; +import { BaseRepository, RelationDefinition } from '../base-repository'; +import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service'; +import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; +import { State } from 'app/shared/models/motions/state'; +import { ViewState, StateTitleInformation } from 'app/site/motions/models/view-state'; + +const StateRelations: RelationDefinition[] = [ + { + type: 'O2M', + ownIdKey: 'workflow_id', + ownKey: 'workflow', + foreignModel: ViewWorkflow + }, + { + type: 'M2M', + ownIdKey: 'next_states_id', + ownKey: 'next_states', + foreignModel: ViewState + } +]; + +/** + * Repository Services for States + * + * The repository is meant to process domain objects (those found under + * shared/models), so components can display them and interact with them. + * + * Rather than manipulating models directly, the repository is meant to + * inform the {@link DataSendService} about changes which will send + * them to the Server. + */ +@Injectable({ + providedIn: 'root' +}) +export class StateRepositoryService extends BaseRepository { + /** + * Creates a WorkflowRepository + * Converts existing and incoming workflow to ViewWorkflows + * + * @param DS Accessing the data store + * @param mapperService mapping models + * @param dataSend sending data to the server + * @param httpService HttpService + */ + public constructor( + DS: DataStoreService, + dataSend: DataSendService, + mapperService: CollectionStringMapperService, + viewModelStoreService: ViewModelStoreService, + translate: TranslateService + ) { + super(DS, dataSend, mapperService, viewModelStoreService, translate, State, StateRelations); + } + + public getTitle = (titleInformation: WorkflowTitleInformation) => { + return titleInformation.name; + }; + + public getVerboseName = (plural: boolean = false) => { + return this.translate.instant(plural ? 'Workflows' : 'Workflow'); + }; +} diff --git a/client/src/app/core/repositories/motions/statute-paragraph-repository.service.ts b/client/src/app/core/repositories/motions/statute-paragraph-repository.service.ts index 6d14647f9..16b1976c9 100644 --- a/client/src/app/core/repositories/motions/statute-paragraph-repository.service.ts +++ b/client/src/app/core/repositories/motions/statute-paragraph-repository.service.ts @@ -51,8 +51,4 @@ export class StatuteParagraphRepositoryService extends BaseRepository< public getVerboseName = (plural: boolean = false) => { return this.translate.instant(plural ? 'Statute paragraphs' : 'Statute paragraph'); }; - - protected createViewModel(statuteParagraph: StatuteParagraph): ViewStatuteParagraph { - return new ViewStatuteParagraph(statuteParagraph); - } } diff --git a/client/src/app/core/repositories/motions/workflow-repository.service.ts b/client/src/app/core/repositories/motions/workflow-repository.service.ts index 9dab25d7f..8d7a16f5e 100644 --- a/client/src/app/core/repositories/motions/workflow-repository.service.ts +++ b/client/src/app/core/repositories/motions/workflow-repository.service.ts @@ -6,15 +6,29 @@ import { Workflow } from 'app/shared/models/motions/workflow'; import { ViewWorkflow, WorkflowTitleInformation } from 'app/site/motions/models/view-workflow'; import { DataSendService } from '../../core-services/data-send.service'; import { DataStoreService } from '../../core-services/data-store.service'; -import { BaseRepository } from '../base-repository'; +import { BaseRepository, RelationDefinition } from '../base-repository'; import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service'; -import { WorkflowState } from 'app/shared/models/motions/workflow-state'; import { ViewMotion } from 'app/site/motions/models/view-motion'; -import { HttpService } from 'app/core/core-services/http.service'; import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; +import { ViewState } from 'app/site/motions/models/view-state'; + +const WorkflowRelations: RelationDefinition[] = [ + { + type: 'M2M', + ownIdKey: 'states_id', + ownKey: 'states', + foreignModel: ViewState + }, + { + type: 'O2M', + ownIdKey: 'first_state_id', + ownKey: 'first_state', + foreignModel: ViewState + } +]; /** - * Repository Services for Categories + * Repository Services for Workflows * * The repository is meant to process domain objects (those found under * shared/models), so components can display them and interact with them. @@ -27,11 +41,6 @@ import { ViewModelStoreService } from 'app/core/core-services/view-model-store.s providedIn: 'root' }) export class WorkflowRepositoryService extends BaseRepository { - /** - * The url to state on rest - */ - private restStateUrl = '/rest/motions/state/'; - /** * Creates a WorkflowRepository * Converts existing and incoming workflow to ViewWorkflows @@ -46,16 +55,9 @@ export class WorkflowRepositoryService extends BaseRepository { - if (models && models.length > 0) { - this.initSorting(models); - } - }); + super(DS, dataSend, mapperService, viewModelStoreService, translate, Workflow, WorkflowRelations); } public getTitle = (titleInformation: WorkflowTitleInformation) => { @@ -66,86 +68,14 @@ export class WorkflowRepositoryService extends BaseRepository { - const newStatePayload = { - name: stateName, - workflow_id: viewWorkflow.id - }; - await this.httpService.post(this.restStateUrl, newStatePayload); - } - - /** - * Updates workflow state with a new value-object and sends it to the server - * - * @param newValue a key-value pair with the new state value - * @param workflowState the workflow state to update - */ - public async updateState(newValue: object, workflowState: WorkflowState): Promise { - const stateUpdate = Object.assign(workflowState, newValue); - await this.httpService.put(`${this.restStateUrl}${workflowState.id}/`, stateUpdate); - } - - /** - * Deletes the selected work - * - * @param workflowState the workflow state to delete - */ - public async deleteState(workflowState: WorkflowState): Promise { - await this.httpService.delete(`${this.restStateUrl}${workflowState.id}/`); - } - - /** - * Collects all existing states from all workflows - * - * @returns All currently existing workflow states - */ - public getAllWorkflowStates(): WorkflowState[] { - let states: WorkflowState[] = []; - this.getViewModelList().forEach(workflow => { - if (workflow) { - states = states.concat(workflow.states); - } - }); - return states; - } - /** * Returns all workflowStates that cover the list of viewMotions given * * @param motions The motions to get the workflows from * @returns The workflow states to the given motion */ - public getWorkflowStatesForMotions(motions: ViewMotion[]): WorkflowState[] { - let states: WorkflowState[] = []; + public getWorkflowStatesForMotions(motions: ViewMotion[]): ViewState[] { + let states: ViewState[] = []; const workflowIds = motions .map(motion => motion.workflow_id) .filter((value, index, self) => self.indexOf(value) === index); diff --git a/client/src/app/core/repositories/projector/countdown-repository.service.ts b/client/src/app/core/repositories/projector/countdown-repository.service.ts index 0408c538f..0ab0740db 100644 --- a/client/src/app/core/repositories/projector/countdown-repository.service.ts +++ b/client/src/app/core/repositories/projector/countdown-repository.service.ts @@ -36,10 +36,6 @@ export class CountdownRepositoryService extends BaseRepository { return this.translate.instant(plural ? 'Messages' : 'Message'); }; - - protected createViewModel(message: ProjectorMessage): ViewProjectorMessage { - return new ViewProjectorMessage(message); - } } diff --git a/client/src/app/core/repositories/projector/projector-repository.service.ts b/client/src/app/core/repositories/projector/projector-repository.service.ts index 52e8789cd..0a03e83a2 100644 --- a/client/src/app/core/repositories/projector/projector-repository.service.ts +++ b/client/src/app/core/repositories/projector/projector-repository.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { BaseRepository } from '../base-repository'; +import { BaseRepository, RelationDefinition } from '../base-repository'; import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service'; import { DataSendService } from '../../core-services/data-send.service'; import { DataStoreService } from '../../core-services/data-store.service'; @@ -21,6 +21,15 @@ export enum ScrollScaleDirection { Reset = 'reset' } +const ProjectorRelations: RelationDefinition[] = [ + { + type: 'O2M', + ownIdKey: 'reference_projector_id', + ownKey: 'referenceProjector', + foreignModel: ViewProjector + } +]; + /** * Manages all projector instances. */ @@ -44,7 +53,7 @@ export class ProjectorRepositoryService extends BaseRepository { @@ -55,10 +64,6 @@ export class ProjectorRepositoryService extends BaseRepository { @@ -63,29 +70,4 @@ export class TopicRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCon public getVerboseName = (plural: boolean = false) => { return this.translate.instant(plural ? 'Topics' : 'Topic'); }; - - /** - * Creates a new viewModel out of the given model - * - * @param topic The topic that shall be converted into a view topic - * @returns a new view topic - */ - public createViewModel(topic: Topic): ViewTopic { - const attachments = this.viewModelStoreService.getMany(ViewMediafile, topic.attachments_id); - const item = this.viewModelStoreService.get(ViewItem, topic.agenda_item_id); - const listOfSpeakers = this.viewModelStoreService.get(ViewListOfSpeakers, topic.list_of_speakers_id); - return new ViewTopic(topic, attachments, item, listOfSpeakers); - } - - /** - * Returns an array of all duplicates for a topic - * - * @param topic - */ - public getTopicDuplicates(topic: ViewTopic): ViewTopic[] { - const duplicates = this.DS.filter(Topic, item => topic.title === item.title); - const viewTopics: ViewTopic[] = []; - duplicates.forEach(item => viewTopics.push(this.createViewModel(item))); - return viewTopics; - } } diff --git a/client/src/app/core/repositories/users/group-repository.service.ts b/client/src/app/core/repositories/users/group-repository.service.ts index 11b90a81d..b2ccdd584 100644 --- a/client/src/app/core/repositories/users/group-repository.service.ts +++ b/client/src/app/core/repositories/users/group-repository.service.ts @@ -70,10 +70,6 @@ export class GroupRepositoryService extends BaseRepository('users_sort_by').subscribe(conf => { this.sortProperty = conf; @@ -133,12 +141,14 @@ export class UserRepositoryService extends BaseRepository this.getFullName(viewUser); - viewUser.getShortName = () => this.getShortName(viewUser); - return viewUser; + /** + * Adds teh short and full name to the view user. + */ + protected createViewModelWithTitles(model: User): ViewUser { + const viewModel = super.createViewModelWithTitles(model); + viewModel.getFullName = () => this.getFullName(viewModel); + viewModel.getShortName = () => this.getShortName(viewModel); + return viewModel; } /** 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 271056e29..523a5157f 100644 --- a/client/src/app/core/ui-services/base-import.service.ts +++ b/client/src/app/core/ui-services/base-import.service.ts @@ -25,7 +25,7 @@ export interface NewEntry { newEntry: V; status: CsvImportStatus; errors: string[]; - duplicates: V[]; + hasDuplicates: boolean; importTrackId?: number; } @@ -259,11 +259,11 @@ export abstract class BaseImportService { if (entry.status === 'done') { summary.done += 1; return; - } else if (entry.status === 'error' && !entry.duplicates.length) { + } else if (entry.status === 'error' && !entry.hasDuplicates) { // errors that are not due to duplicates summary.errors += 1; return; - } else if (entry.duplicates.length) { + } else if (entry.hasDuplicates) { summary.duplicates += 1; return; } else if (entry.status === 'new') { diff --git a/client/src/app/shared/models/agenda/speaker.ts b/client/src/app/shared/models/agenda/speaker.ts index 18d36e394..403100245 100644 --- a/client/src/app/shared/models/agenda/speaker.ts +++ b/client/src/app/shared/models/agenda/speaker.ts @@ -1,3 +1,5 @@ +import { BaseModel } from '../base/base-model'; + /** * Representation of a speaker in an agenda item. * @@ -5,23 +7,28 @@ * Part of the 'speakers' list. * @ignore */ -export interface Speaker { - id: number; - user_id: number; +export class Speaker extends BaseModel { + public static COLLECTIONSTRING = 'agenda/speaker'; + + public id: number; + public user_id: number; + public weight: number; + public marked: boolean; + public item_id: number; /** * ISO datetime string to indicate the begin time of the speech. Empty if * the speaker has not started */ - begin_time: string; + public begin_time: string; /** * ISO datetime string to indicate the end time of the speech. Empty if the * speech has not ended */ - end_time: string; + public end_time: string; - weight: number; - marked: boolean; - item_id: number; + public constructor(input?: any) { + super(Speaker.COLLECTIONSTRING, input); + } } diff --git a/client/src/app/shared/models/assignments/assignment-poll-option.ts b/client/src/app/shared/models/assignments/assignment-poll-option.ts index 92fee7de9..78da1f667 100644 --- a/client/src/app/shared/models/assignments/assignment-poll-option.ts +++ b/client/src/app/shared/models/assignments/assignment-poll-option.ts @@ -1,5 +1,5 @@ -import { Deserializer } from '../base/deserializer'; import { PollVoteValue } from 'app/core/ui-services/poll.service'; +import { BaseModel } from '../base/base-model'; export interface AssignmentOptionVote { weight: number; @@ -12,7 +12,9 @@ export interface AssignmentOptionVote { * part of the 'polls-options'-array in poll * @ignore */ -export class AssignmentPollOption extends Deserializer { +export class AssignmentPollOption extends BaseModel { + public static COLLECTIONSTRING = 'assignments/assignment-poll-option'; + public id: number; // The AssignmentUser id of the candidate public candidate_id: number; // the User id of the candidate public is_elected: boolean; @@ -21,8 +23,6 @@ export class AssignmentPollOption extends Deserializer { public weight: number; // weight to order the display /** - * Needs to be completely optional because poll has (yet) the optional parameter 'poll-options' - * * @param input */ public constructor(input?: any) { @@ -33,6 +33,6 @@ export class AssignmentPollOption extends Deserializer { } }); } - super(input); + super(AssignmentPollOption.COLLECTIONSTRING, input); } } diff --git a/client/src/app/shared/models/assignments/assignment-poll.ts b/client/src/app/shared/models/assignments/assignment-poll.ts index 85564e03f..51d5abf98 100644 --- a/client/src/app/shared/models/assignments/assignment-poll.ts +++ b/client/src/app/shared/models/assignments/assignment-poll.ts @@ -1,12 +1,13 @@ import { AssignmentPollMethod } from 'app/site/assignments/services/assignment-poll.service'; -import { Deserializer } from '../base/deserializer'; import { AssignmentPollOption } from './assignment-poll-option'; +import { BaseModel } from '../base/base-model'; /** * Content of the 'polls' property of assignments * @ignore */ -export class AssignmentPoll extends Deserializer { +export class AssignmentPoll extends BaseModel { + public static COLLECTIONSTRING = 'assignments/assignment-poll'; private static DECIMAL_FIELDS = ['votesvalid', 'votesinvalid', 'votescast', 'votesno', 'votesabstain']; public id: number; @@ -23,7 +24,6 @@ export class AssignmentPoll extends Deserializer { public assignment_id: number; /** - * Needs to be completely optional because assignment has (yet) the optional parameter 'polls' * @param input */ public constructor(input?: any) { @@ -35,14 +35,6 @@ export class AssignmentPoll extends Deserializer { } }); } - super(input); - } - - public deserialize(input: any): void { - Object.assign(this, input); - this.options = []; - if (input.options instanceof Array) { - this.options = input.options.map(pollOptionData => new AssignmentPollOption(pollOptionData)); - } + super(AssignmentPoll.COLLECTIONSTRING, input); } } diff --git a/client/src/app/shared/models/assignments/assignment-related-user.ts b/client/src/app/shared/models/assignments/assignment-related-user.ts index 95e718f8d..5325d2a9f 100644 --- a/client/src/app/shared/models/assignments/assignment-related-user.ts +++ b/client/src/app/shared/models/assignments/assignment-related-user.ts @@ -1,27 +1,18 @@ +import { BaseModel } from '../base/base-model'; + /** * Content of the 'assignment_related_users' property. */ -export interface AssignmentRelatedUser { - id: number; +export class AssignmentRelatedUser extends BaseModel { + public static COLLECTIONSTRING = 'assignments/assignment-related-user'; - /** - * id of the user this assignment user relates to - */ - user_id: number; + public id: number; + public user_id: number; + public elected: boolean; + public assignment_id: number; + public weight: number; - /** - * The current 'elected' state - */ - elected: boolean; - - /** - * id of the related assignment - */ - assignment_id: number; - - /** - * A weight to determine the position in the list of candidates - * (determined by the server) - */ - weight: number; + public constructor(input?: any) { + super(AssignmentRelatedUser.COLLECTIONSTRING, input); + } } diff --git a/client/src/app/shared/models/motions/motion-submitter.ts b/client/src/app/shared/models/motions/motion-submitter.ts deleted file mode 100644 index d5bb9e8ac..000000000 --- a/client/src/app/shared/models/motions/motion-submitter.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Deserializer } from '../base/deserializer'; -import { User } from '../users/user'; - -/** - * Representation of a Motion Submitter. - * - * @ignore - */ -export class MotionSubmitter extends Deserializer { - public id: number; - public user_id: number; - public motion_id: number; - public weight: number; - - public constructor(input?: any, motion_id?: number, weight?: number) { - super(); - this.id = input.id; - if (input instanceof User) { - const user_obj = input as User; - this.user_id = user_obj.id; - this.motion_id = motion_id; - this.weight = weight; - } else { - this.deserialize(input); - } - } -} diff --git a/client/src/app/shared/models/motions/motion.ts b/client/src/app/shared/models/motions/motion.ts index e6bdcad7c..2d7f321d0 100644 --- a/client/src/app/shared/models/motions/motion.ts +++ b/client/src/app/shared/models/motions/motion.ts @@ -1,4 +1,4 @@ -import { MotionSubmitter } from './motion-submitter'; +import { Submitter } from './submitter'; import { MotionPoll } from './motion-poll'; import { BaseModelWithAgendaItemAndListOfSpeakers } from '../base/base-model-with-agenda-item-and-list-of-speakers'; @@ -31,7 +31,7 @@ export class Motion extends BaseModelWithAgendaItemAndListOfSpeakers { public category_weight: number; public motion_block_id: number; public origin: string; - public submitters: MotionSubmitter[]; + public submitters: Submitter[]; public supporters_id: number[]; public comments: MotionComment[]; public workflow_id: number; @@ -48,6 +48,7 @@ export class Motion extends BaseModelWithAgendaItemAndListOfSpeakers { public sort_parent_id: number; public created: string; public last_modified: string; + public change_recommendations_id: number[]; public constructor(input?: any) { super(Motion.COLLECTIONSTRING, input); @@ -58,9 +59,9 @@ export class Motion extends BaseModelWithAgendaItemAndListOfSpeakers { */ public get sorted_submitters_id(): number[] { return this.submitters - .sort((a: MotionSubmitter, b: MotionSubmitter) => { + .sort((a: Submitter, b: Submitter) => { return a.weight - b.weight; }) - .map((submitter: MotionSubmitter) => submitter.user_id); + .map((submitter: Submitter) => submitter.user_id); } } diff --git a/client/src/app/shared/models/motions/workflow-state.ts b/client/src/app/shared/models/motions/state.ts similarity index 69% rename from client/src/app/shared/models/motions/workflow-state.ts rename to client/src/app/shared/models/motions/state.ts index 8cb644f12..12c5ad921 100644 --- a/client/src/app/shared/models/motions/workflow-state.ts +++ b/client/src/app/shared/models/motions/state.ts @@ -1,5 +1,4 @@ -import { Deserializer } from '../base/deserializer'; -import { Workflow } from './workflow'; +import { BaseModel } from '../base/base-model'; /** * Specifies if an amendment of this state/recommendation should be merged into the motion @@ -16,7 +15,9 @@ export enum MergeAmendment { * Part of the 'states'-array in motion/workflow * @ignore */ -export class WorkflowState extends Deserializer { +export class State extends BaseModel { + public static COLLECTIONSTRING = 'motions/state'; + public id: number; public name: string; public recommendation_label: string; @@ -37,23 +38,7 @@ export class WorkflowState extends Deserializer { * @param input If given, it will be deserialized */ public constructor(input?: any) { - super(input); - } - - /** - * return a list of the next possible states. - * Also adds the current state. - */ - public getNextStates(workflow: Workflow): WorkflowState[] { - return workflow.states.filter(state => { - return this.next_states_id.includes(state.id); - }); - } - - public getPreviousStates(workflow: Workflow): WorkflowState[] { - return workflow.states.filter(state => { - return state.next_states_id.includes(this.id); - }); + super(State.COLLECTIONSTRING, input); } public toString = (): string => { diff --git a/client/src/app/shared/models/motions/submitter.ts b/client/src/app/shared/models/motions/submitter.ts new file mode 100644 index 000000000..4509750f2 --- /dev/null +++ b/client/src/app/shared/models/motions/submitter.ts @@ -0,0 +1,19 @@ +import { BaseModel } from '../base/base-model'; + +/** + * Representation of a Motion Submitter. + * + * @ignore + */ +export class Submitter extends BaseModel { + public static COLLECTIONSTRING = 'motions/submitter'; + + public id: number; + public user_id: number; + public motion_id: number; + public weight: number; + + public constructor(input?: any) { + super(Submitter.COLLECTIONSTRING, input); + } +} diff --git a/client/src/app/shared/models/motions/workflow.ts b/client/src/app/shared/models/motions/workflow.ts index ea42883fa..86cf6b705 100644 --- a/client/src/app/shared/models/motions/workflow.ts +++ b/client/src/app/shared/models/motions/workflow.ts @@ -1,5 +1,4 @@ import { BaseModel } from '../base/base-model'; -import { WorkflowState } from './workflow-state'; /** * Representation of a motion workflow. Has the nested property 'states' @@ -10,40 +9,10 @@ export class Workflow extends BaseModel { public id: number; public name: string; - public states: WorkflowState[]; + public states_id: number[]; public first_state_id: number; public constructor(input?: any) { super(Workflow.COLLECTIONSTRING, input); } - - /** - * Check if the containing @link{WorkflowState}s contain a given ID - * @param id The State ID - */ - public isStateContained(obj: number | WorkflowState): boolean { - let id: number; - if (obj instanceof WorkflowState) { - id = obj.id; - } else { - id = obj; - } - - return this.states.some(state => state.id === id); - } - - /** - * Sorts the states of this workflow - */ - public sortStates(): void { - this.states.sort((a, b) => (a.id > b.id ? 1 : -1)); - } - - public deserialize(input: any): void { - Object.assign(this, input); - - if (input.states instanceof Array) { - this.states = input.states.map(workflowStateData => new WorkflowState(workflowStateData)); - } - } } diff --git a/client/src/app/site/agenda/models/view-item.ts b/client/src/app/site/agenda/models/view-item.ts index 0e1b7fa39..837de6410 100644 --- a/client/src/app/site/agenda/models/view-item.ts +++ b/client/src/app/site/agenda/models/view-item.ts @@ -1,8 +1,5 @@ import { Item, ItemVisibilityChoices } from 'app/shared/models/agenda/item'; -import { - BaseViewModelWithAgendaItem, - isBaseViewModelWithAgendaItem -} from 'app/site/base/base-view-model-with-agenda-item'; +import { BaseViewModelWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item'; import { BaseViewModelWithContentObject } from 'app/site/base/base-view-model-with-content-object'; import { ContentObject } from 'app/shared/models/base/content-object'; @@ -87,7 +84,7 @@ export class ViewItem extends BaseViewModelWithContentObject this.getTitle() }; } - - public updateDependencies(update: BaseViewModel): boolean { - const updated = super.updateDependencies(update); - if (!updated && update instanceof ViewUser) { - return this.speakers.map(speaker => speaker.updateDependencies(update)).some(x => x); - } - return updated; - } } diff --git a/client/src/app/site/agenda/models/view-speaker.ts b/client/src/app/site/agenda/models/view-speaker.ts index 0599a98f6..ddf7d98d9 100644 --- a/client/src/app/site/agenda/models/view-speaker.ts +++ b/client/src/app/site/agenda/models/view-speaker.ts @@ -1,8 +1,6 @@ import { BaseViewModel } from 'app/site/base/base-view-model'; import { Speaker } from 'app/shared/models/agenda/speaker'; import { ViewUser } from 'app/site/users/models/view-user'; -import { Updateable } from 'app/site/base/updateable'; -import { Identifiable } from 'app/shared/models/base/identifiable'; /** * Determine the state of the speaker @@ -16,15 +14,15 @@ export enum SpeakerState { /** * Provides "safe" access to a speaker with all it's components */ -export class ViewSpeaker implements Updateable, Identifiable { - private _speaker: Speaker; +export class ViewSpeaker extends BaseViewModel { + public static COLLECTIONSTRING = Speaker.COLLECTIONSTRING; private _user?: ViewUser; public get speaker(): Speaker { - return this._speaker; + return this._model; } - public get user(): ViewUser { + public get user(): ViewUser | null { return this._user; } @@ -82,20 +80,11 @@ export class ViewSpeaker implements Updateable, Identifiable { return this.user ? this.user.gender : ''; } - public constructor(speaker: Speaker, user?: ViewUser) { - this._speaker = speaker; - this._user = user; + public constructor(speaker: Speaker) { + super(Speaker.COLLECTIONSTRING, speaker); } public getTitle = () => { return this.name; }; - - public updateDependencies(update: BaseViewModel): boolean { - if (update instanceof ViewUser && update.id === this.speaker.user_id) { - this._user = update; - return true; - } - return false; - } } diff --git a/client/src/app/site/agenda/services/agenda-import.service.ts b/client/src/app/site/agenda/services/agenda-import.service.ts index 043f7dd3f..2cf322d71 100644 --- a/client/src/app/site/agenda/services/agenda-import.service.ts +++ b/client/src/app/site/agenda/services/agenda-import.service.ts @@ -100,7 +100,7 @@ export class AgendaImportService extends BaseImportService { newEntry[this.expectedHeader[idx]] = line[idx]; } } - const updateModels = this.repo.getTopicDuplicates(newEntry) as 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) { @@ -108,12 +108,11 @@ export class AgendaImportService extends BaseImportService { } const mappedEntry: NewEntry = { newEntry: newEntry, - duplicates: [], + hasDuplicates: hasDuplicates, status: 'new', errors: [] }; - if (updateModels.length) { - mappedEntry.duplicates = updateModels; + if (hasDuplicates) { this.setError(mappedEntry, 'Duplicates'); } if (hasErrors) { @@ -198,17 +197,14 @@ export class AgendaImportService extends BaseImportService { agenda_type: 1 // set type to 'public item' by default }) ); + const hasDuplicates = this.repo.getViewModelList().some(topic => topic.title === newTopic.title); const newEntry: NewEntry = { newEntry: newTopic, - duplicates: [], + hasDuplicates: hasDuplicates, status: 'new', errors: [] }; - const duplicates = this.repo.getTopicDuplicates(newTopic); - if (duplicates.length) { - // TODO duplicates are not really ViewCreateTopics, but ViewTopics. - // TODO this should be fine as the duplicates will not be created - newEntry.duplicates = duplicates as ViewCreateTopic[]; + if (hasDuplicates) { this.setError(newEntry, 'Duplicates'); } newEntries.push(newEntry); diff --git a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html index ebe9b0a34..975cf5f70 100644 --- a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html +++ b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html @@ -162,10 +162,10 @@

Candidates

- {{ option.user.full_name }} + {{ option.user.getFullName() }} + No user {{ option.user_id }}
diff --git a/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts b/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts index ef1b47c1a..ea7bdef21 100644 --- a/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts +++ b/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts @@ -236,7 +236,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit } // TODO additional conditions: assignment not finished? - const viewAssignmentRelatedUser = this.assignment.assignmentRelatedUsers.find( + const viewAssignmentRelatedUser = this.assignment.assignment_related_users.find( user => user.user_id === option.user_id ); if (viewAssignmentRelatedUser) { diff --git a/client/src/app/site/assignments/models/view-assignment-poll-option.ts b/client/src/app/site/assignments/models/view-assignment-poll-option.ts index c941783f0..126d294e2 100644 --- a/client/src/app/site/assignments/models/view-assignment-poll-option.ts +++ b/client/src/app/site/assignments/models/view-assignment-poll-option.ts @@ -1,8 +1,6 @@ import { AssignmentPollOption, AssignmentOptionVote } from 'app/shared/models/assignments/assignment-poll-option'; import { BaseViewModel } from 'app/site/base/base-view-model'; -import { Identifiable } from 'app/shared/models/base/identifiable'; import { PollVoteValue } from 'app/core/ui-services/poll.service'; -import { Updateable } from 'app/site/base/updateable'; import { ViewUser } from 'app/site/users/models/view-user'; /** @@ -10,12 +8,12 @@ import { ViewUser } from 'app/site/users/models/view-user'; */ const votesOrder: PollVoteValue[] = ['Votes', 'Yes', 'No', 'Abstain']; -export class ViewAssignmentPollOption implements Identifiable, Updateable { - private _assignmentPollOption: AssignmentPollOption; - private _user: ViewUser; // This is the "candidate". We'll stay consistent wich user here... +export class ViewAssignmentPollOption extends BaseViewModel { + public static COLLECTIONSTRING = AssignmentPollOption.COLLECTIONSTRING; + private _user?: ViewUser; // This is the "candidate". We'll stay consistent wich user here... public get option(): AssignmentPollOption { - return this._assignmentPollOption; + return this._model; } /** @@ -52,14 +50,7 @@ export class ViewAssignmentPollOption implements Identifiable, Updateable { return this.option.weight; } - public constructor(assignmentPollOption: AssignmentPollOption, user?: ViewUser) { - this._assignmentPollOption = assignmentPollOption; - this._user = user; - } - - public updateDependencies(update: BaseViewModel): void { - if (update instanceof ViewUser && update.id === this.user_id) { - this._user = update; - } + public constructor(assignmentPollOption: AssignmentPollOption) { + super(AssignmentPollOption.COLLECTIONSTRING, assignmentPollOption); } } diff --git a/client/src/app/site/assignments/models/view-assignment-poll.ts b/client/src/app/site/assignments/models/view-assignment-poll.ts index f919ec43a..bec9fbedf 100644 --- a/client/src/app/site/assignments/models/view-assignment-poll.ts +++ b/client/src/app/site/assignments/models/view-assignment-poll.ts @@ -1,22 +1,20 @@ -import { BaseViewModel } from 'app/site/base/base-view-model'; -import { Updateable } from 'app/site/base/updateable'; -import { Identifiable } from 'app/shared/models/base/identifiable'; import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll'; import { AssignmentPollMethod } from '../services/assignment-poll.service'; import { ViewAssignmentPollOption } from './view-assignment-poll-option'; +import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; +import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model'; import { AssignmentPollOption } from 'app/shared/models/assignments/assignment-poll-option'; -import { Projectable, ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; -export class ViewAssignmentPoll implements Identifiable, Updateable, Projectable { - private _assignmentPoll: AssignmentPoll; - private _assignmentPollOptions: ViewAssignmentPollOption[]; +export class ViewAssignmentPoll extends BaseProjectableViewModel { + public static COLLECTIONSTRING = AssignmentPoll.COLLECTIONSTRING; + private _options: ViewAssignmentPollOption[]; public get poll(): AssignmentPoll { - return this._assignmentPoll; + return this._model; } public get options(): ViewAssignmentPollOption[] { - return this._assignmentPollOptions; + return this._options; } public get id(): number { @@ -78,43 +76,38 @@ export class ViewAssignmentPoll implements Identifiable, Updateable, Projectable return this.poll.assignment_id; } - public constructor(assignmentPoll: AssignmentPoll, assignmentPollOptions: ViewAssignmentPollOption[]) { - this._assignmentPoll = assignmentPoll; - this._assignmentPollOptions = assignmentPollOptions; + public constructor(assignmentPoll: AssignmentPoll) { + super(AssignmentPoll.COLLECTIONSTRING, assignmentPoll); } - public updateDependencies(update: BaseViewModel): void { - this.options.forEach(option => option.updateDependencies(update)); - } + public getTitle = () => { + return 'Poll'; + }; - public getTitle(): string { - return 'TODO'; - } - - public getListTitle(): string { + public getListTitle = () => { return this.getTitle(); - } + }; - public getProjectorTitle(): string { + public getProjectorTitle = () => { return this.getTitle(); - } + }; /** * Creates a copy with deep-copy on all changing numerical values, * but intact uncopied references to the users * - * TODO check and review + * TODO: This MUST NOT be done this way. Do not create ViewModels on your own... */ public copy(): ViewAssignmentPoll { - return new ViewAssignmentPoll( - new AssignmentPoll(JSON.parse(JSON.stringify(this._assignmentPoll))), - this._assignmentPollOptions.map(option => { - return new ViewAssignmentPollOption( - new AssignmentPollOption(JSON.parse(JSON.stringify(option.option))), - option.user - ); - }) - ); + const poll = new ViewAssignmentPoll(new AssignmentPoll(JSON.parse(JSON.stringify(this.poll)))); + (poll)._options = this.options.map(option => { + const polloption = new ViewAssignmentPollOption( + new AssignmentPollOption(JSON.parse(JSON.stringify(option.option))) + ); + (polloption)._user = option.user; + return polloption; + }); + return poll; } public getSlide(): ProjectorElementBuildDeskriptor { diff --git a/client/src/app/site/assignments/models/view-assignment-related-user.ts b/client/src/app/site/assignments/models/view-assignment-related-user.ts index 4ccce12f7..bba9a4ceb 100644 --- a/client/src/app/site/assignments/models/view-assignment-related-user.ts +++ b/client/src/app/site/assignments/models/view-assignment-related-user.ts @@ -1,16 +1,14 @@ import { AssignmentRelatedUser } from 'app/shared/models/assignments/assignment-related-user'; import { BaseViewModel } from 'app/site/base/base-view-model'; -import { Displayable } from 'app/site/base/displayable'; -import { Identifiable } from 'app/shared/models/base/identifiable'; -import { Updateable } from 'app/site/base/updateable'; import { ViewUser } from 'app/site/users/models/view-user'; -export class ViewAssignmentRelatedUser implements Updateable, Identifiable, Displayable { - private _assignmentRelatedUser: AssignmentRelatedUser; +export class ViewAssignmentRelatedUser extends BaseViewModel { + public static COLLECTIONSTRING = AssignmentRelatedUser.COLLECTIONSTRING; + private _user?: ViewUser; public get assignmentRelatedUser(): AssignmentRelatedUser { - return this._assignmentRelatedUser; + return this._model; } public get user(): ViewUser { @@ -37,22 +35,13 @@ export class ViewAssignmentRelatedUser implements Updateable, Identifiable, Disp return this.assignmentRelatedUser.weight; } - public constructor(assignmentRelatedUser: AssignmentRelatedUser, user?: ViewUser) { - this._assignmentRelatedUser = assignmentRelatedUser; - this._user = user; + public getListTitle: () => string = this.getTitle; + + public constructor(assignmentRelatedUser: AssignmentRelatedUser) { + super(AssignmentRelatedUser.COLLECTIONSTRING, assignmentRelatedUser); } - public updateDependencies(update: BaseViewModel): void { - if (update instanceof ViewUser && update.id === this.user_id) { - this._user = update; - } - } - - public getTitle(): string { - return this.user ? this.user.getTitle() : ''; - } - - public getListTitle(): string { - return this.getTitle(); - } + public getTitle: () => string = () => { + return this.user ? this.user.getFullName() : ''; + }; } diff --git a/client/src/app/site/assignments/models/view-assignment.ts b/client/src/app/site/assignments/models/view-assignment.ts index 613c68fe0..ae3964cc2 100644 --- a/client/src/app/site/assignments/models/view-assignment.ts +++ b/client/src/app/site/assignments/models/view-assignment.ts @@ -2,14 +2,11 @@ import { Assignment } from 'app/shared/models/assignments/assignment'; import { SearchRepresentation } from 'app/core/ui-services/search.service'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { ViewUser } from 'app/site/users/models/view-user'; -import { ViewItem } from 'app/site/agenda/models/view-item'; import { ViewTag } from 'app/site/tags/models/view-tag'; -import { BaseViewModel } from 'app/site/base/base-view-model'; import { ViewAssignmentRelatedUser } from './view-assignment-related-user'; import { ViewAssignmentPoll } from './view-assignment-poll'; import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; import { BaseViewModelWithAgendaItemAndListOfSpeakers } from 'app/site/base/base-view-model-with-agenda-item-and-list-of-speakers'; -import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers'; import { TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item'; export interface AssignmentTitleInformation extends TitleInformationWithAgendaItem { @@ -43,8 +40,8 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers implements AssignmentTitleInformation { public static COLLECTIONSTRING = Assignment.COLLECTIONSTRING; - private _assignmentRelatedUsers: ViewAssignmentRelatedUser[]; - private _assignmentPolls: ViewAssignmentPoll[]; + private _assignment_related_users?: ViewAssignmentRelatedUser[]; + private _polls?: ViewAssignmentPoll[]; private _tags?: ViewTag[]; private _attachments?: ViewMediafile[]; @@ -53,7 +50,7 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers } public get polls(): ViewAssignmentPoll[] { - return this._assignmentPolls; + return this._polls || []; } public get title(): string { @@ -69,21 +66,29 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers } public get candidates(): ViewUser[] { - return this._assignmentRelatedUsers.map(aru => aru.user); + return this.assignment_related_users.map(aru => aru.user).filter(x => !!x); } - public get assignmentRelatedUsers(): ViewAssignmentRelatedUser[] { - return this._assignmentRelatedUsers; + public get assignment_related_users(): ViewAssignmentRelatedUser[] { + return this._assignment_related_users || []; } public get tags(): ViewTag[] { return this._tags || []; } + public get tags_id(): number[] { + return this.assignment.tags_id; + } + public get attachments(): ViewMediafile[] { return this._attachments || []; } + public get attachments_id(): number[] { + return this.assignment.attachments_id; + } + /** * unknown where the identifier to the phase is get */ @@ -117,46 +122,11 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers * @returns the amount of candidates in the assignment's candidate list */ public get candidateAmount(): number { - return this._assignmentRelatedUsers ? this._assignmentRelatedUsers.length : 0; + return this._assignment_related_users ? this._assignment_related_users.length : 0; } - public constructor( - assignment: Assignment, - assignmentRelatedUsers: ViewAssignmentRelatedUser[], - assignmentPolls: ViewAssignmentPoll[], - item?: ViewItem, - listOfSpeakers?: ViewListOfSpeakers, - tags?: ViewTag[], - attachments?: ViewMediafile[] - ) { - super(Assignment.COLLECTIONSTRING, assignment, item, listOfSpeakers); - - this._assignmentRelatedUsers = assignmentRelatedUsers; - this._assignmentPolls = assignmentPolls; - this._tags = tags; - this._attachments = attachments; - } - - public updateDependencies(update: BaseViewModel): void { - super.updateDependencies(update); - if (update instanceof ViewTag && this.assignment.tags_id.includes(update.id)) { - const tagIndex = this._tags.findIndex(_tag => _tag.id === update.id); - if (tagIndex < 0) { - this._tags.push(update); - } else { - this._tags[tagIndex] = update; - } - } else if (update instanceof ViewUser) { - this.assignmentRelatedUsers.forEach(aru => aru.updateDependencies(update)); - this.polls.forEach(poll => poll.updateDependencies(update)); - } else if (update instanceof ViewMediafile && this.assignment.attachments_id.includes(update.id)) { - const mediafileIndex = this._attachments.findIndex(_mediafile => _mediafile.id === update.id); - if (mediafileIndex < 0) { - this._attachments.push(update); - } else { - this._attachments[mediafileIndex] = update; - } - } + public constructor(assignment: Assignment) { + super(Assignment.COLLECTIONSTRING, assignment); } public formatForSearch(): SearchRepresentation { diff --git a/client/src/app/site/assignments/services/assignment-pdf.service.ts b/client/src/app/site/assignments/services/assignment-pdf.service.ts index 27fe53256..4ae26bc0d 100644 --- a/client/src/app/site/assignments/services/assignment-pdf.service.ts +++ b/client/src/app/site/assignments/services/assignment-pdf.service.ts @@ -127,7 +127,7 @@ export class AssignmentPdfService { */ private createCandidateList(assignment: ViewAssignment): object { if (assignment.phase !== 2) { - const candidates = assignment.assignmentRelatedUsers.sort((a, b) => a.weight - b.weight); + const candidates = assignment.assignment_related_users.sort((a, b) => a.weight - b.weight); const candidatesText = `${this.translate.instant('Candidates')}: `; const userList = candidates.map(candidate => { diff --git a/client/src/app/site/base/base-import-list.ts b/client/src/app/site/base/base-import-list.ts index 14ad0099d..3cfeca25a 100644 --- a/client/src/app/site/base/base-import-list.ts +++ b/client/src/app/site/base/base-import-list.ts @@ -191,10 +191,7 @@ export abstract class BaseImportListComponent extends B }; } else if (this.shown === 'error') { this.dataSource.filterPredicate = (data, filter) => { - if (data.errors.length || data.duplicates.length) { - return true; - } - return false; + return !!data.errors.length || data.hasDuplicates; }; } this.dataSource.filter = 'X'; // TODO: This is just a bogus non-null string to trigger the filter diff --git a/client/src/app/site/base/base-view-model-with-agenda-item-and-list-of-speakers.ts b/client/src/app/site/base/base-view-model-with-agenda-item-and-list-of-speakers.ts index 059c29ac1..3e62be619 100644 --- a/client/src/app/site/base/base-view-model-with-agenda-item-and-list-of-speakers.ts +++ b/client/src/app/site/base/base-view-model-with-agenda-item-and-list-of-speakers.ts @@ -8,7 +8,6 @@ import { isBaseViewModelWithListOfSpeakers, IBaseViewModelWithListOfSpeakers } from './base-view-model-with-list-of-speakers'; -import { BaseViewModel } from './base-view-model'; export function isBaseViewModelWithAgendaItemAndListOfSpeakers( obj: any @@ -23,7 +22,7 @@ export abstract class BaseViewModelWithAgendaItemAndListOfSpeakers< M extends BaseModelWithAgendaItemAndListOfSpeakers = any > extends BaseProjectableViewModel implements IBaseViewModelWithAgendaItem, IBaseViewModelWithListOfSpeakers { protected _item?: ViewItem; - protected _listOfSpeakers?: ViewListOfSpeakers; + protected _list_of_speakers?: ViewListOfSpeakers; public get agendaItem(): ViewItem | null { return this._item; @@ -38,7 +37,7 @@ export abstract class BaseViewModelWithAgendaItemAndListOfSpeakers< } public get listOfSpeakers(): ViewListOfSpeakers | null { - return this._listOfSpeakers; + return this._list_of_speakers; } public get list_of_speakers_id(): number { @@ -50,11 +49,8 @@ export abstract class BaseViewModelWithAgendaItemAndListOfSpeakers< public getListOfSpeakersTitle: () => string; public getListOfSpeakersSlideTitle: () => string; - public constructor(collectionString: string, model: M, item?: ViewItem, listOfSpeakers?: ViewListOfSpeakers) { + public constructor(collectionString: string, model: M) { super(collectionString, model); - // Explicit set to null instead of undefined, if not given - this._item = item || null; - this._listOfSpeakers = listOfSpeakers || null; } /** @@ -71,12 +67,4 @@ export abstract class BaseViewModelWithAgendaItemAndListOfSpeakers< * Should return a string representation of the object, so there can be searched for. */ public abstract formatForSearch(): SearchRepresentation; - - public updateDependencies(update: BaseViewModel): void { - if (update instanceof ViewItem && update.id === this.agenda_item_id) { - this._item = update; - } else if (update instanceof ViewListOfSpeakers && update.id === this.list_of_speakers_id) { - this._listOfSpeakers = update; - } - } } diff --git a/client/src/app/site/base/base-view-model-with-agenda-item.ts b/client/src/app/site/base/base-view-model-with-agenda-item.ts index ce50ed553..54f5f9c82 100644 --- a/client/src/app/site/base/base-view-model-with-agenda-item.ts +++ b/client/src/app/site/base/base-view-model-with-agenda-item.ts @@ -3,8 +3,7 @@ import { SearchRepresentation } from 'app/core/ui-services/search.service'; import { isDetailNavigable, DetailNavigable } from 'app/shared/models/base/detail-navigable'; import { isSearchable, Searchable } from './searchable'; import { BaseModelWithAgendaItem } from 'app/shared/models/base/base-model-with-agenda-item'; -import { BaseViewModel, TitleInformation } from './base-view-model'; -import { Item } from 'app/shared/models/agenda/item'; +import { TitleInformation } from './base-view-model'; export function isBaseViewModelWithAgendaItem(obj: any): obj is BaseViewModelWithAgendaItem { const model = obj; @@ -105,11 +104,4 @@ export abstract class BaseViewModelWithAgendaItem obj is C, - private CVerbose: string, - contentObject?: C - ) { + public constructor(collectionString: string, model: M) { super(collectionString, model); - this._contentObject = contentObject; - } - - /** - * Check, if the given model mathces the content object definition. If so, the function - * returns true, else false. - */ - public updateDependencies(update: BaseViewModel): boolean { - if ( - update && - update.collectionString === this.contentObjectData.collection && - update.id === this.contentObjectData.id - ) { - if (this.isC(update)) { - this._contentObject = update; - return true; - } else { - throw new Error(`The object is not an ${this.CVerbose}:` + update); - } - } - return false; } } diff --git a/client/src/app/site/base/base-view-model-with-list-of-speakers.ts b/client/src/app/site/base/base-view-model-with-list-of-speakers.ts index 101ab9811..33139619c 100644 --- a/client/src/app/site/base/base-view-model-with-list-of-speakers.ts +++ b/client/src/app/site/base/base-view-model-with-list-of-speakers.ts @@ -1,8 +1,6 @@ import { BaseProjectableViewModel } from './base-projectable-view-model'; import { isDetailNavigable, DetailNavigable } from 'app/shared/models/base/detail-navigable'; import { BaseModelWithListOfSpeakers } from 'app/shared/models/base/base-model-with-list-of-speakers'; -import { BaseViewModel } from './base-view-model'; -import { ListOfSpeakers } from 'app/shared/models/agenda/list-of-speakers'; export function isBaseViewModelWithListOfSpeakers(obj: any): obj is BaseViewModelWithListOfSpeakers { const model = obj; @@ -38,10 +36,10 @@ export interface IBaseViewModelWithListOfSpeakers extends BaseProjectableViewModel implements IBaseViewModelWithListOfSpeakers { - protected _listOfSpeakers?: any; + protected _list_of_speakers?: any; public get listOfSpeakers(): any | null { - return this._listOfSpeakers; + return this._list_of_speakers; } public get list_of_speakers_id(): number { @@ -53,15 +51,8 @@ export abstract class BaseViewModelWithListOfSpeakers { /** * Base class for view models. alls view models should have titles. */ -export abstract class BaseViewModel - implements Displayable, Identifiable, Collection, Updateable { +export abstract class BaseViewModel implements Displayable, Identifiable, Collection { protected _model: M; public get id(): number { @@ -72,8 +70,6 @@ export abstract class BaseViewModel return this._model; } - public abstract updateDependencies(update: BaseViewModel): void; - public toString(): string { return this.getTitle(); } diff --git a/client/src/app/site/config/models/view-config.ts b/client/src/app/site/config/models/view-config.ts index 321360613..13da70b5e 100644 --- a/client/src/app/site/config/models/view-config.ts +++ b/client/src/app/site/config/models/view-config.ts @@ -94,8 +94,6 @@ export class ViewConfig extends BaseViewModel implements ConfigTitleInfo super(Config.COLLECTIONSTRING, config); } - public updateDependencies(update: BaseViewModel): void {} - /** * Returns the time this config field needs to debounce before sending a request to the server. * A little debounce time for all inputs is given here and is usefull, if inputs sends multiple onChange-events, diff --git a/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html b/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html index 09bd63fca..b105fceb9 100644 --- a/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html +++ b/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html @@ -52,8 +52,8 @@ {{ directory.title }} - - + + + + + + Noone + + + diff --git a/client/src/app/site/mediafiles/models/view-mediafile.ts b/client/src/app/site/mediafiles/models/view-mediafile.ts index f34034e62..9607e5d43 100644 --- a/client/src/app/site/mediafiles/models/view-mediafile.ts +++ b/client/src/app/site/mediafiles/models/view-mediafile.ts @@ -1,10 +1,8 @@ -import { BaseViewModel } from '../../base/base-view-model'; import { Mediafile } from 'app/shared/models/mediafiles/mediafile'; import { Searchable } from 'app/site/base/searchable'; import { SearchRepresentation } from 'app/core/ui-services/search.service'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { BaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers'; -import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers'; import { ViewGroup } from 'app/site/users/models/view-group'; export const IMAGE_MIMETYPES = ['image/png', 'image/jpeg', 'image/gif']; @@ -95,17 +93,8 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers return this.mediafile.create_timestamp ? this.mediafile.create_timestamp : null; } - public constructor( - mediafile: Mediafile, - listOfSpeakers?: ViewListOfSpeakers, - parent?: ViewMediafile, - access_groups?: ViewGroup[], - inherited_access_groups?: ViewGroup[] - ) { - super(Mediafile.COLLECTIONSTRING, mediafile, listOfSpeakers); - this._parent = parent; - this._access_groups = access_groups; - this._inherited_access_groups = inherited_access_groups; + public constructor(mediafile: Mediafile) { + super(Mediafile.COLLECTIONSTRING, mediafile); } public formatForSearch(): SearchRepresentation { @@ -202,29 +191,4 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers return 'insert_drive_file'; } } - - public updateDependencies(update: BaseViewModel): void { - super.updateDependencies(update); - if (update instanceof ViewMediafile && update.id === this.parent_id) { - this._parent = update; - } else if (update instanceof ViewGroup) { - if (this.access_groups_id.includes(update.id)) { - const groupIndex = this.access_groups.findIndex(group => group.id === update.id); - if (groupIndex < 0) { - this.access_groups.push(update); - } else { - this.access_groups[groupIndex] = update; - } - } - - if (this.has_inherited_access_groups && (this.inherited_access_groups_id).includes(update.id)) { - const groupIndex = this.inherited_access_groups.findIndex(group => group.id === update.id); - if (groupIndex < 0) { - this.inherited_access_groups.push(update); - } else { - this.inherited_access_groups[groupIndex] = update; - } - } - } - } } diff --git a/client/src/app/site/motions/models/view-category.ts b/client/src/app/site/motions/models/view-category.ts index 9ba876c60..22b74deb8 100644 --- a/client/src/app/site/motions/models/view-category.ts +++ b/client/src/app/site/motions/models/view-category.ts @@ -75,9 +75,8 @@ export class ViewCategory extends BaseViewModel implements CategoryTit } } - public constructor(category: Category, parent?: ViewCategory) { + public constructor(category: Category) { super(Category.COLLECTIONSTRING, category); - this._parent = parent; } public formatForSearch(): SearchRepresentation { @@ -102,14 +101,4 @@ export class ViewCategory extends BaseViewModel implements CategoryTit return []; } } - - /** - * Updates the local objects if required - * @param update - */ - public updateDependencies(update: BaseViewModel): void { - if (update instanceof ViewCategory && update.id === this.parent_id) { - this._parent = update; - } - } } diff --git a/client/src/app/site/motions/models/view-create-motion.ts b/client/src/app/site/motions/models/view-create-motion.ts index 8fefd69c9..6c0551be7 100644 --- a/client/src/app/site/motions/models/view-create-motion.ts +++ b/client/src/app/site/motions/models/view-create-motion.ts @@ -1,16 +1,6 @@ -import { WorkflowState } from 'app/shared/models/motions/workflow-state'; import { ViewMotion } from './view-motion'; import { CreateMotion } from './create-motion'; import { ViewUser } from 'app/site/users/models/view-user'; -import { ViewMotionBlock } from './view-motion-block'; -import { ViewItem } from 'app/site/agenda/models/view-item'; -import { ViewCategory } from './view-category'; -import { ViewWorkflow } from './view-workflow'; -import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers'; -import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; -import { ViewTag } from 'app/site/tags/models/view-tag'; -import { ViewMotionChangeRecommendation } from './view-motion-change-recommendation'; -import { PersonalNoteContent } from 'app/shared/models/users/personal-note'; /** * Create motion class for the View. Its different to ViewMotion in fact that the submitter handling is different @@ -21,57 +11,23 @@ import { PersonalNoteContent } from 'app/shared/models/users/personal-note'; export class ViewCreateMotion extends ViewMotion { protected _model: CreateMotion; + protected _submitterUsers: ViewUser[]; + public get motion(): CreateMotion { return this._model; } - public get submitters(): ViewUser[] { - return this._submitters; + public get submittersAsUsers(): ViewUser[] { + return this._submitterUsers; } - public get submitters_id(): number[] { - return this.motion ? this.motion.sorted_submitters_id : null; - } - - public set submitters(users: ViewUser[]) { - this._submitters = users; + public set submittersAsUsers(users: ViewUser[]) { + this._submitterUsers = users; this._model.submitters_id = users.map(user => user.id); } - public constructor( - motion: CreateMotion, - category?: ViewCategory, - submitters?: ViewUser[], - supporters?: ViewUser[], - workflow?: ViewWorkflow, - state?: WorkflowState, - item?: ViewItem, - listOfSpeakers?: ViewListOfSpeakers, - block?: ViewMotionBlock, - attachments?: ViewMediafile[], - tags?: ViewTag[], - parent?: ViewMotion, - changeRecommendations?: ViewMotionChangeRecommendation[], - amendments?: ViewMotion[], - personalNote?: PersonalNoteContent - ) { - super( - motion, - category, - submitters, - supporters, - workflow, - state, - item, - listOfSpeakers, - block, - attachments, - tags, - parent, - changeRecommendations, - amendments, - personalNote - ); + public constructor(motion: CreateMotion) { + super(motion); } public getVerboseName = () => { @@ -82,13 +38,6 @@ export class ViewCreateMotion extends ViewMotion { * Duplicate this motion into a copy of itself */ public copy(): ViewCreateMotion { - return new ViewCreateMotion( - this._model, - this._category, - this._submitters, - this._supporters, - this._workflow, - this._state - ); + return new ViewCreateMotion(this._model); } } diff --git a/client/src/app/site/motions/models/view-motion-amended-paragraph.ts b/client/src/app/site/motions/models/view-motion-amended-paragraph.ts index cd984b9ef..3d0125b24 100644 --- a/client/src/app/site/motions/models/view-motion-amended-paragraph.ts +++ b/client/src/app/site/motions/models/view-motion-amended-paragraph.ts @@ -1,7 +1,7 @@ import { ViewUnifiedChange, ViewUnifiedChangeType } from '../../../shared/models/motions/view-unified-change'; import { ViewMotion } from './view-motion'; import { LineRange } from 'app/core/ui-services/diff.service'; -import { MergeAmendment } from 'app/shared/models/motions/workflow-state'; +import { MergeAmendment } from 'app/shared/models/motions/state'; /** * This represents the Unified Diff part of an amendments. diff --git a/client/src/app/site/motions/models/view-motion-block.ts b/client/src/app/site/motions/models/view-motion-block.ts index aaac4e7fc..f66ca7642 100644 --- a/client/src/app/site/motions/models/view-motion-block.ts +++ b/client/src/app/site/motions/models/view-motion-block.ts @@ -2,9 +2,7 @@ import { MotionBlock } from 'app/shared/models/motions/motion-block'; import { SearchRepresentation } from 'app/core/ui-services/search.service'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { Searchable } from 'app/site/base/searchable'; -import { ViewItem } from 'app/site/agenda/models/view-item'; import { BaseViewModelWithAgendaItemAndListOfSpeakers } from 'app/site/base/base-view-model-with-agenda-item-and-list-of-speakers'; -import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers'; import { TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item'; export interface MotionBlockTitleInformation extends TitleInformationWithAgendaItem { @@ -31,8 +29,8 @@ export class ViewMotionBlock extends BaseViewModelWithAgendaItemAndListOfSpeaker return this.motionBlock.internal; } - public constructor(motionBlock: MotionBlock, agendaItem?: ViewItem, listOfSpeakers?: ViewListOfSpeakers) { - super(MotionBlock.COLLECTIONSTRING, motionBlock, agendaItem, listOfSpeakers); + public constructor(motionBlock: MotionBlock) { + super(MotionBlock.COLLECTIONSTRING, motionBlock); } /** diff --git a/client/src/app/site/motions/models/view-motion-change-recommendation.ts b/client/src/app/site/motions/models/view-motion-change-recommendation.ts index 078200c49..8bd43d60a 100644 --- a/client/src/app/site/motions/models/view-motion-change-recommendation.ts +++ b/client/src/app/site/motions/models/view-motion-change-recommendation.ts @@ -24,8 +24,6 @@ export class ViewMotionChangeRecommendation extends BaseViewModel submitter.user); + } + public get supporters(): ViewUser[] { return this._supporters || []; } @@ -104,7 +114,7 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers recommendation.recommendation_label !== undefined) - : null; + public get possibleRecommendations(): ViewState[] { + return this.workflow ? this.workflow.states.filter(state => state.recommendation_label !== undefined) : null; } public get origin(): string { return this.motion.origin; } - public get nextStates(): WorkflowState[] { - return this.state && this.workflow ? this.state.getNextStates(this.workflow.workflow) : []; - } - - public get previousStates(): WorkflowState[] { - return this.state && this.workflow ? this.state.getPreviousStates(this.workflow.workflow) : []; - } - public get agenda_type(): number { return this.agendaItem ? this.agendaItem.type : null; } @@ -327,8 +314,6 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers string; - public constructor( - motion: Motion, - category?: ViewCategory, - submitters?: ViewUser[], - supporters?: ViewUser[], - workflow?: ViewWorkflow, - state?: WorkflowState, - item?: ViewItem, - listOfSpeakers?: ViewListOfSpeakers, - block?: ViewMotionBlock, - attachments?: ViewMediafile[], - tags?: ViewTag[], - parent?: ViewMotion, - changeRecommendations?: ViewMotionChangeRecommendation[], - amendments?: ViewMotion[], - personalNote?: PersonalNoteContent - ) { - super(Motion.COLLECTIONSTRING, motion, item, listOfSpeakers); - this._category = category; - this._submitters = submitters; - this._supporters = supporters; - this._workflow = workflow; - this._state = state; - this._block = block; - this._attachments = attachments; - this._tags = tags; - this._parent = parent; - this._amendments = amendments; - this._changeRecommendations = changeRecommendations; - this.personalNote = personalNote; + public constructor(motion: Motion) { + super(Motion.COLLECTIONSTRING, motion); } /** @@ -397,7 +354,7 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers !!x)); } - searchValues = searchValues.concat(this.submitters.map(user => user.full_name)); + searchValues = searchValues.concat(this.submittersAsUsers.map(user => user.full_name)); searchValues = searchValues.concat(this.supporters.map(user => user.full_name)); searchValues = searchValues.concat(this.tags.map(tag => tag.getTitle())); searchValues = searchValues.concat(this.motion.comments.map(comment => comment.comment)); @@ -429,146 +386,6 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers comment.section_id === section.id); } - /** - * Updates the local objects if required - * - * @param update - */ - public updateDependencies(update: BaseViewModel): void { - super.updateDependencies(update); - if (update instanceof ViewWorkflow) { - this.updateWorkflow(update); - } else if (update instanceof ViewCategory) { - this.updateCategory(update); - } else if (update instanceof ViewMotionBlock) { - this.updateMotionBlock(update); - } else if (update instanceof ViewUser) { - this.updateUser(update); - } else if (update instanceof ViewMediafile) { - this.updateAttachments(update); - } else if (update instanceof ViewTag) { - this.updateTags(update); - } else if (update instanceof ViewMotion && update.id !== this.id) { - this.updateMotion(update); - } else if (update instanceof ViewMotionChangeRecommendation) { - this.updateChangeRecommendation(update); - } else if (update instanceof ViewPersonalNote) { - this.updatePersonalNote(update); - } - } - - /** - * Update routine for the workflow - * - * @param workflow potentially the (changed workflow (state). Needs manual verification - */ - private updateWorkflow(workflow: ViewWorkflow): void { - if (workflow.id === this.motion.workflow_id) { - this._workflow = workflow; - this._state = workflow.getStateById(this.state_id); - } - } - - /** - * Update routine for the category - * - * @param category potentially the changed category. Needs manual verification - */ - private updateCategory(category: ViewCategory): void { - if (this.category_id && category.id === this.motion.category_id) { - this._category = category; - } - } - - /** - * Update routine for the motion block - * - * @param block potentially the changed motion block. Needs manual verification - */ - private updateMotionBlock(block: ViewMotionBlock): void { - if (this.motion_block_id && block.id === this.motion.motion_block_id) { - this._block = block; - } - } - - /** - * Update routine for supporters and submitters - * - * @param update potentially the changed user. Needs manual verification - */ - private updateUser(update: ViewUser): void { - if (this.motion.submitters && this.motion.submitters.find(user => user.user_id === update.id)) { - const userIndex = this.motion.submitters.findIndex(submitter => submitter.user_id === update.id); - this.submitters[userIndex] = update; - } - if (this.motion.supporters_id && this.motion.supporters_id.includes(update.id)) { - const userIndex = this.supporters.findIndex(user => user.id === update.id); - if (userIndex < 0) { - this.supporters.push(update); - } else { - this.supporters[userIndex] = update; - } - } - } - - /** - * Update routine for attachments - * - * @param mediafile - */ - private updateAttachments(mediafile: ViewMediafile): void { - if (this.attachments_id && this.attachments_id.includes(mediafile.id)) { - const attachmentIndex = this.attachments.findIndex(_mediafile => _mediafile.id === mediafile.id); - if (attachmentIndex < 0) { - this.attachments.push(mediafile); - } else { - this.attachments[attachmentIndex] = mediafile; - } - } - } - - private updateTags(tag: ViewTag): void { - if (this.tags_id && this.tags_id.includes(tag.id)) { - const tagIndex = this.tags.findIndex(_tag => _tag.id === tag.id); - if (tagIndex < 0) { - this.tags.push(tag); - } else { - this.tags[tagIndex] = tag; - } - } - } - - /** - * The update cen be the parent or a child motion (=amendment). - */ - private updateMotion(update: ViewMotion): void { - if (this.parent_id && this.parent_id === update.id) { - this._parent = update; - } else if (update.parent_id && update.parent_id === this.id) { - const index = this._amendments.findIndex(m => m.id === update.id); - if (index >= 0) { - this._amendments[index] = update; - } else { - this._amendments.push(update); - } - } - } - - private updateChangeRecommendation(cr: ViewMotionChangeRecommendation): void { - if (cr.motion_id === this.id) { - const index = this.changeRecommendations.findIndex(_cr => _cr.id === cr.id); - if (index < 0) { - this.changeRecommendations.push(cr); - } else { - this.changeRecommendations[index] = cr; - } - } - } - - private updatePersonalNote(personalNote: ViewPersonalNote): void { - this.personalNote = personalNote.getNoteContent(this.collectionString, this.id); - } - public hasSupporters(): boolean { return !!(this.supporters && this.supporters.length > 0); } @@ -589,7 +406,7 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers implements StateTitleInformation { + public static COLLECTIONSTRING = State.COLLECTIONSTRING; + + private _next_states?: ViewState[]; + public _workflow?: ViewWorkflow; + + public get state(): State { + return this._model; + } + + public get workflow(): ViewWorkflow | null { + return this._workflow; + } + + public get next_states(): ViewState[] { + return this._next_states || []; + } + + public get name(): string { + return this.state.name; + } + + public get recommendation_label(): string { + return this.state.recommendation_label; + } + + public get css_class(): string { + return this.state.css_class; + } + + public get restriction(): string[] { + return this.state.restriction; + } + + public get allow_support(): boolean { + return this.state.allow_support; + } + + public get allow_create_poll(): boolean { + return this.state.allow_create_poll; + } + + public get allow_submitter_edit(): boolean { + return this.state.allow_submitter_edit; + } + + public get dont_set_identifier(): boolean { + return this.state.dont_set_identifier; + } + + public get show_state_extension_field(): boolean { + return this.state.show_state_extension_field; + } + + public get merge_amendment_into_final(): MergeAmendment { + return this.state.merge_amendment_into_final; + } + + public get show_recommendation_extension_field(): boolean { + return this.state.show_recommendation_extension_field; + } + + public get next_states_id(): number[] { + return this.state.next_states_id; + } + + public get workflow_id(): number { + return this.state.workflow_id; + } + + public get isFinalState(): boolean { + return !this.next_states_id || this.next_states_id.length === 0; + } + + public get previous_states(): ViewState[] { + if (!this.workflow) { + return []; + } + return this.workflow.states.filter(state => { + return state.next_states_id.includes(this.id); + }); + } + + public constructor(state: State) { + super(State.COLLECTIONSTRING, state); + } +} diff --git a/client/src/app/site/motions/models/view-statute-paragraph.ts b/client/src/app/site/motions/models/view-statute-paragraph.ts index d6cec7f6b..43f039107 100644 --- a/client/src/app/site/motions/models/view-statute-paragraph.ts +++ b/client/src/app/site/motions/models/view-statute-paragraph.ts @@ -45,10 +45,4 @@ export class ViewStatuteParagraph extends BaseViewModel public getDetailStateURL(): string { return '/motions/statute-paragraphs'; } - - /** - * Updates the local objects if required - * @param section - */ - public updateDependencies(update: BaseViewModel): void {} } diff --git a/client/src/app/site/motions/models/view-submitter.ts b/client/src/app/site/motions/models/view-submitter.ts new file mode 100644 index 000000000..564319dee --- /dev/null +++ b/client/src/app/site/motions/models/view-submitter.ts @@ -0,0 +1,44 @@ +import { ViewUser } from 'app/site/users/models/view-user'; +import { Submitter } from 'app/shared/models/motions/submitter'; +import { BaseViewModel } from 'app/site/base/base-view-model'; + +export class ViewSubmitter extends BaseViewModel { + public static COLLECTIONSTRING = Submitter.COLLECTIONSTRING; + private _user?: ViewUser; + + public get submitter(): Submitter { + return this._model; + } + + public get user(): ViewUser { + return this._user; + } + + public get id(): number { + return this.submitter.id; + } + + public get user_id(): number { + return this.submitter.user_id; + } + + public get motion_id(): number { + return this.submitter.motion_id; + } + + public get weight(): number { + return this.submitter.weight; + } + + public constructor(submitter: Submitter) { + super(Submitter.COLLECTIONSTRING, submitter); + } + + public getTitle = () => { + return this.user ? this.user.getTitle() : ''; + }; + + public getListTitle = () => { + return this.getTitle(); + }; +} diff --git a/client/src/app/site/motions/models/view-workflow.ts b/client/src/app/site/motions/models/view-workflow.ts index fe11ec202..ba2b4af1c 100644 --- a/client/src/app/site/motions/models/view-workflow.ts +++ b/client/src/app/site/motions/models/view-workflow.ts @@ -1,6 +1,6 @@ import { Workflow } from 'app/shared/models/motions/workflow'; -import { WorkflowState } from 'app/shared/models/motions/workflow-state'; import { BaseViewModel } from '../../base/base-view-model'; +import { ViewState } from './view-state'; export interface WorkflowTitleInformation { name: string; @@ -13,6 +13,9 @@ export interface WorkflowTitleInformation { export class ViewWorkflow extends BaseViewModel implements WorkflowTitleInformation { public static COLLECTIONSTRING = Workflow.COLLECTIONSTRING; + private _states?: ViewState[]; + private _first_state?: ViewState; + public get workflow(): Workflow { return this._model; } @@ -21,34 +24,23 @@ export class ViewWorkflow extends BaseViewModel implements WorkflowTit return this.workflow.name; } - public get states(): WorkflowState[] { - return this.workflow.states; + public get states(): ViewState[] { + return this._states || []; + } + + public get states_id(): number[] { + return this.workflow.states_id; } public get first_state_id(): number { return this.workflow.first_state_id; } - public get firstState(): WorkflowState { - return this.getStateById(this.first_state_id); + public get first_state(): ViewState | null { + return this._first_state; } public constructor(workflow: Workflow) { super(Workflow.COLLECTIONSTRING, workflow); } - - public sortStates(): void { - this.workflow.sortStates(); - } - - /** - * Updates the local objects if required - * - * @param update - */ - public updateDependencies(update: BaseViewModel): void {} - - public getStateById(id: number): WorkflowState { - return this.states.find(state => id === state.id); - } } diff --git a/client/src/app/site/motions/modules/motion-detail/components/manage-submitters/manage-submitters.component.html b/client/src/app/site/motions/modules/motion-detail/components/manage-submitters/manage-submitters.component.html index 65556f0e5..b181f2c84 100644 --- a/client/src/app/site/motions/modules/motion-detail/components/manage-submitters/manage-submitters.component.html +++ b/client/src/app/site/motions/modules/motion-detail/components/manage-submitters/manage-submitters.component.html @@ -13,8 +13,8 @@
- - {{ submitter.full_name }} + + {{ user.getTitle() }}
diff --git a/client/src/app/site/motions/modules/motion-detail/components/manage-submitters/manage-submitters.component.ts b/client/src/app/site/motions/modules/motion-detail/components/manage-submitters/manage-submitters.component.ts index 1a31b4bd8..e5f9bc190 100644 --- a/client/src/app/site/motions/modules/motion-detail/components/manage-submitters/manage-submitters.component.ts +++ b/client/src/app/site/motions/modules/motion-detail/components/manage-submitters/manage-submitters.component.ts @@ -93,7 +93,7 @@ export class ManageSubmittersComponent extends BaseViewComponent { */ public onEdit(): void { this.isEditMode = true; - this.editSubmitterSubject.next(this.motion.submitters.map(x => x)); + this.editSubmitterSubject.next(this.motion.submittersAsUsers); this.addSubmitterForm.reset(); // get all users for the submitter add form diff --git a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.html b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.html index c202674ab..796e41c5b 100644 --- a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.html +++ b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.html @@ -261,13 +261,13 @@ extensionLabel="{{ 'Extension' | translate }}" (success)="setStateExtension($event)" > - -
- - diff --git a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts index 1e0c651f2..e530da213 100644 --- a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts +++ b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts @@ -47,10 +47,7 @@ import { LineNumberingMode, verboseChangeRecoMode } from 'app/site/motions/models/view-motion'; -import { - ViewMotionNotificationEditMotion, - TypeOfNotificationViewMotion -} from 'app/site/motions/models/view-motion-notify'; +import { MotionEditNotification, MotionEditNotificationType } from 'app/site/motions/motion-edit-notification'; import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block'; import { ViewCategory } from 'app/site/motions/models/view-category'; import { ViewCreateMotion } from 'app/site/motions/models/view-create-motion'; @@ -517,7 +514,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, * Sends a notification to user editors of the motion was edited */ public ngOnDestroy(): void { - this.unsubscribeEditNotifications(TypeOfNotificationViewMotion.TYPE_CLOSING_EDITING_MOTION); + this.unsubscribeEditNotifications(MotionEditNotificationType.TYPE_CLOSING_EDITING_MOTION); if (this.navigationSubscription) { this.navigationSubscription.unsubscribe(); } @@ -832,7 +829,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, } else { this.updateMotionFromForm(); // When saving the changes, notify other users if they edit the same motion. - this.unsubscribeEditNotifications(TypeOfNotificationViewMotion.TYPE_SAVING_EDITING_MOTION); + this.unsubscribeEditNotifications(MotionEditNotificationType.TYPE_SAVING_EDITING_MOTION); } } @@ -1165,7 +1162,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, this.motionCopy = this.motion.copy(); this.patchForm(this.motionCopy); this.editNotificationSubscription = this.listenToEditNotification(); - this.sendEditNotification(TypeOfNotificationViewMotion.TYPE_BEGIN_EDITING_MOTION); + this.sendEditNotification(MotionEditNotificationType.TYPE_BEGIN_EDITING_MOTION); } if (!mode && this.newMotion) { this.router.navigate(['./motions/']); @@ -1173,7 +1170,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, // If the user cancels the work on this motion, // notify the users who are still editing the same motion if (!mode && !this.newMotion) { - this.unsubscribeEditNotifications(TypeOfNotificationViewMotion.TYPE_CLOSING_EDITING_MOTION); + this.unsubscribeEditNotifications(MotionEditNotificationType.TYPE_CLOSING_EDITING_MOTION); } } @@ -1412,8 +1409,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, * @param type TypeOfNotificationViewMotion defines the type of the notification which is sent. * @param user Optional userId. If set the function will send a notification to the given userId. */ - private sendEditNotification(type: TypeOfNotificationViewMotion, user?: number): void { - const content: ViewMotionNotificationEditMotion = { + private sendEditNotification(type: MotionEditNotificationType, user?: number): void { + const content: MotionEditNotification = { motionId: this.motion.id, senderId: this.operator.viewUser.id, senderName: this.operator.viewUser.short_name, @@ -1422,7 +1419,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, if (user) { this.notifyService.sendToUsers(this.NOTIFICATION_EDIT_MOTION, content, user); } else { - this.notifyService.sendToAllUsers(this.NOTIFICATION_EDIT_MOTION, content); + this.notifyService.sendToAllUsers(this.NOTIFICATION_EDIT_MOTION, content); } } @@ -1434,13 +1431,13 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, */ private listenToEditNotification(): Subscription { return this.notifyService.getMessageObservable(this.NOTIFICATION_EDIT_MOTION).subscribe(message => { - const content = message.content; + const content = message.content; if (this.operator.viewUser.id !== content.senderId && content.motionId === this.motion.id) { let warning = ''; switch (content.type) { - case TypeOfNotificationViewMotion.TYPE_BEGIN_EDITING_MOTION: - case TypeOfNotificationViewMotion.TYPE_ALSO_EDITING_MOTION: { + case MotionEditNotificationType.TYPE_BEGIN_EDITING_MOTION: + case MotionEditNotificationType.TYPE_ALSO_EDITING_MOTION: { if (!this.otherWorkOnMotion.includes(content.senderName)) { this.otherWorkOnMotion.push(content.senderName); } @@ -1448,19 +1445,19 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, warning = `${this.translate.instant('Following users are currently editing this motion:')} ${ this.otherWorkOnMotion }`; - if (content.type === TypeOfNotificationViewMotion.TYPE_BEGIN_EDITING_MOTION) { + if (content.type === MotionEditNotificationType.TYPE_BEGIN_EDITING_MOTION) { this.sendEditNotification( - TypeOfNotificationViewMotion.TYPE_ALSO_EDITING_MOTION, + MotionEditNotificationType.TYPE_ALSO_EDITING_MOTION, message.senderUserId ); } break; } - case TypeOfNotificationViewMotion.TYPE_CLOSING_EDITING_MOTION: { + case MotionEditNotificationType.TYPE_CLOSING_EDITING_MOTION: { this.recognizeOtherWorkerOnMotion(content.senderName); break; } - case TypeOfNotificationViewMotion.TYPE_SAVING_EDITING_MOTION: { + case MotionEditNotificationType.TYPE_SAVING_EDITING_MOTION: { warning = `${content.senderName} ${this.translate.instant( 'has saved his work on this motion.' )}`; @@ -1496,7 +1493,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, * * @param unsubscriptionReason The reason for the unsubscription. */ - private unsubscribeEditNotifications(unsubscriptionReason: TypeOfNotificationViewMotion): void { + private unsubscribeEditNotifications(unsubscriptionReason: MotionEditNotificationType): void { if (!!this.editNotificationSubscription && !this.editNotificationSubscription.closed) { this.sendEditNotification(unsubscriptionReason); this.closeSnackBar(); diff --git a/client/src/app/site/motions/modules/motion-workflow/components/workflow-detail/workflow-detail.component.html b/client/src/app/site/motions/modules/motion-workflow/components/workflow-detail/workflow-detail.component.html index b28e2b3d1..f51c85561 100644 --- a/client/src/app/site/motions/modules/motion-workflow/components/workflow-detail/workflow-detail.component.html +++ b/client/src/app/site/motions/modules/motion-workflow/components/workflow-detail/workflow-detail.component.html @@ -19,7 +19,7 @@
First state: - {{ workflow.firstState.name | translate }} + {{ workflow.first_state.name | translate }}
@@ -70,20 +70,13 @@ {{ state[perm.selector] | translate }}
-
+
-
+
-
-
-
+
+
{{ nextstate.name | translate }}
@@ -92,11 +85,9 @@ class="clickable-cell stretch-to-fill-parent" [matMenuTriggerFor]="nextStatesMenu" [matMenuTriggerData]="{ state: state }" - >
+ >
-
+
{{ getMergeAmendmentLabel(state.merge_amendment_into_final) | translate }}
@@ -106,9 +97,7 @@ [matMenuTriggerData]="{ state: state }" >
-
+
- diff --git a/client/src/app/site/motions/modules/motion-workflow/components/workflow-detail/workflow-detail.component.ts b/client/src/app/site/motions/modules/motion-workflow/components/workflow-detail/workflow-detail.component.ts index 7e7bc3ab4..eb6d862ae 100644 --- a/client/src/app/site/motions/modules/motion-workflow/components/workflow-detail/workflow-detail.component.ts +++ b/client/src/app/site/motions/modules/motion-workflow/components/workflow-detail/workflow-detail.component.ts @@ -12,8 +12,10 @@ import { TranslateService } from '@ngx-translate/core'; import { BaseViewComponent } from 'app/site/base/base-view'; import { ViewWorkflow } from 'app/site/motions/models/view-workflow'; import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service'; -import { WorkflowState, MergeAmendment } from 'app/shared/models/motions/workflow-state'; import { PromptService } from 'app/core/ui-services/prompt.service'; +import { MergeAmendment, State } from 'app/shared/models/motions/state'; +import { ViewState } from 'app/site/motions/models/view-state'; +import { StateRepositoryService } from 'app/core/repositories/motions/state-repository.service'; /** * Declares data for the workflow dialog @@ -153,6 +155,7 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit private promtService: PromptService, private dialog: MatDialog, private workflowRepo: WorkflowRepositoryService, + private stateRepo: StateRepositoryService, private route: ActivatedRoute ) { super(title, translate, matSnackBar); @@ -180,17 +183,17 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit * * @param state the selected workflow state */ - public onClickStateName(state: WorkflowState): void { + public onClickStateName(state: ViewState): void { this.openEditDialog(state.name, 'Rename state', '', true).subscribe(result => { if (result) { if (result.action === 'update') { - this.workflowRepo.updateState({ name: result.value }, state).then(() => {}, this.raiseError); + this.stateRepo.update({ name: result.value }, state).then(() => {}, this.raiseError); } else if (result.action === 'delete') { const content = this.translate.instant('Delete') + ` ${state.name}?`; this.promtService.open('Are you sure', content).then(promptResult => { if (promptResult) { - this.workflowRepo.deleteState(state).then(() => {}, this.raiseError); + this.stateRepo.delete(state).then(() => {}, this.raiseError); } }); } @@ -206,7 +209,11 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit this.openEditDialog('', this.translate.instant('New state'), this.translate.instant('Name')).subscribe( result => { if (result && result.action === 'update') { - this.workflowRepo.addState(result.value, this.workflow).then(() => {}, this.raiseError); + const state = new State({ + name: result.value, + workflow_id: this.workflow.id + }); + this.stateRepo.create(state).then(() => {}, this.raiseError); } } ); @@ -231,10 +238,10 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit * @param perm The permission * @param state The selected workflow state */ - public onClickInputPerm(perm: StatePerm, state: WorkflowState): void { + public onClickInputPerm(perm: StatePerm, state: ViewState): void { this.openEditDialog(state[perm.selector], 'Edit', perm.name).subscribe(result => { if (result && result.action === 'update') { - this.workflowRepo.updateState({ [perm.selector]: result.value }, state).then(() => {}, this.raiseError); + this.stateRepo.update({ [perm.selector]: result.value }, state).then(() => {}, this.raiseError); } }); } @@ -246,8 +253,8 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit * @param perm The states permission that was changed * @param event The change event. */ - public onToggleStatePerm(state: WorkflowState, perm: string, event: MatCheckboxChange): void { - this.workflowRepo.updateState({ [perm]: event.checked }, state).then(() => {}, this.raiseError); + public onToggleStatePerm(state: ViewState, perm: string, event: MatCheckboxChange): void { + this.stateRepo.update({ [perm]: event.checked }, state).then(() => {}, this.raiseError); } /** @@ -257,8 +264,8 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit * @param state The selected workflow state * @param color The selected color */ - public onSelectColor(state: WorkflowState, color: string): void { - this.workflowRepo.updateState({ css_class: color }, state).then(() => {}, this.raiseError); + public onSelectColor(state: ViewState, color: string): void { + this.stateRepo.update({ css_class: color }, state).then(() => {}, this.raiseError); } /** @@ -267,7 +274,7 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit * @param nextState the potential next workflow state * @param state the state to add or remove another state to */ - public onSetNextState(nextState: WorkflowState, state: WorkflowState): void { + public onSetNextState(nextState: ViewState, state: ViewState): void { const ids = state.next_states_id.map(id => id); const stateIdIndex = ids.findIndex(id => id === nextState.id); @@ -276,7 +283,7 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit } else { ids.splice(stateIdIndex, 1); } - this.workflowRepo.updateState({ next_states_id: ids }, state).then(() => {}, this.raiseError); + this.stateRepo.update({ next_states_id: ids }, state).then(() => {}, this.raiseError); } /** @@ -285,7 +292,7 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit * @param restrictions The new restrictions * @param state the state to change */ - public onSetRestriction(restriction: string, state: WorkflowState): void { + public onSetRestriction(restriction: string, state: ViewState): void { const restrictions = state.restriction.map(r => r); const restrictionIndex = restrictions.findIndex(r => r === restriction); @@ -294,7 +301,7 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit } else { restrictions.splice(restrictionIndex, 1); } - this.workflowRepo.updateState({ restriction: restrictions }, state).then(() => {}, this.raiseError); + this.stateRepo.update({ restriction: restrictions }, state).then(() => {}, this.raiseError); } /** @@ -311,8 +318,8 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit * @param amendment determines the amendment * @param state the state to change */ - public setMergeAmendment(amendment: number, state: WorkflowState): void { - this.workflowRepo.updateState({ merge_amendment_into_final: amendment }, state).then(() => {}, this.raiseError); + public setMergeAmendment(amendment: number, state: ViewState): void { + this.stateRepo.update({ merge_amendment_into_final: amendment }, state).then(() => {}, this.raiseError); } /** @@ -383,7 +390,7 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit * @param state the workflow state * @returns a unique definition */ - public getColumnDef(state: WorkflowState): string { + public getColumnDef(state: ViewState): string { return `${state.name}${state.id}`; } diff --git a/client/src/app/site/motions/modules/statute-paragraph/components/statute-paragraph-list/statute-paragraph-list.component.html b/client/src/app/site/motions/modules/statute-paragraph/components/statute-paragraph-list/statute-paragraph-list.component.html index a59f0d197..bdbbcf1e1 100644 --- a/client/src/app/site/motions/modules/statute-paragraph/components/statute-paragraph-list/statute-paragraph-list.component.html +++ b/client/src/app/site/motions/modules/statute-paragraph/components/statute-paragraph-list/statute-paragraph-list.component.html @@ -26,7 +26,7 @@

-

Statute paragraph

+

Statute paragraph

@@ -67,7 +67,7 @@

-

Statute paragraph

+

Statute paragraph

diff --git a/client/src/app/site/motions/models/view-motion-notify.ts b/client/src/app/site/motions/motion-edit-notification.ts similarity index 89% rename from client/src/app/site/motions/models/view-motion-notify.ts rename to client/src/app/site/motions/motion-edit-notification.ts index 01ce196be..da40b3a50 100644 --- a/client/src/app/site/motions/models/view-motion-notify.ts +++ b/client/src/app/site/motions/motion-edit-notification.ts @@ -1,7 +1,7 @@ /** * Enum to define different types of notifications. */ -export enum TypeOfNotificationViewMotion { +export enum MotionEditNotificationType { /** * Type to declare editing a motion. */ @@ -25,7 +25,7 @@ export enum TypeOfNotificationViewMotion { /** * Class to specify the notifications for editing a motion. */ -export interface ViewMotionNotificationEditMotion { +export interface MotionEditNotification { /** * The id of the motion the user wants to edit. * Necessary to identify if users edit the same motion. @@ -48,5 +48,5 @@ export interface ViewMotionNotificationEditMotion { * The type of the notification. * Separates if the user is beginning the work or closing the edit-view. */ - type: TypeOfNotificationViewMotion; + type: MotionEditNotificationType; } diff --git a/client/src/app/site/motions/motions.config.ts b/client/src/app/site/motions/motions.config.ts index f6587c840..bd7e2b537 100644 --- a/client/src/app/site/motions/motions.config.ts +++ b/client/src/app/site/motions/motions.config.ts @@ -20,6 +20,9 @@ import { ViewMotionBlock } from './models/view-motion-block'; import { ViewStatuteParagraph } from './models/view-statute-paragraph'; import { ViewMotion } from './models/view-motion'; import { ViewWorkflow } from './models/view-workflow'; +import { State } from 'app/shared/models/motions/state'; +import { ViewState } from './models/view-state'; +import { StateRepositoryService } from 'app/core/repositories/motions/state-repository.service'; export const MotionsAppConfig: AppConfig = { name: 'motions', @@ -44,6 +47,12 @@ export const MotionsAppConfig: AppConfig = { viewModel: ViewWorkflow, repository: WorkflowRepositoryService }, + { + collectionString: 'motions/state', + model: State, + viewModel: ViewState, + repository: StateRepositoryService + }, { collectionString: 'motions/motion-comment-section', model: MotionCommentSection, diff --git a/client/src/app/site/motions/services/local-permissions.service.ts b/client/src/app/site/motions/services/local-permissions.service.ts index 341ec5620..da2f36033 100644 --- a/client/src/app/site/motions/services/local-permissions.service.ts +++ b/client/src/app/site/motions/services/local-permissions.service.ts @@ -70,9 +70,9 @@ export class LocalPermissionsService { motion.state && motion.state.allow_support && motion.submitters && - motion.submitters.indexOf(this.operator.viewUser) === -1 && + !motion.submittersAsUsers.includes(this.operator.viewUser) && motion.supporters && - motion.supporters.indexOf(this.operator.viewUser) === -1 + !motion.supporters.includes(this.operator.viewUser) ); } case 'unsupport': { 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 76d1cbd7d..6e09fc597 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 @@ -152,7 +152,7 @@ export class MotionCsvExportService { [ { label: 'Called', map: motion => (motion.sort_parent_id ? '' : motion.identifierOrTitle) }, { label: 'Called with', map: motion => (!motion.sort_parent_id ? '' : motion.identifierOrTitle) }, - { label: 'submitters', map: motion => motion.submitters.map(s => s.short_name).join(',') }, + { label: 'submitters', map: motion => motion.submittersAsUsers.map(s => s.short_name).join(',') }, { property: 'title' }, { label: 'recommendation', 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 79a201649..dfd9a42a9 100644 --- a/client/src/app/site/motions/services/motion-import.service.ts +++ b/client/src/app/site/motions/services/motion-import.service.ts @@ -125,12 +125,12 @@ export class MotionImportService extends BaseImportService { newEntry.motion[this.expectedHeader[idx]] = line[idx]; } } - const updateModels = this.repo.getMotionDuplicates(newEntry); + const hasDuplicates = this.repo.getViewModelList().some(motion => motion.identifier === newEntry.identifier); const entry: NewEntry = { newEntry: newEntry, - duplicates: updateModels, - status: updateModels.length ? 'error' : 'new', - errors: updateModels.length ? ['Duplicates'] : [] + hasDuplicates: hasDuplicates, + status: hasDuplicates ? 'error' : 'new', + errors: hasDuplicates ? ['Duplicates'] : [] }; if (!entry.newEntry.title) { this.setError(entry, 'Title'); diff --git a/client/src/app/site/motions/services/motion-pdf.service.ts b/client/src/app/site/motions/services/motion-pdf.service.ts index ba547970b..bd4e0ad47 100644 --- a/client/src/app/site/motions/services/motion-pdf.service.ts +++ b/client/src/app/site/motions/services/motion-pdf.service.ts @@ -237,9 +237,9 @@ export class MotionPdfService { // submitters if (!infoToExport || infoToExport.includes('submitters')) { - const submitters = motion.submitters - .map(submitter => { - return submitter.full_name; + const submitters = motion.submittersAsUsers + .map(user => { + return user.full_name; }) .join(', '); @@ -793,7 +793,7 @@ export class MotionPdfService { text: motion.sort_parent_id ? '' : motion.identifierOrTitle }, { text: motion.sort_parent_id ? motion.identifierOrTitle : '' }, - { text: motion.submitters.length ? motion.submitters.map(s => s.short_name).join(', ') : '' }, + { text: motion.submitters.length ? motion.submittersAsUsers.map(s => s.short_name).join(', ') : '' }, { text: motion.title }, { text: motion.recommendation ? this.motionRepo.getExtendedRecommendationLabel(motion) : '' 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 df9bfcd5f..a6d2f0943 100644 --- a/client/src/app/site/motions/services/statute-import.service.ts +++ b/client/src/app/site/motions/services/statute-import.service.ts @@ -72,12 +72,12 @@ export class StatuteImportService extends BaseImportService paragraph.title === newEntry.title); + const hasDuplicates = this.repo.getViewModelList().some(paragraph => paragraph.title === newEntry.title); return { newEntry: newEntry, - duplicates: updateModels, - status: updateModels.length ? 'error' : 'new', - errors: updateModels.length ? ['Duplicates'] : [] + hasDuplicates: hasDuplicates, + status: hasDuplicates ? 'error' : 'new', + errors: hasDuplicates ? ['Duplicates'] : [] }; } diff --git a/client/src/app/site/projector/models/view-countdown.ts b/client/src/app/site/projector/models/view-countdown.ts index 5eccbf480..9da361c38 100644 --- a/client/src/app/site/projector/models/view-countdown.ts +++ b/client/src/app/site/projector/models/view-countdown.ts @@ -1,7 +1,6 @@ import { Countdown } from 'app/shared/models/core/countdown'; import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; -import { BaseViewModel } from 'app/site/base/base-view-model'; export interface CountdownTitleInformation { title: string; @@ -39,8 +38,6 @@ export class ViewCountdown extends BaseProjectableViewModel implement super(Countdown.COLLECTIONSTRING, countdown); } - public updateDependencies(update: BaseViewModel): void {} - public getSlide(): ProjectorElementBuildDeskriptor { return { getBasicProjectorElement: options => ({ diff --git a/client/src/app/site/projector/models/view-projection-default.ts b/client/src/app/site/projector/models/view-projection-default.ts index c4fbf4ac4..254f013e4 100644 --- a/client/src/app/site/projector/models/view-projection-default.ts +++ b/client/src/app/site/projector/models/view-projection-default.ts @@ -28,6 +28,4 @@ export class ViewProjectionDefault extends BaseViewModel public constructor(projectionDefault: ProjectionDefault) { super(ProjectionDefault.COLLECTIONSTRING, projectionDefault); } - - public updateDependencies(update: BaseViewModel): void {} } diff --git a/client/src/app/site/projector/models/view-projector-message.ts b/client/src/app/site/projector/models/view-projector-message.ts index 0b74fcb4f..0ba351cfe 100644 --- a/client/src/app/site/projector/models/view-projector-message.ts +++ b/client/src/app/site/projector/models/view-projector-message.ts @@ -1,7 +1,6 @@ import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { ProjectorMessage } from 'app/shared/models/core/projector-message'; -import { BaseViewModel } from 'app/site/base/base-view-model'; import { stripHtmlTags } from 'app/shared/utils/strip-html-tags'; export type ProjectorMessageTitleInformation = object; @@ -22,8 +21,6 @@ export class ViewProjectorMessage extends BaseProjectableViewModel ({ diff --git a/client/src/app/site/projector/models/view-projector.ts b/client/src/app/site/projector/models/view-projector.ts index f3ef6cdba..219e6b6f8 100644 --- a/client/src/app/site/projector/models/view-projector.ts +++ b/client/src/app/site/projector/models/view-projector.ts @@ -106,14 +106,7 @@ export class ViewProjector extends BaseViewModel { return this.projector.show_logo; } - public constructor(projector: Projector, referenceProjector?: ViewProjector) { + public constructor(projector: Projector) { super(Projector.COLLECTIONSTRING, projector); - this._referenceProjector = referenceProjector; - } - - public updateDependencies(update: BaseViewModel): void { - if (update instanceof ViewProjector && this.reference_projector_id === update.id) { - this._referenceProjector = update; - } } } diff --git a/client/src/app/site/tags/models/view-tag.ts b/client/src/app/site/tags/models/view-tag.ts index 8c9add1ea..c91521078 100644 --- a/client/src/app/site/tags/models/view-tag.ts +++ b/client/src/app/site/tags/models/view-tag.ts @@ -36,10 +36,4 @@ export class ViewTag extends BaseViewModel implements TagTitleInformation, public getDetailStateURL(): string { return `/tags`; } - - /** - * Updates the local objects if required - * @param update - */ - public updateDependencies(update: BaseViewModel): void {} } diff --git a/client/src/app/site/topics/models/view-topic.ts b/client/src/app/site/topics/models/view-topic.ts index 751eca253..e469eb35b 100644 --- a/client/src/app/site/topics/models/view-topic.ts +++ b/client/src/app/site/topics/models/view-topic.ts @@ -2,9 +2,6 @@ import { Topic } from 'app/shared/models/topics/topic'; import { SearchRepresentation } from 'app/core/ui-services/search.service'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; -import { ViewItem } from '../../agenda/models/view-item'; -import { BaseViewModel } from 'app/site/base/base-view-model'; -import { ViewListOfSpeakers } from '../../agenda/models/view-list-of-speakers'; import { BaseViewModelWithAgendaItemAndListOfSpeakers } from 'app/site/base/base-view-model-with-agenda-item-and-list-of-speakers'; import { TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item'; @@ -42,14 +39,8 @@ export class ViewTopic extends BaseViewModelWithAgendaItemAndListOfSpeakers impl return this.topic.text; } - public constructor( - topic: Topic, - attachments?: ViewMediafile[], - item?: ViewItem, - listOfSpeakers?: ViewListOfSpeakers - ) { - super(Topic.COLLECTIONSTRING, topic, item, listOfSpeakers); - this._attachments = attachments; + public constructor(topic: Topic) { + super(Topic.COLLECTIONSTRING, topic); } /** @@ -89,16 +80,4 @@ export class ViewTopic extends BaseViewModelWithAgendaItemAndListOfSpeakers impl public hasAttachments(): boolean { return this.attachments && this.attachments.length > 0; } - - public updateDependencies(update: BaseViewModel): void { - super.updateDependencies(update); - if (update instanceof ViewMediafile && this.attachments_id.includes(update.id)) { - const attachmentIndex = this.attachments.findIndex(mediafile => mediafile.id === update.id); - if (attachmentIndex < 0) { - this.attachments.push(update); - } else { - this.attachments[attachmentIndex] = update; - } - } - } } diff --git a/client/src/app/site/users/models/view-group.ts b/client/src/app/site/users/models/view-group.ts index 9e93a189b..7bab5d7ad 100644 --- a/client/src/app/site/users/models/view-group.ts +++ b/client/src/app/site/users/models/view-group.ts @@ -36,6 +36,4 @@ export class ViewGroup extends BaseViewModel implements GroupTitleInforma public hasPermission(perm: string): boolean { return this.permissions.includes(perm); } - - public updateDependencies(update: BaseViewModel): void {} } diff --git a/client/src/app/site/users/models/view-personal-note.ts b/client/src/app/site/users/models/view-personal-note.ts index 798047320..37b03da0b 100644 --- a/client/src/app/site/users/models/view-personal-note.ts +++ b/client/src/app/site/users/models/view-personal-note.ts @@ -29,6 +29,4 @@ export class ViewPersonalNote extends BaseViewModel implements Per return null; } } - - public updateDependencies(update: BaseViewModel): void {} } diff --git a/client/src/app/site/users/models/view-user.ts b/client/src/app/site/users/models/view-user.ts index 488a5f248..a7ad0f5fd 100644 --- a/client/src/app/site/users/models/view-user.ts +++ b/client/src/app/site/users/models/view-user.ts @@ -4,7 +4,6 @@ import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { Searchable } from 'app/site/base/searchable'; import { SearchRepresentation } from 'app/core/ui-services/search.service'; import { ViewGroup } from './view-group'; -import { BaseViewModel } from 'app/site/base/base-view-model'; export interface UserTitleInformation { username: string; @@ -121,9 +120,8 @@ export class ViewUser extends BaseProjectableViewModel implements UserTitl public getFullName: () => string; public getShortName: () => string; - public constructor(user: User, groups?: ViewGroup[]) { + public constructor(user: User) { super(User.COLLECTIONSTRING, user); - this._groups = groups; } /** @@ -151,15 +149,4 @@ export class ViewUser extends BaseProjectableViewModel implements UserTitl getDialogTitle: () => this.getTitle() }; } - - public updateDependencies(update: BaseViewModel): void { - if (update instanceof ViewGroup && this.user.groups_id.includes(update.id)) { - const groupIndex = this.groups.findIndex(group => group.id === update.id); - if (groupIndex < 0) { - this.groups.push(update); - } else { - this.groups[groupIndex] = update; - } - } - } } 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 491a67174..79e2b542d 100644 --- a/client/src/app/site/users/services/user-import.service.ts +++ b/client/src/app/site/users/services/user-import.service.ts @@ -269,14 +269,13 @@ export class UserImportService extends BaseImportService { private userToEntry(newUser: ViewCsvCreateUser): NewEntry { const newEntry: NewEntry = { newEntry: newUser, - duplicates: [], + hasDuplicates: false, status: 'new', errors: [] }; if (newUser.isValid) { - const updateModels = this.repo.getUserDuplicates(newUser); - if (updateModels.length) { - newEntry.duplicates = updateModels; + newEntry.hasDuplicates = this.repo.getViewModelList().some(user => user.full_name === newUser.full_name); + if (newEntry.hasDuplicates) { this.setError(newEntry, 'Duplicates'); } } else { diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index fc27c0103..873f5c32b 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -499,7 +499,7 @@ class ListOfSpeakersViewSet( if not isinstance(speaker_id, int) or speakers.get(speaker_id) is None: raise ValidationError({"detail": "Invalid data."}) valid_speakers.append(speakers[speaker_id]) - weight = 0 + weight = 1 with transaction.atomic(): for speaker in valid_speakers: speaker.weight = weight diff --git a/openslides/motions/access_permissions.py b/openslides/motions/access_permissions.py index f718d2eff..c76711fd1 100644 --- a/openslides/motions/access_permissions.py +++ b/openslides/motions/access_permissions.py @@ -171,3 +171,11 @@ class WorkflowAccessPermissions(BaseAccessPermissions): """ base_permission = "motions.can_see" + + +class StateAccessPermissions(BaseAccessPermissions): + """ + Access permissions container for State and StateViewSet. + """ + + base_permission = "motions.can_see" diff --git a/openslides/motions/apps.py b/openslides/motions/apps.py index 59d9a9d7d..73e9319dd 100644 --- a/openslides/motions/apps.py +++ b/openslides/motions/apps.py @@ -89,6 +89,7 @@ class MotionsAppConfig(AppConfig): "Motion", "MotionBlock", "Workflow", + "State", "MotionChangeRecommendation", "MotionCommentSection", ): diff --git a/openslides/motions/models.py b/openslides/motions/models.py index f40b4ee6d..9c181afe5 100644 --- a/openslides/motions/models.py +++ b/openslides/motions/models.py @@ -26,6 +26,7 @@ from .access_permissions import ( MotionBlockAccessPermissions, MotionChangeRecommendationAccessPermissions, MotionCommentSectionAccessPermissions, + StateAccessPermissions, StatuteParagraphAccessPermissions, WorkflowAccessPermissions, ) @@ -979,6 +980,8 @@ class State(RESTModelMixin, models.Model): state. """ + access_permissions = StateAccessPermissions() + name = models.CharField(max_length=255) """A string representing the state.""" @@ -1101,12 +1104,6 @@ class State(RESTModelMixin, models.Model): ] return state_id in next_state_ids or state_id in previous_state_ids - def get_root_rest_element(self): - """ - Returns the workflow to this instance which is the root REST element. - """ - return self.workflow - class WorkflowManager(models.Manager): """ diff --git a/openslides/motions/projector.py b/openslides/motions/projector.py index 0ea6d31ef..3b94c2b39 100644 --- a/openslides/motions/projector.py +++ b/openslides/motions/projector.py @@ -21,17 +21,14 @@ async def get_state( all_data: AllData, motion: Dict[str, Any], state_id_key: str ) -> Dict[str, Any]: """ - Returns a state element from one motion. - - Returns an error if the state_id does not exist for the workflow in the motion. + Returns a state element from one motion. Raises an error if the state does not exist. """ - states = all_data["motions/workflow"][motion["workflow_id"]]["states"] - for state in states: - if state["id"] == motion[state_id_key]: - return state - raise ProjectorElementException( - f"motion {motion['id']} can not be on the state with id {motion[state_id_key]}" - ) + state = all_data["motions/state"].get(motion[state_id_key]) + if not state: + raise ProjectorElementException( + f"motion {motion['id']} can not be on the state with id {motion[state_id_key]}" + ) + return state async def get_amendment_merge_into_motion_diff(all_data, amendment): @@ -165,6 +162,7 @@ async def motion_slide( * change_recommendations * submitter """ + # Get motion mode = element.get( "mode", await get_config(all_data, "motions_recommendation_text_mode") ) @@ -178,6 +176,7 @@ async def motion_slide( except KeyError: raise ProjectorElementException(f"motion with id {motion_id} does not exist") + # Get some needed config values show_meta_box = not await get_config( all_data, "motions_disable_sidebox_on_projector" ) @@ -185,26 +184,25 @@ async def motion_slide( line_numbering_mode = await get_config(all_data, "motions_default_line_numbering") motions_preamble = await get_config(all_data, "motions_preamble") + # Query all change-recommendation and amendment related things. + change_recommendations = [] # type: ignore + amendments = [] # type: ignore + base_motion = None + base_statute = None if motion["statute_paragraph_id"]: - change_recommendations = [] # type: ignore - amendments = [] # type: ignore - base_motion = None base_statute = await get_amendment_base_statute(motion, all_data) - elif bool(motion["parent_id"]) and motion["amendment_paragraphs"]: - change_recommendations = [] - amendments = [] + elif motion["parent_id"] is not None and motion["amendment_paragraphs"]: base_motion = await get_amendment_base_motion(motion, all_data) - base_statute = None else: - change_recommendations = list( - filter( - lambda reco: reco["internal"] is False, motion["change_recommendations"] + for change_recommendation_id in motion["change_recommendations_id"]: + cr = all_data["motions/motion-change-recommendation"].get( + change_recommendation_id ) - ) + if cr is not None and not cr["internal"]: + change_recommendations.append(cr) amendments = await get_amendments_for_motion(motion, all_data) - base_motion = None - base_statute = None + # The base return value. More fields will get added below. return_value = { "identifier": motion["identifier"], "title": motion["title"], diff --git a/openslides/motions/serializers.py b/openslides/motions/serializers.py index 7be000671..ab3076eac 100644 --- a/openslides/motions/serializers.py +++ b/openslides/motions/serializers.py @@ -166,7 +166,8 @@ class WorkflowSerializer(ModelSerializer): Serializer for motion.models.Workflow objects. """ - states = StateSerializer(many=True, read_only=True) + # states = StateSerializer(many=True, read_only=True) + states = IdPrimaryKeyRelatedField(many=True, read_only=True) class Meta: model = Workflow @@ -431,9 +432,7 @@ class MotionSerializer(ModelSerializer): ) agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1) submitters = SubmitterSerializer(many=True, read_only=True) - change_recommendations = MotionChangeRecommendationSerializer( - many=True, read_only=True - ) + change_recommendations = IdPrimaryKeyRelatedField(many=True, read_only=True) class Meta: model = Motion diff --git a/openslides/motions/views.py b/openslides/motions/views.py index 4b92f0971..2632b0b1b 100644 --- a/openslides/motions/views.py +++ b/openslides/motions/views.py @@ -14,7 +14,6 @@ from ..core.models import Tag from ..utils.auth import has_perm, in_some_groups from ..utils.autoupdate import inform_changed_data, inform_deleted_data from ..utils.rest_api import ( - CreateModelMixin, DestroyModelMixin, GenericViewSet, ModelViewSet, @@ -32,6 +31,7 @@ from .access_permissions import ( MotionBlockAccessPermissions, MotionChangeRecommendationAccessPermissions, MotionCommentSectionAccessPermissions, + StateAccessPermissions, StatuteParagraphAccessPermissions, WorkflowAccessPermissions, ) @@ -50,7 +50,7 @@ from .models import ( Workflow, ) from .numbering import numbering -from .serializers import MotionPollSerializer, StateSerializer +from .serializers import MotionPollSerializer # Viewsets for the REST API @@ -1533,10 +1533,8 @@ class WorkflowViewSet(ModelViewSet, ProtectedErrorMessageMixin): """ Returns True if the user has required permissions. """ - if self.action in ("list", "retrieve"): + if self.action in ("list", "retrieve", "metadata"): result = self.get_access_permissions().check_permissions(self.request.user) - elif self.action == "metadata": - result = has_perm(self.request.user, "motions.can_see") elif self.action in ("create", "partial_update", "update", "destroy"): result = has_perm(self.request.user, "motions.can_see") and has_perm( self.request.user, "motions.can_manage" @@ -1571,13 +1569,7 @@ class WorkflowViewSet(ModelViewSet, ProtectedErrorMessageMixin): return result -class StateViewSet( - CreateModelMixin, - UpdateModelMixin, - DestroyModelMixin, - GenericViewSet, - ProtectedErrorMessageMixin, -): +class StateViewSet(ModelViewSet, ProtectedErrorMessageMixin): """ API endpoint for workflow states. @@ -1585,21 +1577,29 @@ class StateViewSet( """ queryset = State.objects.all() - serializer_class = StateSerializer + # serializer_class = StateSerializer + access_permissions = StateAccessPermissions() def check_view_permissions(self): """ Returns True if the user has required permissions. """ - return has_perm(self.request.user, "motions.can_see") and has_perm( - self.request.user, "motions.can_manage" - ) + if self.action in ("list", "retrieve", "metadata"): + result = self.get_access_permissions().check_permissions(self.request.user) + elif self.action in ("create", "partial_update", "update", "destroy"): + result = has_perm(self.request.user, "motions.can_see") and has_perm( + self.request.user, "motions.can_manage" + ) + else: + result = False + return result def destroy(self, *args, **kwargs): """ Customized view endpoint to delete a state. """ state = self.get_object() + workflow = state.workflow if state.workflow.first_state.pk == state.pk: # is this the first state of the workflow? raise ValidationError( @@ -1610,6 +1610,17 @@ class StateViewSet( except ProtectedError as err: msg = self.getProtectedErrorMessage("workflow", err) raise ValidationError({"detail": msg}) + inform_changed_data(workflow) + return result + + def create(self, request, *args, **kwargs): + """ + """ + result = super().create(request, *args, **kwargs) + workflow_id = request.data[ + "workflow_id" + ] # This must be correct, if the state was created successfully + inform_changed_data(Workflow.objects.get(pk=workflow_id)) return result def update(self, *args, **kwargs): diff --git a/tests/unit/motions/test_projector.py b/tests/unit/motions/test_projector.py index 21116003f..1b3500e9d 100644 --- a/tests/unit/motions/test_projector.py +++ b/tests/unit/motions/test_projector.py @@ -74,32 +74,7 @@ def all_data(): "weight": 10000, "created": "2019-01-19T18:37:34.741336+01:00", "last_modified": "2019-01-19T18:37:34.741368+01:00", - "change_recommendations": [ - { - "id": 1, - "motion_id": 1, - "rejected": False, - "internal": True, - "type": 0, - "other_description": "", - "line_from": 1, - "line_to": 2, - "text": "internal new motion text", - "creation_time": "2019-02-09T09:54:06.256378+01:00", - }, - { - "id": 2, - "motion_id": 1, - "rejected": False, - "internal": False, - "type": 0, - "other_description": "", - "line_from": 1, - "line_to": 2, - "text": "public new motion text", - "creation_time": "2019-02-09T09:54:06.256378+01:00", - }, - ], + "change_recommendations_id": [1, 2], }, 2: { "id": 2, @@ -172,75 +147,76 @@ def all_data(): 1: { "id": 1, "name": "Simple Workflow", - "states": [ - { - "id": 1, - "name": "submitted", - "recommendation_label": None, - "css_class": "lightblue", - "restriction": [], - "allow_support": True, - "allow_create_poll": True, - "allow_submitter_edit": True, - "dont_set_identifier": False, - "show_state_extension_field": False, - "merge_amendment_into_final": 0, - "show_recommendation_extension_field": False, - "next_states_id": [2, 3, 4], - "workflow_id": 1, - }, - { - "id": 2, - "name": "accepted", - "recommendation_label": "Acceptance", - "css_class": "green", - "restriction": [], - "allow_support": False, - "allow_create_poll": False, - "allow_submitter_edit": False, - "dont_set_identifier": False, - "show_state_extension_field": False, - "merge_amendment_into_final": 1, - "show_recommendation_extension_field": False, - "next_states_id": [], - "workflow_id": 1, - }, - { - "id": 3, - "name": "rejected", - "recommendation_label": "Rejection", - "css_class": "red", - "restriction": [], - "allow_support": False, - "allow_create_poll": False, - "allow_submitter_edit": False, - "dont_set_identifier": False, - "show_state_extension_field": False, - "merge_amendment_into_final": -1, - "show_recommendation_extension_field": False, - "next_states_id": [], - "workflow_id": 1, - }, - { - "id": 4, - "name": "not decided", - "recommendation_label": "No decision", - "css_class": "grey", - "restriction": [], - "allow_support": False, - "allow_create_poll": False, - "allow_submitter_edit": False, - "dont_set_identifier": False, - "show_state_extension_field": False, - "merge_amendment_into_final": -1, - "show_recommendation_extension_field": False, - "next_states_id": [], - "workflow_id": 1, - }, - ], + "states": [1, 2, 3, 4], "first_state_id": 1, } } + return_value["motions/state"] = { + 1: { + "id": 1, + "name": "submitted", + "recommendation_label": None, + "css_class": "lightblue", + "restriction": [], + "allow_support": True, + "allow_create_poll": True, + "allow_submitter_edit": True, + "dont_set_identifier": False, + "show_state_extension_field": False, + "merge_amendment_into_final": 0, + "show_recommendation_extension_field": False, + "next_states_id": [2, 3, 4], + "workflow_id": 1, + }, + 2: { + "id": 2, + "name": "accepted", + "recommendation_label": "Acceptance", + "css_class": "green", + "restriction": [], + "allow_support": False, + "allow_create_poll": False, + "allow_submitter_edit": False, + "dont_set_identifier": False, + "show_state_extension_field": False, + "merge_amendment_into_final": 1, + "show_recommendation_extension_field": False, + "next_states_id": [], + "workflow_id": 1, + }, + 3: { + "id": 3, + "name": "rejected", + "recommendation_label": "Rejection", + "css_class": "red", + "restriction": [], + "allow_support": False, + "allow_create_poll": False, + "allow_submitter_edit": False, + "dont_set_identifier": False, + "show_state_extension_field": False, + "merge_amendment_into_final": -1, + "show_recommendation_extension_field": False, + "next_states_id": [], + "workflow_id": 1, + }, + 4: { + "id": 4, + "name": "not decided", + "recommendation_label": "No decision", + "css_class": "grey", + "restriction": [], + "allow_support": False, + "allow_create_poll": False, + "allow_submitter_edit": False, + "dont_set_identifier": False, + "show_state_extension_field": False, + "merge_amendment_into_final": -1, + "show_recommendation_extension_field": False, + "next_states_id": [], + "workflow_id": 1, + }, + } return_value["motions/statute-paragraph"] = { 1: { "id": 1, @@ -249,7 +225,32 @@ def all_data(): "weight": 10000, } } - return_value["motions/motion-change-recommendation"] = {} + return_value["motions/motion-change-recommendation"] = { + 1: { + "id": 1, + "motion_id": 1, + "rejected": False, + "internal": True, + "type": 0, + "other_description": "", + "line_from": 1, + "line_to": 2, + "text": "internal new motion text", + "creation_time": "2019-02-09T09:54:06.256378+01:00", + }, + 2: { + "id": 2, + "motion_id": 1, + "rejected": False, + "internal": False, + "type": 0, + "other_description": "", + "line_from": 1, + "line_to": 2, + "text": "public new motion text", + "creation_time": "2019-02-09T09:54:06.256378+01:00", + }, + } return return_value