Generic relations for the repos

This commit is contained in:
FinnStutzenstein 2019-07-17 16:13:49 +02:00
parent 13e8bbb168
commit dd66244d84
105 changed files with 1489 additions and 1670 deletions

View File

@ -20,7 +20,7 @@ type TypeIdentifier = UnifiedConstructors | BaseRepository<any, any, any> | stri
type CollectionStringMappedTypes = [ type CollectionStringMappedTypes = [
ModelConstructor<BaseModel>, ModelConstructor<BaseModel>,
ViewModelConstructor<BaseViewModel>, ViewModelConstructor<BaseViewModel>,
BaseRepository<BaseViewModel, BaseModel, TitleInformation> BaseRepository<BaseViewModel<any>, BaseModel<any>, TitleInformation>
]; ];
/** /**
@ -46,7 +46,7 @@ export class CollectionStringMapperService {
* @param collectionString * @param collectionString
* @param model * @param model
*/ */
public registerCollectionElement<V extends BaseViewModel, M extends BaseModel>( public registerCollectionElement<V extends BaseViewModel<M>, M extends BaseModel>(
collectionString: string, collectionString: string,
model: ModelConstructor<M>, model: ModelConstructor<M>,
viewModel: ViewModelConstructor<V>, viewModel: ViewModelConstructor<V>,

View File

@ -16,14 +16,23 @@ import {
IBaseViewModelWithAgendaItem IBaseViewModelWithAgendaItem
} from 'app/site/base/base-view-model-with-agenda-item'; } from 'app/site/base/base-view-model-with-agenda-item';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; 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 { 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 { 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 * Repository service for items
@ -58,12 +67,7 @@ export class ItemRepositoryService extends BaseHasContentObjectRepository<
private httpService: HttpService, private httpService: HttpService,
private config: ConfigService private config: ConfigService
) { ) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, Item, [ super(DS, dataSend, mapperService, viewModelStoreService, translate, Item, ItemRelations);
Topic,
Assignment,
Motion,
MotionBlock
]);
this.setSortFunction((a, b) => a.weight - b.weight); 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<BaseViewModel>(
agendaItem.content_object.collection,
agendaItem.content_object.id
);
if (!contentObject || !isBaseViewModelWithAgendaItem(contentObject)) {
return null;
}
return contentObject;
}
/** /**
* Trigger the automatic numbering sequence on the server * Trigger the automatic numbering sequence on the server
*/ */

View File

@ -12,20 +12,42 @@ import {
BaseViewModelWithListOfSpeakers, BaseViewModelWithListOfSpeakers,
isBaseViewModelWithListOfSpeakers isBaseViewModelWithListOfSpeakers
} from 'app/site/base/base-view-model-with-list-of-speakers'; } 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 { 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 { Identifiable } from 'app/shared/models/base/identifiable';
import { HttpService } from 'app/core/core-services/http.service'; import { HttpService } from 'app/core/core-services/http.service';
import { BaseIsListOfSpeakersContentObjectRepository } from '../base-is-list-of-speakers-content-object-repository'; import { BaseIsListOfSpeakersContentObjectRepository } from '../base-is-list-of-speakers-content-object-repository';
import { BaseHasContentObjectRepository } from '../base-has-content-object-repository'; import { BaseHasContentObjectRepository, GenericRelationDefinition } 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 { ItemRepositoryService } from './item-repository.service'; 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 * Repository service for lists of speakers
@ -60,14 +82,7 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit
private httpService: HttpService, private httpService: HttpService,
private itemRepo: ItemRepositoryService private itemRepo: ItemRepositoryService
) { ) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, ListOfSpeakers, [ super(DS, dataSend, mapperService, viewModelStoreService, translate, ListOfSpeakers, ListOfSpeakersRelations);
Topic,
Assignment,
Motion,
MotionBlock,
Mediafile,
User
]);
} }
public getVerboseName = (plural: boolean = false) => { 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<BaseViewModel>(
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. * Add a new speaker to a list of speakers.
* Sends the users id to the server * Sends the users id to the server

View File

@ -3,26 +3,71 @@ import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Assignment } from 'app/shared/models/assignments/assignment'; 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 { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { DataSendService } from 'app/core/core-services/data-send.service'; import { DataSendService } from 'app/core/core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service'; import { DataStoreService } from '../../core-services/data-store.service';
import { HttpService } from 'app/core/core-services/http.service'; import { HttpService } from 'app/core/core-services/http.service';
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll'; 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 { 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 { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewTag } from 'app/site/tags/models/view-tag'; import { ViewTag } from 'app/site/tags/models/view-tag';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
import { ViewAssignmentRelatedUser } from 'app/site/assignments/models/view-assignment-related-user'; import { ViewAssignmentRelatedUser } from 'app/site/assignments/models/view-assignment-related-user';
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll'; 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 { 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. * Repository Service for Assignments.
@ -62,7 +107,7 @@ export class AssignmentRepositoryService extends BaseIsAgendaItemAndListOfSpeake
protected translate: TranslateService, protected translate: TranslateService,
private httpService: HttpService 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) => { public getTitle = (titleInformation: AssignmentTitleInformation) => {
@ -73,48 +118,6 @@ export class AssignmentRepositoryService extends BaseIsAgendaItemAndListOfSpeake
return this.translate.instant(plural ? 'Elections' : 'Election'); 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 * Adds/removes another user to/from the candidates list of an assignment
* *

View File

@ -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 { BaseModelWithContentObject } from 'app/shared/models/base/base-model-with-content-object';
import { BaseViewModelWithContentObject } from 'app/site/base/base-view-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 { 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<VForeign extends BaseViewModel = BaseViewModel> {
type: 'generic';
possibleModels: ViewModelConstructor<BaseViewModel>[];
isVForeign: (obj: any) => obj is VForeign;
VForeignVerbose: string;
}
/** /**
* A base repository for objects that *have* content objects, e.g. items and lists of speakers. * 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<M>,
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(<any>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 = <any>this.viewModelStoreService.get(collection, changedId);
if (
foreignModel &&
foreignModel.collectionString === ownViewModel.contentObjectData.collection &&
foreignModel.id === ownViewModel.contentObjectData.id
) {
if (relation.isVForeign(foreignModel)) {
(<any>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<K extends BaseViewModel = V>(
model: M,
viewModel: K,
relation: RelationDefinition | GenericRelationDefinition
): void {
if (relation.type === 'generic') {
(<any>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<BaseViewModel>(
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. * Returns the object with has the given content object as the content object.
* *

View File

@ -1,7 +1,7 @@
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model'; import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model';
import { BaseRepository } from './base-repository'; import { BaseRepository, RelationDefinition } from './base-repository';
import { import {
isBaseIsAgendaItemContentObjectRepository, isBaseIsAgendaItemContentObjectRepository,
IBaseIsAgendaItemContentObjectRepository IBaseIsAgendaItemContentObjectRepository
@ -13,8 +13,6 @@ import {
import { DataStoreService } from '../core-services/data-store.service'; import { DataStoreService } from '../core-services/data-store.service';
import { DataSendService } from '../core-services/data-send.service'; import { DataSendService } from '../core-services/data-send.service';
import { ViewModelStoreService } from '../core-services/view-model-store.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 { CollectionStringMapperService } from '../core-services/collection-string-mapper.service';
import { import {
TitleInformationWithAgendaItem, TitleInformationWithAgendaItem,
@ -22,6 +20,8 @@ import {
} from 'app/site/base/base-view-model-with-agenda-item'; } from 'app/site/base/base-view-model-with-agenda-item';
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model'; import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
import { IBaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers'; 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( export function isBaseIsAgendaItemAndListOfSpeakersContentObjectRepository(
obj: any obj: any
@ -50,7 +50,7 @@ export abstract class BaseIsAgendaItemAndListOfSpeakersContentObjectRepository<
viewModelStoreService: ViewModelStoreService, viewModelStoreService: ViewModelStoreService,
translate: TranslateService, translate: TranslateService,
baseModelCtor: ModelConstructor<M>, baseModelCtor: ModelConstructor<M>,
depsModelCtors?: ModelConstructor<BaseModel>[] relationDefinitions?: RelationDefinition[]
) { ) {
super( super(
DS, DS,
@ -59,13 +59,24 @@ export abstract class BaseIsAgendaItemAndListOfSpeakersContentObjectRepository<
viewModelStoreService, viewModelStoreService,
translate, translate,
baseModelCtor, baseModelCtor,
depsModelCtors relationDefinitions
); );
if (!this.depsModelCtors) { }
this.depsModelCtors = [];
} protected groupRelationsByCollections(): void {
this.depsModelCtors.push(Item); this.relationDefinitions.push({
this.depsModelCtors.push(ListOfSpeakers); 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 { public getAgendaListTitle(titleInformation: T): string {

View File

@ -3,14 +3,14 @@ import { TranslateService } from '@ngx-translate/core';
import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model'; import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model';
import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service'; import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service';
import { DataSendService } from '../core-services/data-send.service'; import { DataSendService } from '../core-services/data-send.service';
import { BaseRepository } from './base-repository'; import { BaseRepository, RelationDefinition } from './base-repository';
import { Item } from 'app/shared/models/agenda/item';
import { DataStoreService } from '../core-services/data-store.service'; import { DataStoreService } from '../core-services/data-store.service';
import { ViewModelStoreService } from '../core-services/view-model-store.service'; import { ViewModelStoreService } from '../core-services/view-model-store.service';
import { import {
TitleInformationWithAgendaItem, TitleInformationWithAgendaItem,
BaseViewModelWithAgendaItem BaseViewModelWithAgendaItem
} from 'app/site/base/base-view-model-with-agenda-item'; } from 'app/site/base/base-view-model-with-agenda-item';
import { ViewItem } from 'app/site/agenda/models/view-item';
export function isBaseIsAgendaItemContentObjectRepository( export function isBaseIsAgendaItemContentObjectRepository(
obj: any obj: any
@ -46,7 +46,7 @@ export abstract class BaseIsAgendaItemContentObjectRepository<
viewModelStoreService: ViewModelStoreService, viewModelStoreService: ViewModelStoreService,
translate: TranslateService, translate: TranslateService,
baseModelCtor: ModelConstructor<M>, baseModelCtor: ModelConstructor<M>,
depsModelCtors?: ModelConstructor<BaseModel>[] relationDefinitions?: RelationDefinition[]
) { ) {
super( super(
DS, DS,
@ -55,12 +55,18 @@ export abstract class BaseIsAgendaItemContentObjectRepository<
viewModelStoreService, viewModelStoreService,
translate, translate,
baseModelCtor, baseModelCtor,
depsModelCtors relationDefinitions
); );
if (!this.depsModelCtors) { }
this.depsModelCtors = [];
} protected groupRelationsByCollections(): void {
this.depsModelCtors.push(Item); this.relationDefinitions.push({
type: 'O2M',
ownIdKey: 'agenda_item_id',
ownKey: 'item',
foreignModel: ViewItem
});
super.groupRelationsByCollections();
} }
/** /**

View File

@ -2,13 +2,13 @@ import { TranslateService } from '@ngx-translate/core';
import { TitleInformation } from '../../site/base/base-view-model'; import { TitleInformation } from '../../site/base/base-view-model';
import { BaseModel, ModelConstructor } from '../../shared/models/base/base-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 { DataStoreService } from '../core-services/data-store.service';
import { DataSendService } from '../core-services/data-send.service'; import { DataSendService } from '../core-services/data-send.service';
import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service'; import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service';
import { ViewModelStoreService } from '../core-services/view-model-store.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 { 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( export function isBaseIsListOfSpeakersContentObjectRepository(
obj: any obj: any
@ -44,7 +44,7 @@ export abstract class BaseIsListOfSpeakersContentObjectRepository<
viewModelStoreService: ViewModelStoreService, viewModelStoreService: ViewModelStoreService,
translate: TranslateService, translate: TranslateService,
baseModelCtor: ModelConstructor<M>, baseModelCtor: ModelConstructor<M>,
depsModelCtors?: ModelConstructor<BaseModel>[] relationDefinitions?: RelationDefinition[]
) { ) {
super( super(
DS, DS,
@ -53,12 +53,18 @@ export abstract class BaseIsListOfSpeakersContentObjectRepository<
viewModelStoreService, viewModelStoreService,
translate, translate,
baseModelCtor, baseModelCtor,
depsModelCtors relationDefinitions
); );
if (!this.depsModelCtors) { }
this.depsModelCtors = [];
} protected groupRelationsByCollections(): void {
this.depsModelCtors.push(ListOfSpeakers); this.relationDefinitions.push({
type: 'O2M',
ownIdKey: 'list_of_speakers_id',
ownKey: 'list_of_speakers',
foreignModel: ViewListOfSpeakers
});
super.groupRelationsByCollections();
} }
public getListOfSpeakersTitle(titleInformation: T): string { public getListOfSpeakersTitle(titleInformation: T): string {

View File

@ -2,7 +2,7 @@ import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { auditTime } from 'rxjs/operators'; import { auditTime } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core'; 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 { BaseModel, ModelConstructor } from '../../shared/models/base/base-model';
import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service'; import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service';
import { DataSendService } from '../core-services/data-send.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 { OnAfterAppsLoaded } from '../onAfterAppsLoaded';
import { Collection } from 'app/shared/models/base/collection'; import { Collection } from 'app/shared/models/base/collection';
// All "standard" relations.
export type RelationDefinition<VForeign extends BaseViewModel = BaseViewModel> =
| NormalRelationDefinition<VForeign>
| NestedRelationDefinition<VForeign>
| CustomRelationDefinition<VForeign>;
/**
* Normal relations.
*/
interface NormalRelationDefinition<VForeign extends BaseViewModel> {
/**
* - 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 `_<ownKey`!
*
* E.g. `category`. (private variable `_category`)
*/
ownKey: string;
/**
* The model on the other side of the relation.
*/
foreignModel: ViewModelConstructor<VForeign>;
/**
* TODO: reverse relations.
*/
foreignKey?: keyof VForeign;
}
/**
* Nested relations in the REST-API. For the most values see
* `NormalRelationDefinition`.
*/
interface NestedRelationDefinition<VForeign extends BaseViewModel> {
type: 'nested';
ownKey: string;
foreignModel: ViewModelConstructor<VForeign>;
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<VForeign extends BaseViewModel> {
type: 'custom';
foreignModel: ViewModelConstructor<VForeign>;
/**
* 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<V extends BaseViewModel & T, M extends BaseModel, T extends TitleInformation> export abstract class BaseRepository<V extends BaseViewModel & T, M extends BaseModel, T extends TitleInformation>
implements OnAfterAppsLoaded, Collection { implements OnAfterAppsLoaded, Collection {
/** /**
@ -71,6 +154,20 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
public abstract getVerboseName: (plural?: boolean) => string; public abstract getVerboseName: (plural?: boolean) => string;
public abstract getTitle: (titleInformation: T) => 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<BaseViewModel>[] } = {};
/**
* The view model ctor of the encapsulated view model.
*/
protected baseViewModelCtor: ViewModelConstructor<V>;
/** /**
* Construction routine for the base repository * Construction routine for the base repository
* *
@ -87,10 +184,12 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
protected viewModelStoreService: ViewModelStoreService, protected viewModelStoreService: ViewModelStoreService,
protected translate: TranslateService, protected translate: TranslateService,
protected baseModelCtor: ModelConstructor<M>, protected baseModelCtor: ModelConstructor<M>,
protected depsModelCtors?: ModelConstructor<BaseModel>[] protected relationDefinitions: RelationDefinition<BaseViewModel>[] = []
) { ) {
this._collectionString = baseModelCtor.COLLECTIONSTRING; this._collectionString = baseModelCtor.COLLECTIONSTRING;
this.groupRelationsByCollections();
// All data is piped through an auditTime of 1ms. This is to prevent massive // 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 // 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. // update of the new list instead of many unnecessary updates.
@ -103,7 +202,34 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
this.languageCollator = new Intl.Collator(this.translate.currentLang); this.languageCollator = new Intl.Collator(this.translate.currentLang);
} }
/**
* Reorders the relations to provide faster access.
*/
protected groupRelationsByCollections(): void {
this.relationDefinitions.forEach(relation => {
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 { public onAfterAppsLoaded(): void {
this.baseViewModelCtor = this.collectionStringMapperService.getViewModelConstructor(this.collectionString);
this.DS.clearObservable.subscribe(() => this.clear()); this.DS.clearObservable.subscribe(() => this.clear());
this.translate.onLangChange.subscribe(change => { this.translate.onLangChange.subscribe(change => {
this.languageCollator = new Intl.Collator(change.lang); this.languageCollator = new Intl.Collator(change.lang);
@ -113,6 +239,10 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
}); });
} }
public getListTitle: (titleInformation: T) => string = (titleInformation: T) => {
return this.getTitle(titleInformation);
};
/** /**
* Deletes all models from the repository (internally, no requests). Changes need * Deletes all models from the repository (internally, no requests). Changes need
* to be committed via `commitUpdate()`. * to be committed via `commitUpdate()`.
@ -139,6 +269,73 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
}); });
} }
/**
* After creating a view model, all functions for models form the repo
* are assigned to the new view model.
*/
protected createViewModelWithTitles(model: M): V {
const viewModel = this.createViewModel(model, this.baseViewModelCtor, this.relationDefinitions);
viewModel.getTitle = () => 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<K extends BaseViewModel = V>(
model: M,
modelCtor: ViewModelConstructor<K>,
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<K extends BaseViewModel = V>(
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. * Updates all models in this repository with all changed models.
* *
@ -146,7 +343,7 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
* @returns if at least one model was affected. * @returns if at least one model was affected.
*/ */
public updateDependencies(changedModels: CollectionIds): boolean { public updateDependencies(changedModels: CollectionIds): boolean {
if (!this.depsModelCtors || this.depsModelCtors.length === 0) { if (!this.relationDefinitions.length) {
return; return;
} }
@ -154,20 +351,25 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
const viewModels = this.getViewModelList(); const viewModels = this.getViewModelList();
let somethingUpdated = false; let somethingUpdated = false;
Object.keys(changedModels).forEach(collection => { Object.keys(changedModels).forEach(collection => {
const dependencyChanged: boolean = this.depsModelCtors.some(ctor => { const dependencyChanged: boolean = Object.keys(this.relationsByCollection).includes(collection);
return ctor.COLLECTIONSTRING === collection;
});
if (!dependencyChanged) { if (!dependencyChanged) {
return; return;
} }
// Ok, we are affected by this collection. Update all viewModels from this repo. // Ok, we are affected by this collection. Update all viewModels from this repo.
viewModels.forEach(ownViewModel => { viewModels.forEach(ownViewModel => {
changedModels[collection].forEach(id => { const relations = this.relationsByCollection[collection];
ownViewModel.updateDependencies(this.viewModelStoreService.get(collection, id)); 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) { if (somethingUpdated) {
viewModels.forEach(ownViewModel => { viewModels.forEach(ownViewModel => {
@ -177,9 +379,64 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
return somethingUpdated; return somethingUpdated;
} }
public getListTitle: (titleInformation: T) => 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 = <any>this.viewModelStoreService.get(collection, changedId);
let ownModelArray = <any>ownViewModel['_' + relation.ownKey];
if (!ownModelArray) {
ownViewModel['_' + relation.ownKey] = [];
ownModelArray = <any>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] === <any>changedId) {
ownViewModel['_' + relation.ownKey] = <any>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 = <any>this.viewModelStoreService.get(collection, changedId);
return relation.updateDependency(ownViewModel, foreignViewModel);
}
return false;
}
/** /**
* Saves the (full) update to an existing model. So called "update"-function * Saves the (full) update to an existing model. So called "update"-function
@ -242,27 +499,6 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
return await this.dataSend.createModel(sendModel); return await this.dataSend.createModel(sendModel);
} }
/**
* Creates a view model out of a base model.
*
* Should read all necessary objects from the datastore
* that the viewmodel needs
* @param model
*/
protected abstract createViewModel(model: M): V;
/**
* After creating a view model, all functions for models form the repo
* are assigned to the new view model.
*/
protected createViewModelWithTitles(model: M): V {
const viewModel = this.createViewModel(model);
viewModel.getTitle = () => this.getTitle(viewModel);
viewModel.getListTitle = () => this.getListTitle(viewModel);
viewModel.getVerboseName = this.getVerboseName;
return viewModel;
}
/** /**
* Clears the repository. * Clears the repository.
*/ */

View File

@ -134,14 +134,6 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config,
return titleInformation.key; return titleInformation.key;
}; };
/**
* Creates a new ViewConfig of a given Config object
* @param config
*/
public createViewModel(config: Config): ViewConfig {
return new ViewConfig(config);
}
/** /**
* Overwrites the default delete procedure * Overwrites the default delete procedure
* *
@ -160,14 +152,6 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config,
throw new Error('Config variables cannot be created'); throw new Error('Config variables cannot be created');
} }
protected loadInitialFromDS(): void {
this.DS.getAll(Config).forEach((config: Config) => {
this.viewModelStore[config.id] = this.createViewModel(config);
this.updateConfigStructure(false, this.viewModelStore[config.id]);
});
this.updateConfigListObservable();
}
public changedModels(ids: number[]): void { public changedModels(ids: number[]): void {
super.changedModels(ids); super.changedModels(ids);

View File

@ -14,9 +14,29 @@ import { DataSendService } from 'app/core/core-services/data-send.service';
import { HttpService } from 'app/core/core-services/http.service'; import { HttpService } from 'app/core/core-services/http.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { BaseIsListOfSpeakersContentObjectRepository } from '../base-is-list-of-speakers-content-object-repository'; 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 { 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 * Repository for MediaFiles
@ -46,7 +66,7 @@ export class MediafileRepositoryService extends BaseIsListOfSpeakersContentObjec
dataSend: DataSendService, dataSend: DataSendService,
private httpService: HttpService 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.directoryBehaviorSubject = new BehaviorSubject([]);
this.getViewModelListObservable().subscribe(mediafiles => { this.getViewModelListObservable().subscribe(mediafiles => {
if (mediafiles) { if (mediafiles) {
@ -67,25 +87,6 @@ export class MediafileRepositoryService extends BaseIsListOfSpeakersContentObjec
return this.translate.instant(plural ? 'Files' : 'File'); 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, <number[]>(
file.inherited_access_groups_id
));
}
return new ViewMediafile(file, listOfSpeakers, parent, accessGroups, inheritedAccessGroups);
}
public async getDirectoryIdByPath(pathSegments: string[]): Promise<number | null> { public async getDirectoryIdByPath(pathSegments: string[]): Promise<number | null> {
let parentId = null; let parentId = null;

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/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 { Category } from 'app/shared/models/motions/category';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service'; import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { DataSendService } from '../../core-services/data-send.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 { Motion } from 'app/shared/models/motions/motion';
import { TreeIdNode } from 'app/core/ui-services/tree.service'; 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 * Repository Services for Categories
* *
@ -47,7 +56,7 @@ export class CategoryRepositoryService extends BaseRepository<ViewCategory, Cate
translate: TranslateService, translate: TranslateService,
private httpService: HttpService private httpService: HttpService
) { ) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, Category, [Category]); super(DS, dataSend, mapperService, viewModelStoreService, translate, Category, CategoryRelations);
this.setSortFunction((a, b) => a.weight - b.weight); this.setSortFunction((a, b) => a.weight - b.weight);
} }
@ -62,11 +71,6 @@ export class CategoryRepositoryService extends BaseRepository<ViewCategory, Cate
return this.translate.instant(plural ? 'Categories' : 'Category'); return this.translate.instant(plural ? 'Categories' : 'Category');
}; };
protected createViewModel(category: Category): ViewCategory {
const parent = this.viewModelStoreService.get(ViewCategory, category.parent_id);
return new ViewCategory(category, parent);
}
/** /**
* Updates a categories numbering. * Updates a categories numbering.
* *

View File

@ -5,9 +5,6 @@ import { map } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DataSendService } from 'app/core/core-services/data-send.service'; import { DataSendService } from 'app/core/core-services/data-send.service';
import { User } from 'app/shared/models/users/user';
import { Category } from 'app/shared/models/motions/category';
import { Workflow } from 'app/shared/models/motions/workflow';
import { BaseRepository } from '../base-repository'; import { BaseRepository } from '../base-repository';
import { DataStoreService } from 'app/core/core-services/data-store.service'; import { DataStoreService } from 'app/core/core-services/data-store.service';
import { MotionChangeRecommendation } from 'app/shared/models/motions/motion-change-reco'; import { MotionChangeRecommendation } from 'app/shared/models/motions/motion-change-reco';
@ -61,11 +58,7 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
translate: TranslateService, translate: TranslateService,
private diffService: DiffService private diffService: DiffService
) { ) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, MotionChangeRecommendation, [ super(DS, dataSend, mapperService, viewModelStoreService, translate, MotionChangeRecommendation);
Category,
User,
Workflow
]);
} }
public getTitle = (titleInformation: MotionChangeRecommendationTitleInformation) => { public getTitle = (titleInformation: MotionChangeRecommendationTitleInformation) => {
@ -76,15 +69,6 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
return this.translate.instant(plural ? 'Change recommendations' : 'Change recommendation'); 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. * Given a change recommendation view object, a entry in the backend is created.
* @param view * @param view

View File

@ -14,8 +14,6 @@ import { MotionRepositoryService } from './motion-repository.service';
import { ViewMotion } from 'app/site/motions/models/view-motion'; import { ViewMotion } from 'app/site/motions/models/view-motion';
import { ViewMotionBlock, MotionBlockTitleInformation } from 'app/site/motions/models/view-motion-block'; import { ViewMotionBlock, MotionBlockTitleInformation } from 'app/site/motions/models/view-motion-block';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; 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'; 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'); 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 * Removes the motion block id from the given motion
* *

View File

@ -4,19 +4,33 @@ import { TranslateService } from '@ngx-translate/core';
import { DataSendService } from '../../core-services/data-send.service'; import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service'; import { DataStoreService } from '../../core-services/data-store.service';
import { BaseRepository } from '../base-repository'; import { BaseRepository, RelationDefinition } from '../base-repository';
import { import {
ViewMotionCommentSection, ViewMotionCommentSection,
MotionCommentSectionTitleInformation MotionCommentSectionTitleInformation
} from 'app/site/motions/models/view-motion-comment-section'; } from 'app/site/motions/models/view-motion-comment-section';
import { MotionCommentSection } from 'app/shared/models/motions/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 { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { HttpService } from 'app/core/core-services/http.service'; import { HttpService } from 'app/core/core-services/http.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewGroup } from 'app/site/users/models/view-group'; import { ViewGroup } from 'app/site/users/models/view-group';
import { ViewMotion } from 'app/site/motions/models/view-motion'; 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 * Repository Services for Categories
* *
@ -53,7 +67,15 @@ export class MotionCommentSectionRepositoryService extends BaseRepository<
translate: TranslateService, translate: TranslateService,
private http: HttpService 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) => { this.viewModelSortFn = (a: ViewMotionCommentSection, b: ViewMotionCommentSection) => {
if (a.weight === b.weight) { if (a.weight === b.weight) {
@ -72,18 +94,6 @@ export class MotionCommentSectionRepositoryService extends BaseRepository<
return this.translate.instant(plural ? 'Comment sections' : 'Comment section'); 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 * Saves a comment made at a MotionCommentSection. Does an update, if
* there is a comment text. Deletes the comment, if the text is empty. * there is a comment text. Deletes the comment, if the text is empty.

View File

@ -5,42 +5,35 @@ import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; 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 { ChangeRecoMode, MotionTitleInformation, ViewMotion } from 'app/site/motions/models/view-motion';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service'; import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { DataSendService } from '../../core-services/data-send.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 { DiffService, DiffLinesInParagraph } from 'app/core/ui-services/diff.service';
import { HttpService } from 'app/core/core-services/http.service'; import { HttpService } from 'app/core/core-services/http.service';
import { LinenumberingService, LineNumberRange } from '../../ui-services/linenumbering.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 { 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 { MotionPoll } from 'app/shared/models/motions/motion-poll';
import { TreeIdNode } from 'app/core/ui-services/tree.service'; 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 { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation';
import { ViewMotionAmendedParagraph } from 'app/site/motions/models/view-motion-amended-paragraph'; import { ViewMotionAmendedParagraph } from 'app/site/motions/models/view-motion-amended-paragraph';
import { ViewUnifiedChange } from 'app/shared/models/motions/view-unified-change'; import { ViewUnifiedChange } from 'app/shared/models/motions/view-unified-change';
import { ViewStatuteParagraph } from 'app/site/motions/models/view-statute-paragraph'; 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 { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewCategory } from 'app/site/motions/models/view-category'; import { ViewCategory } from 'app/site/motions/models/view-category';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
import { ViewWorkflow } from 'app/site/motions/models/view-workflow'; 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 { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { ViewTag } from 'app/site/tags/models/view-tag'; import { ViewTag } from 'app/site/tags/models/view-tag';
import { ViewPersonalNote } from 'app/site/users/models/view-personal-note'; import { ViewPersonalNote } from 'app/site/users/models/view-personal-note';
import { OperatorService } from 'app/core/core-services/operator.service'; import { OperatorService } from 'app/core/core-services/operator.service';
import { PersonalNote, PersonalNoteContent } from 'app/shared/models/users/personal-note'; import { PersonalNoteContent } from 'app/shared/models/users/personal-note';
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 { 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'; type SortProperty = 'weight' | 'identifier';
@ -74,6 +67,84 @@ export interface ParagraphToChoose {
lineTo: number; 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) * Repository Services for motions (and potentially categories)
* *
@ -126,23 +197,36 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
private readonly diff: DiffService, private readonly diff: DiffService,
private operator: OperatorService private operator: OperatorService
) { ) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, Motion, [ super(DS, dataSend, mapperService, viewModelStoreService, translate, Motion, MotionRelations);
Category,
User,
Workflow,
MotionBlock,
Mediafile,
Tag,
MotionChangeRecommendation,
PersonalNote,
Motion
]);
config.get<SortProperty>('motions_motions_sorting').subscribe(conf => { config.get<SortProperty>('motions_motions_sorting').subscribe(conf => {
this.sortProperty = conf; this.sortProperty = conf;
this.setConfigSortFn(); 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) => { public getTitle = (titleInformation: MotionTitleInformation) => {
if (titleInformation.identifier) { if (titleInformation.identifier) {
return titleInformation.identifier + ': ' + titleInformation.title; return titleInformation.identifier + ': ' + titleInformation.title;
@ -183,64 +267,20 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
return this.translate.instant(plural ? 'Motions' : 'Motion'); return this.translate.instant(plural ? 'Motions' : 'Motion');
}; };
/** protected createViewModelWithTitles(model: Motion): ViewMotion {
* Converts a motion to a ViewMotion and adds it to the store. const viewModel = super.createViewModelWithTitles(model);
* viewModel.getIdentifierOrTitle = () => this.getIdentifierOrTitle(viewModel);
* Foreign references of the motion will be resolved (e.g submitters to users) viewModel.getProjectorTitle = () => this.getAgendaSlideTitle(viewModel);
* Expandable to all (server side) changes that might occur on the motion object. return viewModel;
*
* @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;
} }
/** /**
* Get the personal note content for one motion by their id * 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 * @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) { if (this.operator.isAnonymous) {
return; return;
} }
@ -248,64 +288,17 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
const personalNote = this.viewModelStoreService.find(ViewPersonalNote, pn => { const personalNote = this.viewModelStoreService.find(ViewPersonalNote, pn => {
return pn.userId === this.operator.user.id; return pn.userId === this.operator.user.id;
}); });
if (!personalNote) { if (!personalNote) {
return; return;
} }
const notes = personalNote.notes; const notes = personalNote.notes;
const collection = Motion.COLLECTIONSTRING; const collection = Motion.COLLECTIONSTRING;
if (notes && notes[collection] && notes[collection][motionId]) { if (notes && notes[collection] && notes[collection][motion.id]) {
return notes[collection][motionId]; 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 &&
(<ViewPersonalNote>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 * Set the state of a motion
* *
@ -757,20 +750,6 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
.filter((para: ViewMotionAmendedParagraph) => para !== null); .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 * Sends a request to the server, creating a new poll for the motion
*/ */

View File

@ -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();
}));
});

View File

@ -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<ViewState, State, StateTitleInformation> {
/**
* 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');
};
}

View File

@ -51,8 +51,4 @@ export class StatuteParagraphRepositoryService extends BaseRepository<
public getVerboseName = (plural: boolean = false) => { public getVerboseName = (plural: boolean = false) => {
return this.translate.instant(plural ? 'Statute paragraphs' : 'Statute paragraph'); return this.translate.instant(plural ? 'Statute paragraphs' : 'Statute paragraph');
}; };
protected createViewModel(statuteParagraph: StatuteParagraph): ViewStatuteParagraph {
return new ViewStatuteParagraph(statuteParagraph);
}
} }

View File

@ -6,15 +6,29 @@ import { Workflow } from 'app/shared/models/motions/workflow';
import { ViewWorkflow, WorkflowTitleInformation } from 'app/site/motions/models/view-workflow'; import { ViewWorkflow, WorkflowTitleInformation } from 'app/site/motions/models/view-workflow';
import { DataSendService } from '../../core-services/data-send.service'; import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.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 { 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 { 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 { 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 * The repository is meant to process domain objects (those found under
* shared/models), so components can display them and interact with them. * 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' providedIn: 'root'
}) })
export class WorkflowRepositoryService extends BaseRepository<ViewWorkflow, Workflow, WorkflowTitleInformation> { export class WorkflowRepositoryService extends BaseRepository<ViewWorkflow, Workflow, WorkflowTitleInformation> {
/**
* The url to state on rest
*/
private restStateUrl = '/rest/motions/state/';
/** /**
* Creates a WorkflowRepository * Creates a WorkflowRepository
* Converts existing and incoming workflow to ViewWorkflows * Converts existing and incoming workflow to ViewWorkflows
@ -46,16 +55,9 @@ export class WorkflowRepositoryService extends BaseRepository<ViewWorkflow, Work
dataSend: DataSendService, dataSend: DataSendService,
mapperService: CollectionStringMapperService, mapperService: CollectionStringMapperService,
viewModelStoreService: ViewModelStoreService, viewModelStoreService: ViewModelStoreService,
translate: TranslateService, translate: TranslateService
private httpService: HttpService
) { ) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, Workflow); super(DS, dataSend, mapperService, viewModelStoreService, translate, Workflow, WorkflowRelations);
this.viewModelListSubject.subscribe(models => {
if (models && models.length > 0) {
this.initSorting(models);
}
});
} }
public getTitle = (titleInformation: WorkflowTitleInformation) => { public getTitle = (titleInformation: WorkflowTitleInformation) => {
@ -66,86 +68,14 @@ export class WorkflowRepositoryService extends BaseRepository<ViewWorkflow, Work
return this.translate.instant(plural ? 'Workflows' : 'Workflow'); return this.translate.instant(plural ? 'Workflows' : 'Workflow');
}; };
/**
* Sort the states of custom workflows. Ignores simple and complex workflows.
* Implying the default workflows always have the IDs 1 und 2
*
* TODO: Temp Solution. Should be replaced by general sorting over repositories after PR 4411
* This is an abstract to prevent further collisions. Real sorting is then done in 4411
* For now this "just" sorts the Workflow states of all custom workflows
*/
private initSorting(workflows: ViewWorkflow[]): void {
for (const workflow of workflows) {
workflow.sortStates();
}
}
/**
* Creates a ViewWorkflow from a given Workflow
*
* @param workflow the Workflow to convert
*/
protected createViewModel(workflow: Workflow): ViewWorkflow {
return new ViewWorkflow(workflow);
}
/**
* Adds a new state to the given workflow
*
* @param stateName The name of the new Workflow
* @param viewWorkflow The workflow
*/
public async addState(stateName: string, viewWorkflow: ViewWorkflow): Promise<void> {
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<void> {
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<void> {
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 * Returns all workflowStates that cover the list of viewMotions given
* *
* @param motions The motions to get the workflows from * @param motions The motions to get the workflows from
* @returns The workflow states to the given motion * @returns The workflow states to the given motion
*/ */
public getWorkflowStatesForMotions(motions: ViewMotion[]): WorkflowState[] { public getWorkflowStatesForMotions(motions: ViewMotion[]): ViewState[] {
let states: WorkflowState[] = []; let states: ViewState[] = [];
const workflowIds = motions const workflowIds = motions
.map(motion => motion.workflow_id) .map(motion => motion.workflow_id)
.filter((value, index, self) => self.indexOf(value) === index); .filter((value, index, self) => self.indexOf(value) === index);

View File

@ -36,10 +36,6 @@ export class CountdownRepositoryService extends BaseRepository<ViewCountdown, Co
return this.translate.instant(plural ? 'Countdowns' : 'Countdown'); return this.translate.instant(plural ? 'Countdowns' : 'Countdown');
}; };
protected createViewModel(countdown: Countdown): ViewCountdown {
return new ViewCountdown(countdown);
}
/** /**
* Starts a countdown. * Starts a countdown.
* *

View File

@ -52,10 +52,6 @@ export class ProjectionDefaultRepositoryService extends BaseRepository<
return this.translate.instant(titleInformation.display_name); return this.translate.instant(titleInformation.display_name);
}; };
public createViewModel(projectionDefault: ProjectionDefault): ViewProjectionDefault {
return new ViewProjectionDefault(projectionDefault);
}
/** /**
* Creation of projection defaults is not supported. * Creation of projection defaults is not supported.
*/ */

View File

@ -38,8 +38,4 @@ export class ProjectorMessageRepositoryService extends BaseRepository<
public getVerboseName = (plural: boolean = false) => { public getVerboseName = (plural: boolean = false) => {
return this.translate.instant(plural ? 'Messages' : 'Message'); return this.translate.instant(plural ? 'Messages' : 'Message');
}; };
protected createViewModel(message: ProjectorMessage): ViewProjectorMessage {
return new ViewProjectorMessage(message);
}
} }

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/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 { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { DataSendService } from '../../core-services/data-send.service'; import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service'; import { DataStoreService } from '../../core-services/data-store.service';
@ -21,6 +21,15 @@ export enum ScrollScaleDirection {
Reset = 'reset' Reset = 'reset'
} }
const ProjectorRelations: RelationDefinition[] = [
{
type: 'O2M',
ownIdKey: 'reference_projector_id',
ownKey: 'referenceProjector',
foreignModel: ViewProjector
}
];
/** /**
* Manages all projector instances. * Manages all projector instances.
*/ */
@ -44,7 +53,7 @@ export class ProjectorRepositoryService extends BaseRepository<ViewProjector, Pr
translate: TranslateService, translate: TranslateService,
private http: HttpService private http: HttpService
) { ) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, Projector, [Projector]); super(DS, dataSend, mapperService, viewModelStoreService, translate, Projector, ProjectorRelations);
} }
public getTitle = (titleInformation: ProjectorTitleInformation) => { public getTitle = (titleInformation: ProjectorTitleInformation) => {
@ -55,10 +64,6 @@ export class ProjectorRepositoryService extends BaseRepository<ViewProjector, Pr
return this.translate.instant(plural ? 'Projectors' : 'Projector'); return this.translate.instant(plural ? 'Projectors' : 'Projector');
}; };
public createViewModel(projector: Projector): ViewProjector {
return new ViewProjector(projector);
}
/** /**
* Creates a new projector. Adds the clock as default, stable element * Creates a new projector. Adds the clock as default, stable element
*/ */

View File

@ -52,10 +52,6 @@ export class TagRepositoryService extends BaseRepository<ViewTag, Tag, TagTitleI
return this.translate.instant(plural ? 'Tags' : 'Tag'); return this.translate.instant(plural ? 'Tags' : 'Tag');
}; };
protected createViewModel(tag: Tag): ViewTag {
return new ViewTag(tag);
}
/** /**
* Sets the default sorting (e.g. in dropdowns and for new users) to 'name' * Sets the default sorting (e.g. in dropdowns and for new users) to 'name'
*/ */

View File

@ -5,14 +5,21 @@ import { TranslateService } from '@ngx-translate/core';
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service'; import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
import { DataStoreService } from 'app/core/core-services/data-store.service'; import { DataStoreService } from 'app/core/core-services/data-store.service';
import { DataSendService } from 'app/core/core-services/data-send.service'; import { DataSendService } from 'app/core/core-services/data-send.service';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
import { Topic } from 'app/shared/models/topics/topic'; import { Topic } from 'app/shared/models/topics/topic';
import { ViewTopic, TopicTitleInformation } from 'app/site/topics/models/view-topic'; import { ViewTopic, TopicTitleInformation } from 'app/site/topics/models/view-topic';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
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'; import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../base-is-agenda-item-and-list-of-speakers-content-object-repository';
import { RelationDefinition } from '../base-repository';
const TopicRelations: RelationDefinition[] = [
{
type: 'M2M',
ownIdKey: 'attachments_id',
ownKey: 'attachments',
foreignModel: ViewMediafile
}
];
/** /**
* Repository for topics * Repository for topics
@ -39,7 +46,7 @@ export class TopicRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCon
viewModelStoreService: ViewModelStoreService, viewModelStoreService: ViewModelStoreService,
translate: TranslateService translate: TranslateService
) { ) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, Topic, [Mediafile]); super(DS, dataSend, mapperService, viewModelStoreService, translate, Topic, TopicRelations);
} }
public getTitle = (titleInformation: TopicTitleInformation) => { public getTitle = (titleInformation: TopicTitleInformation) => {
@ -63,29 +70,4 @@ export class TopicRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCon
public getVerboseName = (plural: boolean = false) => { public getVerboseName = (plural: boolean = false) => {
return this.translate.instant(plural ? 'Topics' : 'Topic'); 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;
}
} }

View File

@ -70,10 +70,6 @@ export class GroupRepositoryService extends BaseRepository<ViewGroup, Group, Gro
return this.translate.instant(plural ? 'Groups' : 'Group'); return this.translate.instant(plural ? 'Groups' : 'Group');
}; };
public createViewModel(group: Group): ViewGroup {
return new ViewGroup(group);
}
/** /**
* Toggles the given permisson. * Toggles the given permisson.
* *

View File

@ -43,10 +43,6 @@ export class PersonalNoteRepositoryService extends BaseRepository<
return this.translate.instant(plural ? 'Personal notes' : 'Personal note'); return this.translate.instant(plural ? 'Personal notes' : 'Personal note');
}; };
protected createViewModel(personalNote: PersonalNote): ViewPersonalNote {
return new ViewPersonalNote(personalNote);
}
/** /**
* Overwrite the default procedure * Overwrite the default procedure
* *

View File

@ -2,13 +2,12 @@ import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/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 { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { DataSendService } from '../../core-services/data-send.service'; import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service'; import { DataStoreService } from '../../core-services/data-store.service';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { Group } from 'app/shared/models/users/group';
import { HttpService } from 'app/core/core-services/http.service'; import { HttpService } from 'app/core/core-services/http.service';
import { NewEntry } from 'app/core/ui-services/base-import.service'; import { NewEntry } from 'app/core/ui-services/base-import.service';
import { User } from 'app/shared/models/users/user'; import { User } from 'app/shared/models/users/user';
@ -24,6 +23,15 @@ type StringNamingSchema = 'lastCommaFirst' | 'firstSpaceLast';
type SortProperty = 'first_name' | 'last_name' | 'number'; type SortProperty = 'first_name' | 'last_name' | 'number';
const UserRelations: RelationDefinition[] = [
{
type: 'M2M',
ownIdKey: 'groups_id',
ownKey: 'groups',
foreignModel: ViewGroup
}
];
/** /**
* Repository service for users * Repository service for users
* *
@ -57,7 +65,7 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
private httpService: HttpService, private httpService: HttpService,
private configService: ConfigService private configService: ConfigService
) { ) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, User, [Group]); super(DS, dataSend, mapperService, viewModelStoreService, translate, User, UserRelations);
this.sortProperty = this.configService.instant('users_sort_by'); this.sortProperty = this.configService.instant('users_sort_by');
this.configService.get<SortProperty>('users_sort_by').subscribe(conf => { this.configService.get<SortProperty>('users_sort_by').subscribe(conf => {
this.sortProperty = conf; this.sortProperty = conf;
@ -133,12 +141,14 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
return pw; return pw;
} }
public createViewModel(user: User): ViewUser { /**
const groups = this.viewModelStoreService.getMany(ViewGroup, user.groups_id); * Adds teh short and full name to the view user.
const viewUser = new ViewUser(user, groups); */
viewUser.getFullName = () => this.getFullName(viewUser); protected createViewModelWithTitles(model: User): ViewUser {
viewUser.getShortName = () => this.getShortName(viewUser); const viewModel = super.createViewModelWithTitles(model);
return viewUser; viewModel.getFullName = () => this.getFullName(viewModel);
viewModel.getShortName = () => this.getShortName(viewModel);
return viewModel;
} }
/** /**

View File

@ -25,7 +25,7 @@ export interface NewEntry<V> {
newEntry: V; newEntry: V;
status: CsvImportStatus; status: CsvImportStatus;
errors: string[]; errors: string[];
duplicates: V[]; hasDuplicates: boolean;
importTrackId?: number; importTrackId?: number;
} }
@ -259,11 +259,11 @@ export abstract class BaseImportService<V extends BaseViewModel> {
if (entry.status === 'done') { if (entry.status === 'done') {
summary.done += 1; summary.done += 1;
return; return;
} else if (entry.status === 'error' && !entry.duplicates.length) { } else if (entry.status === 'error' && !entry.hasDuplicates) {
// errors that are not due to duplicates // errors that are not due to duplicates
summary.errors += 1; summary.errors += 1;
return; return;
} else if (entry.duplicates.length) { } else if (entry.hasDuplicates) {
summary.duplicates += 1; summary.duplicates += 1;
return; return;
} else if (entry.status === 'new') { } else if (entry.status === 'new') {

View File

@ -1,3 +1,5 @@
import { BaseModel } from '../base/base-model';
/** /**
* Representation of a speaker in an agenda item. * Representation of a speaker in an agenda item.
* *
@ -5,23 +7,28 @@
* Part of the 'speakers' list. * Part of the 'speakers' list.
* @ignore * @ignore
*/ */
export interface Speaker { export class Speaker extends BaseModel<Speaker> {
id: number; public static COLLECTIONSTRING = 'agenda/speaker';
user_id: number;
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 * ISO datetime string to indicate the begin time of the speech. Empty if
* the speaker has not started * 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 * ISO datetime string to indicate the end time of the speech. Empty if the
* speech has not ended * speech has not ended
*/ */
end_time: string; public end_time: string;
weight: number; public constructor(input?: any) {
marked: boolean; super(Speaker.COLLECTIONSTRING, input);
item_id: number; }
} }

View File

@ -1,5 +1,5 @@
import { Deserializer } from '../base/deserializer';
import { PollVoteValue } from 'app/core/ui-services/poll.service'; import { PollVoteValue } from 'app/core/ui-services/poll.service';
import { BaseModel } from '../base/base-model';
export interface AssignmentOptionVote { export interface AssignmentOptionVote {
weight: number; weight: number;
@ -12,7 +12,9 @@ export interface AssignmentOptionVote {
* part of the 'polls-options'-array in poll * part of the 'polls-options'-array in poll
* @ignore * @ignore
*/ */
export class AssignmentPollOption extends Deserializer { export class AssignmentPollOption extends BaseModel<AssignmentPollOption> {
public static COLLECTIONSTRING = 'assignments/assignment-poll-option';
public id: number; // The AssignmentUser id of the candidate public id: number; // The AssignmentUser id of the candidate
public candidate_id: number; // the User id of the candidate public candidate_id: number; // the User id of the candidate
public is_elected: boolean; public is_elected: boolean;
@ -21,8 +23,6 @@ export class AssignmentPollOption extends Deserializer {
public weight: number; // weight to order the display public weight: number; // weight to order the display
/** /**
* Needs to be completely optional because poll has (yet) the optional parameter 'poll-options'
*
* @param input * @param input
*/ */
public constructor(input?: any) { public constructor(input?: any) {
@ -33,6 +33,6 @@ export class AssignmentPollOption extends Deserializer {
} }
}); });
} }
super(input); super(AssignmentPollOption.COLLECTIONSTRING, input);
} }
} }

View File

@ -1,12 +1,13 @@
import { AssignmentPollMethod } from 'app/site/assignments/services/assignment-poll.service'; import { AssignmentPollMethod } from 'app/site/assignments/services/assignment-poll.service';
import { Deserializer } from '../base/deserializer';
import { AssignmentPollOption } from './assignment-poll-option'; import { AssignmentPollOption } from './assignment-poll-option';
import { BaseModel } from '../base/base-model';
/** /**
* Content of the 'polls' property of assignments * Content of the 'polls' property of assignments
* @ignore * @ignore
*/ */
export class AssignmentPoll extends Deserializer { export class AssignmentPoll extends BaseModel<AssignmentPoll> {
public static COLLECTIONSTRING = 'assignments/assignment-poll';
private static DECIMAL_FIELDS = ['votesvalid', 'votesinvalid', 'votescast', 'votesno', 'votesabstain']; private static DECIMAL_FIELDS = ['votesvalid', 'votesinvalid', 'votescast', 'votesno', 'votesabstain'];
public id: number; public id: number;
@ -23,7 +24,6 @@ export class AssignmentPoll extends Deserializer {
public assignment_id: number; public assignment_id: number;
/** /**
* Needs to be completely optional because assignment has (yet) the optional parameter 'polls'
* @param input * @param input
*/ */
public constructor(input?: any) { public constructor(input?: any) {
@ -35,14 +35,6 @@ export class AssignmentPoll extends Deserializer {
} }
}); });
} }
super(input); super(AssignmentPoll.COLLECTIONSTRING, 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));
}
} }
} }

View File

@ -1,27 +1,18 @@
import { BaseModel } from '../base/base-model';
/** /**
* Content of the 'assignment_related_users' property. * Content of the 'assignment_related_users' property.
*/ */
export interface AssignmentRelatedUser { export class AssignmentRelatedUser extends BaseModel<AssignmentRelatedUser> {
id: number; public static COLLECTIONSTRING = 'assignments/assignment-related-user';
/** public id: number;
* id of the user this assignment user relates to public user_id: number;
*/ public elected: boolean;
user_id: number; public assignment_id: number;
public weight: number;
/** public constructor(input?: any) {
* The current 'elected' state super(AssignmentRelatedUser.COLLECTIONSTRING, input);
*/ }
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;
} }

View File

@ -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);
}
}
}

View File

@ -1,4 +1,4 @@
import { MotionSubmitter } from './motion-submitter'; import { Submitter } from './submitter';
import { MotionPoll } from './motion-poll'; import { MotionPoll } from './motion-poll';
import { BaseModelWithAgendaItemAndListOfSpeakers } from '../base/base-model-with-agenda-item-and-list-of-speakers'; import { BaseModelWithAgendaItemAndListOfSpeakers } from '../base/base-model-with-agenda-item-and-list-of-speakers';
@ -31,7 +31,7 @@ export class Motion extends BaseModelWithAgendaItemAndListOfSpeakers<Motion> {
public category_weight: number; public category_weight: number;
public motion_block_id: number; public motion_block_id: number;
public origin: string; public origin: string;
public submitters: MotionSubmitter[]; public submitters: Submitter[];
public supporters_id: number[]; public supporters_id: number[];
public comments: MotionComment[]; public comments: MotionComment[];
public workflow_id: number; public workflow_id: number;
@ -48,6 +48,7 @@ export class Motion extends BaseModelWithAgendaItemAndListOfSpeakers<Motion> {
public sort_parent_id: number; public sort_parent_id: number;
public created: string; public created: string;
public last_modified: string; public last_modified: string;
public change_recommendations_id: number[];
public constructor(input?: any) { public constructor(input?: any) {
super(Motion.COLLECTIONSTRING, input); super(Motion.COLLECTIONSTRING, input);
@ -58,9 +59,9 @@ export class Motion extends BaseModelWithAgendaItemAndListOfSpeakers<Motion> {
*/ */
public get sorted_submitters_id(): number[] { public get sorted_submitters_id(): number[] {
return this.submitters return this.submitters
.sort((a: MotionSubmitter, b: MotionSubmitter) => { .sort((a: Submitter, b: Submitter) => {
return a.weight - b.weight; return a.weight - b.weight;
}) })
.map((submitter: MotionSubmitter) => submitter.user_id); .map((submitter: Submitter) => submitter.user_id);
} }
} }

View File

@ -1,5 +1,4 @@
import { Deserializer } from '../base/deserializer'; import { BaseModel } from '../base/base-model';
import { Workflow } from './workflow';
/** /**
* Specifies if an amendment of this state/recommendation should be merged into the motion * 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 * Part of the 'states'-array in motion/workflow
* @ignore * @ignore
*/ */
export class WorkflowState extends Deserializer { export class State extends BaseModel<State> {
public static COLLECTIONSTRING = 'motions/state';
public id: number; public id: number;
public name: string; public name: string;
public recommendation_label: string; public recommendation_label: string;
@ -37,23 +38,7 @@ export class WorkflowState extends Deserializer {
* @param input If given, it will be deserialized * @param input If given, it will be deserialized
*/ */
public constructor(input?: any) { public constructor(input?: any) {
super(input); super(State.COLLECTIONSTRING, 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);
});
} }
public toString = (): string => { public toString = (): string => {

View File

@ -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);
}
}

View File

@ -1,5 +1,4 @@
import { BaseModel } from '../base/base-model'; import { BaseModel } from '../base/base-model';
import { WorkflowState } from './workflow-state';
/** /**
* Representation of a motion workflow. Has the nested property 'states' * Representation of a motion workflow. Has the nested property 'states'
@ -10,40 +9,10 @@ export class Workflow extends BaseModel<Workflow> {
public id: number; public id: number;
public name: string; public name: string;
public states: WorkflowState[]; public states_id: number[];
public first_state_id: number; public first_state_id: number;
public constructor(input?: any) { public constructor(input?: any) {
super(Workflow.COLLECTIONSTRING, input); 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));
}
}
} }

View File

@ -1,8 +1,5 @@
import { Item, ItemVisibilityChoices } from 'app/shared/models/agenda/item'; import { Item, ItemVisibilityChoices } from 'app/shared/models/agenda/item';
import { import { BaseViewModelWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item';
BaseViewModelWithAgendaItem,
isBaseViewModelWithAgendaItem
} from 'app/site/base/base-view-model-with-agenda-item';
import { BaseViewModelWithContentObject } from 'app/site/base/base-view-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 { ContentObject } from 'app/shared/models/base/content-object';
@ -87,7 +84,7 @@ export class ViewItem extends BaseViewModelWithContentObject<Item, BaseViewModel
return this.item.parent_id; return this.item.parent_id;
} }
public constructor(item: Item, contentObject?: BaseViewModelWithAgendaItem) { public constructor(item: Item) {
super(Item.COLLECTIONSTRING, item, isBaseViewModelWithAgendaItem, 'BaseViewModelWithAgendaItem', contentObject); super(Item.COLLECTIONSTRING, item);
} }
} }

View File

@ -1,13 +1,8 @@
import { BaseViewModel } from '../../base/base-view-model';
import { Item } from 'app/shared/models/agenda/item'; import { Item } from 'app/shared/models/agenda/item';
import { ProjectorElementBuildDeskriptor, Projectable } from 'app/site/base/projectable'; import { ProjectorElementBuildDeskriptor, Projectable } from 'app/site/base/projectable';
import { ListOfSpeakers } from 'app/shared/models/agenda/list-of-speakers'; import { ListOfSpeakers } from 'app/shared/models/agenda/list-of-speakers';
import { ViewSpeaker, SpeakerState } from './view-speaker'; import { ViewSpeaker, SpeakerState } from './view-speaker';
import { import { BaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers';
BaseViewModelWithListOfSpeakers,
isBaseViewModelWithListOfSpeakers
} from 'app/site/base/base-view-model-with-list-of-speakers';
import { ViewUser } from 'app/site/users/models/view-user';
import { BaseViewModelWithContentObject } from 'app/site/base/base-view-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 { ContentObject } from 'app/shared/models/base/content-object';
@ -31,7 +26,7 @@ export class ViewListOfSpeakers extends BaseViewModelWithContentObject<ListOfSpe
} }
public get speakers(): ViewSpeaker[] { public get speakers(): ViewSpeaker[] {
return this._speakers; return this._speakers || [];
} }
public get title_information(): object { public get title_information(): object {
@ -53,19 +48,8 @@ export class ViewListOfSpeakers extends BaseViewModelWithContentObject<ListOfSpe
return `/agenda/speakers/${this.id}`; return `/agenda/speakers/${this.id}`;
} }
public constructor( public constructor(listOfSpeakers: ListOfSpeakers) {
listOfSpeakers: ListOfSpeakers, super(Item.COLLECTIONSTRING, listOfSpeakers);
speakers: ViewSpeaker[],
contentObject?: BaseViewModelWithListOfSpeakers
) {
super(
Item.COLLECTIONSTRING,
listOfSpeakers,
isBaseViewModelWithListOfSpeakers,
'BaseViewModelWithListOfSpeakers',
contentObject
);
this._speakers = speakers;
} }
public getProjectorTitle(): string { public getProjectorTitle(): string {
@ -84,12 +68,4 @@ export class ViewListOfSpeakers extends BaseViewModelWithContentObject<ListOfSpe
getDialogTitle: () => this.getTitle() getDialogTitle: () => 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;
}
} }

View File

@ -1,8 +1,6 @@
import { BaseViewModel } from 'app/site/base/base-view-model'; import { BaseViewModel } from 'app/site/base/base-view-model';
import { Speaker } from 'app/shared/models/agenda/speaker'; import { Speaker } from 'app/shared/models/agenda/speaker';
import { ViewUser } from 'app/site/users/models/view-user'; 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 * Determine the state of the speaker
@ -16,15 +14,15 @@ export enum SpeakerState {
/** /**
* Provides "safe" access to a speaker with all it's components * Provides "safe" access to a speaker with all it's components
*/ */
export class ViewSpeaker implements Updateable, Identifiable { export class ViewSpeaker extends BaseViewModel<Speaker> {
private _speaker: Speaker; public static COLLECTIONSTRING = Speaker.COLLECTIONSTRING;
private _user?: ViewUser; private _user?: ViewUser;
public get speaker(): Speaker { public get speaker(): Speaker {
return this._speaker; return this._model;
} }
public get user(): ViewUser { public get user(): ViewUser | null {
return this._user; return this._user;
} }
@ -82,20 +80,11 @@ export class ViewSpeaker implements Updateable, Identifiable {
return this.user ? this.user.gender : ''; return this.user ? this.user.gender : '';
} }
public constructor(speaker: Speaker, user?: ViewUser) { public constructor(speaker: Speaker) {
this._speaker = speaker; super(Speaker.COLLECTIONSTRING, speaker);
this._user = user;
} }
public getTitle = () => { public getTitle = () => {
return this.name; 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;
}
} }

View File

@ -100,7 +100,7 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
newEntry[this.expectedHeader[idx]] = line[idx]; 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 // set type to 'public' if none is given in import
if (!newEntry.type) { if (!newEntry.type) {
@ -108,12 +108,11 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
} }
const mappedEntry: NewEntry<ViewCreateTopic> = { const mappedEntry: NewEntry<ViewCreateTopic> = {
newEntry: newEntry, newEntry: newEntry,
duplicates: [], hasDuplicates: hasDuplicates,
status: 'new', status: 'new',
errors: [] errors: []
}; };
if (updateModels.length) { if (hasDuplicates) {
mappedEntry.duplicates = updateModels;
this.setError(mappedEntry, 'Duplicates'); this.setError(mappedEntry, 'Duplicates');
} }
if (hasErrors) { if (hasErrors) {
@ -198,17 +197,14 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
agenda_type: 1 // set type to 'public item' by default agenda_type: 1 // set type to 'public item' by default
}) })
); );
const hasDuplicates = this.repo.getViewModelList().some(topic => topic.title === newTopic.title);
const newEntry: NewEntry<ViewCreateTopic> = { const newEntry: NewEntry<ViewCreateTopic> = {
newEntry: newTopic, newEntry: newTopic,
duplicates: [], hasDuplicates: hasDuplicates,
status: 'new', status: 'new',
errors: [] errors: []
}; };
const duplicates = this.repo.getTopicDuplicates(newTopic); if (hasDuplicates) {
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[];
this.setError(newEntry, 'Duplicates'); this.setError(newEntry, 'Duplicates');
} }
newEntries.push(newEntry); newEntries.push(newEntry);

View File

@ -162,10 +162,10 @@
<div> <div>
<div <div
class="candidates-list" class="candidates-list"
*ngIf="assignment && assignment.assignmentRelatedUsers && assignment.assignmentRelatedUsers.length > 0" *ngIf="assignment && assignment.assignment_related_users && assignment.assignment_related_users.length > 0"
> >
<os-sorting-list <os-sorting-list
[input]="assignment.assignmentRelatedUsers" [input]="assignment.assignment_related_users"
[live]="true" [live]="true"
[count]="true" [count]="true"
[enable]="hasPerms('addOthers')" [enable]="hasPerms('addOthers')"

View File

@ -388,7 +388,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
super.setTitle('New election'); super.setTitle('New election');
this.newAssignment = true; this.newAssignment = true;
// TODO set defaults? // TODO set defaults?
this.assignment = new ViewAssignment(new Assignment(), [], []); this.assignment = new ViewAssignment(new Assignment());
this.patchForm(this.assignment); this.patchForm(this.assignment);
this.setEditMode(true); this.setEditMode(true);
} }

View File

@ -52,7 +52,7 @@ export class AssignmentListComponent extends BaseListViewComponent<ViewAssignmen
/** /**
* Define extra filter properties * Define extra filter properties
*/ */
public filterProps = ['title', 'candidates', 'assignmentRelatedUsers', 'tags', 'candidateAmount']; public filterProps = ['title', 'candidates', 'assignment_related_users', 'tags', 'candidateAmount'];
/** /**
* Constructor. * Constructor.

View File

@ -169,7 +169,8 @@
<div *ngIf="!pollData"> <div *ngIf="!pollData">
<h4 translate>Candidates</h4> <h4 translate>Candidates</h4>
<div *ngFor="let option of poll.options"> <div *ngFor="let option of poll.options">
<span class="accent"> {{ option.user.full_name }}</span> <span class="accent" *ngIf="option.user">{{ option.user.getFullName() }}</span>
<span *ngIf="!option.user">No user {{ option.user_id }}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -236,7 +236,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
} }
// TODO additional conditions: assignment not finished? // 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 user => user.user_id === option.user_id
); );
if (viewAssignmentRelatedUser) { if (viewAssignmentRelatedUser) {

View File

@ -1,8 +1,6 @@
import { AssignmentPollOption, AssignmentOptionVote } from 'app/shared/models/assignments/assignment-poll-option'; import { AssignmentPollOption, AssignmentOptionVote } from 'app/shared/models/assignments/assignment-poll-option';
import { BaseViewModel } from 'app/site/base/base-view-model'; 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 { PollVoteValue } from 'app/core/ui-services/poll.service';
import { Updateable } from 'app/site/base/updateable';
import { ViewUser } from 'app/site/users/models/view-user'; 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']; const votesOrder: PollVoteValue[] = ['Votes', 'Yes', 'No', 'Abstain'];
export class ViewAssignmentPollOption implements Identifiable, Updateable { export class ViewAssignmentPollOption extends BaseViewModel<AssignmentPollOption> {
private _assignmentPollOption: AssignmentPollOption; public static COLLECTIONSTRING = AssignmentPollOption.COLLECTIONSTRING;
private _user: ViewUser; // This is the "candidate". We'll stay consistent wich user here... private _user?: ViewUser; // This is the "candidate". We'll stay consistent wich user here...
public get option(): AssignmentPollOption { public get option(): AssignmentPollOption {
return this._assignmentPollOption; return this._model;
} }
/** /**
@ -52,14 +50,7 @@ export class ViewAssignmentPollOption implements Identifiable, Updateable {
return this.option.weight; return this.option.weight;
} }
public constructor(assignmentPollOption: AssignmentPollOption, user?: ViewUser) { public constructor(assignmentPollOption: AssignmentPollOption) {
this._assignmentPollOption = assignmentPollOption; super(AssignmentPollOption.COLLECTIONSTRING, assignmentPollOption);
this._user = user;
}
public updateDependencies(update: BaseViewModel): void {
if (update instanceof ViewUser && update.id === this.user_id) {
this._user = update;
}
} }
} }

View File

@ -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 { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
import { AssignmentPollMethod } from '../services/assignment-poll.service'; import { AssignmentPollMethod } from '../services/assignment-poll.service';
import { ViewAssignmentPollOption } from './view-assignment-poll-option'; 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 { AssignmentPollOption } from 'app/shared/models/assignments/assignment-poll-option';
import { Projectable, ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
export class ViewAssignmentPoll implements Identifiable, Updateable, Projectable { export class ViewAssignmentPoll extends BaseProjectableViewModel<AssignmentPoll> {
private _assignmentPoll: AssignmentPoll; public static COLLECTIONSTRING = AssignmentPoll.COLLECTIONSTRING;
private _assignmentPollOptions: ViewAssignmentPollOption[]; private _options: ViewAssignmentPollOption[];
public get poll(): AssignmentPoll { public get poll(): AssignmentPoll {
return this._assignmentPoll; return this._model;
} }
public get options(): ViewAssignmentPollOption[] { public get options(): ViewAssignmentPollOption[] {
return this._assignmentPollOptions; return this._options;
} }
public get id(): number { public get id(): number {
@ -78,43 +76,38 @@ export class ViewAssignmentPoll implements Identifiable, Updateable, Projectable
return this.poll.assignment_id; return this.poll.assignment_id;
} }
public constructor(assignmentPoll: AssignmentPoll, assignmentPollOptions: ViewAssignmentPollOption[]) { public constructor(assignmentPoll: AssignmentPoll) {
this._assignmentPoll = assignmentPoll; super(AssignmentPoll.COLLECTIONSTRING, assignmentPoll);
this._assignmentPollOptions = assignmentPollOptions;
} }
public updateDependencies(update: BaseViewModel): void { public getTitle = () => {
this.options.forEach(option => option.updateDependencies(update)); return 'Poll';
} };
public getTitle(): string { public getListTitle = () => {
return 'TODO';
}
public getListTitle(): string {
return this.getTitle(); return this.getTitle();
} };
public getProjectorTitle(): string { public getProjectorTitle = () => {
return this.getTitle(); return this.getTitle();
} };
/** /**
* Creates a copy with deep-copy on all changing numerical values, * Creates a copy with deep-copy on all changing numerical values,
* but intact uncopied references to the users * 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 { public copy(): ViewAssignmentPoll {
return new ViewAssignmentPoll( const poll = new ViewAssignmentPoll(new AssignmentPoll(JSON.parse(JSON.stringify(this.poll))));
new AssignmentPoll(JSON.parse(JSON.stringify(this._assignmentPoll))), (<any>poll)._options = this.options.map(option => {
this._assignmentPollOptions.map(option => { const polloption = new ViewAssignmentPollOption(
return new ViewAssignmentPollOption( new AssignmentPollOption(JSON.parse(JSON.stringify(option.option)))
new AssignmentPollOption(JSON.parse(JSON.stringify(option.option))), );
option.user (<any>polloption)._user = option.user;
); return polloption;
}) });
); return poll;
} }
public getSlide(): ProjectorElementBuildDeskriptor { public getSlide(): ProjectorElementBuildDeskriptor {

View File

@ -1,16 +1,14 @@
import { AssignmentRelatedUser } from 'app/shared/models/assignments/assignment-related-user'; import { AssignmentRelatedUser } from 'app/shared/models/assignments/assignment-related-user';
import { BaseViewModel } from 'app/site/base/base-view-model'; 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'; import { ViewUser } from 'app/site/users/models/view-user';
export class ViewAssignmentRelatedUser implements Updateable, Identifiable, Displayable { export class ViewAssignmentRelatedUser extends BaseViewModel<AssignmentRelatedUser> {
private _assignmentRelatedUser: AssignmentRelatedUser; public static COLLECTIONSTRING = AssignmentRelatedUser.COLLECTIONSTRING;
private _user?: ViewUser; private _user?: ViewUser;
public get assignmentRelatedUser(): AssignmentRelatedUser { public get assignmentRelatedUser(): AssignmentRelatedUser {
return this._assignmentRelatedUser; return this._model;
} }
public get user(): ViewUser { public get user(): ViewUser {
@ -37,22 +35,13 @@ export class ViewAssignmentRelatedUser implements Updateable, Identifiable, Disp
return this.assignmentRelatedUser.weight; return this.assignmentRelatedUser.weight;
} }
public constructor(assignmentRelatedUser: AssignmentRelatedUser, user?: ViewUser) { public getListTitle: () => string = this.getTitle;
this._assignmentRelatedUser = assignmentRelatedUser;
this._user = user; public constructor(assignmentRelatedUser: AssignmentRelatedUser) {
super(AssignmentRelatedUser.COLLECTIONSTRING, assignmentRelatedUser);
} }
public updateDependencies(update: BaseViewModel): void { public getTitle: () => string = () => {
if (update instanceof ViewUser && update.id === this.user_id) { return this.user ? this.user.getFullName() : '';
this._user = update; };
}
}
public getTitle(): string {
return this.user ? this.user.getTitle() : '';
}
public getListTitle(): string {
return this.getTitle();
}
} }

View File

@ -2,14 +2,11 @@ import { Assignment } from 'app/shared/models/assignments/assignment';
import { SearchRepresentation } from 'app/core/ui-services/search.service'; import { SearchRepresentation } from 'app/core/ui-services/search.service';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { ViewUser } from 'app/site/users/models/view-user'; 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 { 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 { ViewAssignmentRelatedUser } from './view-assignment-related-user';
import { ViewAssignmentPoll } from './view-assignment-poll'; import { ViewAssignmentPoll } from './view-assignment-poll';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; 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 { 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'; import { TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item';
export interface AssignmentTitleInformation extends TitleInformationWithAgendaItem { export interface AssignmentTitleInformation extends TitleInformationWithAgendaItem {
@ -43,8 +40,8 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers
implements AssignmentTitleInformation { implements AssignmentTitleInformation {
public static COLLECTIONSTRING = Assignment.COLLECTIONSTRING; public static COLLECTIONSTRING = Assignment.COLLECTIONSTRING;
private _assignmentRelatedUsers: ViewAssignmentRelatedUser[]; private _assignment_related_users?: ViewAssignmentRelatedUser[];
private _assignmentPolls: ViewAssignmentPoll[]; private _polls?: ViewAssignmentPoll[];
private _tags?: ViewTag[]; private _tags?: ViewTag[];
private _attachments?: ViewMediafile[]; private _attachments?: ViewMediafile[];
@ -53,7 +50,7 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers
} }
public get polls(): ViewAssignmentPoll[] { public get polls(): ViewAssignmentPoll[] {
return this._assignmentPolls; return this._polls || [];
} }
public get title(): string { public get title(): string {
@ -69,21 +66,29 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers
} }
public get candidates(): ViewUser[] { 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[] { public get assignment_related_users(): ViewAssignmentRelatedUser[] {
return this._assignmentRelatedUsers; return this._assignment_related_users || [];
} }
public get tags(): ViewTag[] { public get tags(): ViewTag[] {
return this._tags || []; return this._tags || [];
} }
public get tags_id(): number[] {
return this.assignment.tags_id;
}
public get attachments(): ViewMediafile[] { public get attachments(): ViewMediafile[] {
return this._attachments || []; return this._attachments || [];
} }
public get attachments_id(): number[] {
return this.assignment.attachments_id;
}
/** /**
* unknown where the identifier to the phase is get * 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 * @returns the amount of candidates in the assignment's candidate list
*/ */
public get candidateAmount(): number { public get candidateAmount(): number {
return this._assignmentRelatedUsers ? this._assignmentRelatedUsers.length : 0; return this._assignment_related_users ? this._assignment_related_users.length : 0;
} }
public constructor( public constructor(assignment: Assignment) {
assignment: Assignment, super(Assignment.COLLECTIONSTRING, 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 formatForSearch(): SearchRepresentation { public formatForSearch(): SearchRepresentation {

View File

@ -127,7 +127,7 @@ export class AssignmentPdfService {
*/ */
private createCandidateList(assignment: ViewAssignment): object { private createCandidateList(assignment: ViewAssignment): object {
if (assignment.phase !== 2) { 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 candidatesText = `${this.translate.instant('Candidates')}: `;
const userList = candidates.map(candidate => { const userList = candidates.map(candidate => {

View File

@ -191,10 +191,7 @@ export abstract class BaseImportListComponent<V extends BaseViewModel> extends B
}; };
} else if (this.shown === 'error') { } else if (this.shown === 'error') {
this.dataSource.filterPredicate = (data, filter) => { this.dataSource.filterPredicate = (data, filter) => {
if (data.errors.length || data.duplicates.length) { return !!data.errors.length || data.hasDuplicates;
return true;
}
return false;
}; };
} }
this.dataSource.filter = 'X'; // TODO: This is just a bogus non-null string to trigger the filter this.dataSource.filter = 'X'; // TODO: This is just a bogus non-null string to trigger the filter

View File

@ -8,7 +8,6 @@ import {
isBaseViewModelWithListOfSpeakers, isBaseViewModelWithListOfSpeakers,
IBaseViewModelWithListOfSpeakers IBaseViewModelWithListOfSpeakers
} from './base-view-model-with-list-of-speakers'; } from './base-view-model-with-list-of-speakers';
import { BaseViewModel } from './base-view-model';
export function isBaseViewModelWithAgendaItemAndListOfSpeakers( export function isBaseViewModelWithAgendaItemAndListOfSpeakers(
obj: any obj: any
@ -23,7 +22,7 @@ export abstract class BaseViewModelWithAgendaItemAndListOfSpeakers<
M extends BaseModelWithAgendaItemAndListOfSpeakers = any M extends BaseModelWithAgendaItemAndListOfSpeakers = any
> extends BaseProjectableViewModel implements IBaseViewModelWithAgendaItem<M>, IBaseViewModelWithListOfSpeakers<M> { > extends BaseProjectableViewModel implements IBaseViewModelWithAgendaItem<M>, IBaseViewModelWithListOfSpeakers<M> {
protected _item?: ViewItem; protected _item?: ViewItem;
protected _listOfSpeakers?: ViewListOfSpeakers; protected _list_of_speakers?: ViewListOfSpeakers;
public get agendaItem(): ViewItem | null { public get agendaItem(): ViewItem | null {
return this._item; return this._item;
@ -38,7 +37,7 @@ export abstract class BaseViewModelWithAgendaItemAndListOfSpeakers<
} }
public get listOfSpeakers(): ViewListOfSpeakers | null { public get listOfSpeakers(): ViewListOfSpeakers | null {
return this._listOfSpeakers; return this._list_of_speakers;
} }
public get list_of_speakers_id(): number { public get list_of_speakers_id(): number {
@ -50,11 +49,8 @@ export abstract class BaseViewModelWithAgendaItemAndListOfSpeakers<
public getListOfSpeakersTitle: () => string; public getListOfSpeakersTitle: () => string;
public getListOfSpeakersSlideTitle: () => string; public getListOfSpeakersSlideTitle: () => string;
public constructor(collectionString: string, model: M, item?: ViewItem, listOfSpeakers?: ViewListOfSpeakers) { public constructor(collectionString: string, model: M) {
super(collectionString, model); 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. * Should return a string representation of the object, so there can be searched for.
*/ */
public abstract formatForSearch(): SearchRepresentation; 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;
}
}
} }

View File

@ -3,8 +3,7 @@ import { SearchRepresentation } from 'app/core/ui-services/search.service';
import { isDetailNavigable, DetailNavigable } from 'app/shared/models/base/detail-navigable'; import { isDetailNavigable, DetailNavigable } from 'app/shared/models/base/detail-navigable';
import { isSearchable, Searchable } from './searchable'; import { isSearchable, Searchable } from './searchable';
import { BaseModelWithAgendaItem } from 'app/shared/models/base/base-model-with-agenda-item'; import { BaseModelWithAgendaItem } from 'app/shared/models/base/base-model-with-agenda-item';
import { BaseViewModel, TitleInformation } from './base-view-model'; import { TitleInformation } from './base-view-model';
import { Item } from 'app/shared/models/agenda/item';
export function isBaseViewModelWithAgendaItem(obj: any): obj is BaseViewModelWithAgendaItem { export function isBaseViewModelWithAgendaItem(obj: any): obj is BaseViewModelWithAgendaItem {
const model = <BaseViewModelWithAgendaItem>obj; const model = <BaseViewModelWithAgendaItem>obj;
@ -105,11 +104,4 @@ export abstract class BaseViewModelWithAgendaItem<M extends BaseModelWithAgendaI
* Should return a string representation of the object, so there can be searched for. * Should return a string representation of the object, so there can be searched for.
*/ */
public abstract formatForSearch(): SearchRepresentation; public abstract formatForSearch(): SearchRepresentation;
public updateDependencies(update: BaseViewModel): void {
// We cannot check with instanceof, because this gives circular dependency issues...
if (update.collectionString === Item.COLLECTIONSTRING && update.id === this.agenda_item_id) {
this._item = update;
}
}
} }

View File

@ -26,38 +26,8 @@ export abstract class BaseViewModelWithContentObject<
/** /**
* @param collectionString The collection string of this model * @param collectionString The collection string of this model
* @param model the model this view model captures * @param model the model this view model captures
* @param isC A function ensuring that an arbitrary object is a valid content object
* @param CVerbose is the verbose name of the base content object class, for debugging purposes
* @param contentObject (optional) The content object, if it is known during creation.
*/ */
public constructor( public constructor(collectionString: string, model: M) {
collectionString: string,
model: M,
private isC: (obj: any) => obj is C,
private CVerbose: string,
contentObject?: C
) {
super(collectionString, model); 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;
} }
} }

View File

@ -1,8 +1,6 @@
import { BaseProjectableViewModel } from './base-projectable-view-model'; import { BaseProjectableViewModel } from './base-projectable-view-model';
import { isDetailNavigable, DetailNavigable } from 'app/shared/models/base/detail-navigable'; import { isDetailNavigable, DetailNavigable } from 'app/shared/models/base/detail-navigable';
import { BaseModelWithListOfSpeakers } from 'app/shared/models/base/base-model-with-list-of-speakers'; 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 { export function isBaseViewModelWithListOfSpeakers(obj: any): obj is BaseViewModelWithListOfSpeakers {
const model = <BaseViewModelWithListOfSpeakers>obj; const model = <BaseViewModelWithListOfSpeakers>obj;
@ -38,10 +36,10 @@ export interface IBaseViewModelWithListOfSpeakers<M extends BaseModelWithListOfS
export abstract class BaseViewModelWithListOfSpeakers<M extends BaseModelWithListOfSpeakers = any> export abstract class BaseViewModelWithListOfSpeakers<M extends BaseModelWithListOfSpeakers = any>
extends BaseProjectableViewModel<M> extends BaseProjectableViewModel<M>
implements IBaseViewModelWithListOfSpeakers<M> { implements IBaseViewModelWithListOfSpeakers<M> {
protected _listOfSpeakers?: any; protected _list_of_speakers?: any;
public get listOfSpeakers(): any | null { public get listOfSpeakers(): any | null {
return this._listOfSpeakers; return this._list_of_speakers;
} }
public get list_of_speakers_id(): number { public get list_of_speakers_id(): number {
@ -53,15 +51,8 @@ export abstract class BaseViewModelWithListOfSpeakers<M extends BaseModelWithLis
public constructor(collectionString: string, model: M, listOfSpeakers?: any) { public constructor(collectionString: string, model: M, listOfSpeakers?: any) {
super(collectionString, model); super(collectionString, model);
this._listOfSpeakers = listOfSpeakers || null; // Explicit set to null instead of undefined, if not given this._list_of_speakers = listOfSpeakers || null; // Explicit set to null instead of undefined, if not given
} }
public abstract getDetailStateURL(): string; public abstract getDetailStateURL(): string;
public updateDependencies(update: BaseViewModel): void {
// We cannot check with instanceof, becuase this givec circular dependency issues...
if (update.collectionString === ListOfSpeakers.COLLECTIONSTRING && update.id === this.list_of_speakers_id) {
this._listOfSpeakers = update;
}
}
} }

View File

@ -2,7 +2,6 @@ import { Displayable } from './displayable';
import { Identifiable } from '../../shared/models/base/identifiable'; import { Identifiable } from '../../shared/models/base/identifiable';
import { Collection } from 'app/shared/models/base/collection'; import { Collection } from 'app/shared/models/base/collection';
import { BaseModel } from 'app/shared/models/base/base-model'; import { BaseModel } from 'app/shared/models/base/base-model';
import { Updateable } from './updateable';
export type TitleInformation = object; export type TitleInformation = object;
@ -14,8 +13,7 @@ export interface ViewModelConstructor<T extends BaseViewModel> {
/** /**
* Base class for view models. alls view models should have titles. * Base class for view models. alls view models should have titles.
*/ */
export abstract class BaseViewModel<M extends BaseModel = any> export abstract class BaseViewModel<M extends BaseModel = any> implements Displayable, Identifiable, Collection {
implements Displayable, Identifiable, Collection, Updateable {
protected _model: M; protected _model: M;
public get id(): number { public get id(): number {
@ -72,8 +70,6 @@ export abstract class BaseViewModel<M extends BaseModel = any>
return this._model; return this._model;
} }
public abstract updateDependencies(update: BaseViewModel): void;
public toString(): string { public toString(): string {
return this.getTitle(); return this.getTitle();
} }

View File

@ -94,8 +94,6 @@ export class ViewConfig extends BaseViewModel<Config> implements ConfigTitleInfo
super(Config.COLLECTIONSTRING, config); 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. * 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, * A little debounce time for all inputs is given here and is usefull, if inputs sends multiple onChange-events,

View File

@ -52,8 +52,8 @@
{{ directory.title }} {{ directory.title }}
</span> </span>
</span> </span>
<span class="visibility" *ngIf="canEdit && directory && directory.inherited_access_groups_id !== true"> <span class="visibility" *ngIf="canEdit && directory && directory.has_inherited_access_groups">
<span class="visible-for" *ngIf="directory.has_inherited_access_groups" translate> <span class="visible-for" translate>
<os-icon-container <os-icon-container
icon="visibility" icon="visibility"
matTooltip="{{ 'Allowed access groups for this directory' | translate }}" matTooltip="{{ 'Allowed access groups for this directory' | translate }}"
@ -64,6 +64,16 @@
</os-icon-container> </os-icon-container>
</span> </span>
</span> </span>
<span class="visibility" *ngIf="canEdit && directory && directory.inherited_access_groups_id === false">
<span class="visible-for">
<os-icon-container
icon="visibility"
matTooltip="{{ 'Allowed access groups for this directory' | translate }}"
>
<span translate>Noone</span>
</os-icon-container>
</span>
</span>
</div> </div>
<mat-divider></mat-divider> <mat-divider></mat-divider>
</div> </div>

View File

@ -1,10 +1,8 @@
import { BaseViewModel } from '../../base/base-view-model';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile'; import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
import { Searchable } from 'app/site/base/searchable'; import { Searchable } from 'app/site/base/searchable';
import { SearchRepresentation } from 'app/core/ui-services/search.service'; import { SearchRepresentation } from 'app/core/ui-services/search.service';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { BaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-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';
import { ViewGroup } from 'app/site/users/models/view-group'; import { ViewGroup } from 'app/site/users/models/view-group';
export const IMAGE_MIMETYPES = ['image/png', 'image/jpeg', 'image/gif']; export const IMAGE_MIMETYPES = ['image/png', 'image/jpeg', 'image/gif'];
@ -95,17 +93,8 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
return this.mediafile.create_timestamp ? this.mediafile.create_timestamp : null; return this.mediafile.create_timestamp ? this.mediafile.create_timestamp : null;
} }
public constructor( public constructor(mediafile: Mediafile) {
mediafile: Mediafile, super(Mediafile.COLLECTIONSTRING, 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 formatForSearch(): SearchRepresentation { public formatForSearch(): SearchRepresentation {
@ -202,29 +191,4 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
return 'insert_drive_file'; 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 && (<number[]>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;
}
}
}
}
} }

View File

@ -75,9 +75,8 @@ export class ViewCategory extends BaseViewModel<Category> implements CategoryTit
} }
} }
public constructor(category: Category, parent?: ViewCategory) { public constructor(category: Category) {
super(Category.COLLECTIONSTRING, category); super(Category.COLLECTIONSTRING, category);
this._parent = parent;
} }
public formatForSearch(): SearchRepresentation { public formatForSearch(): SearchRepresentation {
@ -102,14 +101,4 @@ export class ViewCategory extends BaseViewModel<Category> implements CategoryTit
return []; 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;
}
}
} }

View File

@ -1,16 +1,6 @@
import { WorkflowState } from 'app/shared/models/motions/workflow-state';
import { ViewMotion } from './view-motion'; import { ViewMotion } from './view-motion';
import { CreateMotion } from './create-motion'; import { CreateMotion } from './create-motion';
import { ViewUser } from 'app/site/users/models/view-user'; 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 * 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 { export class ViewCreateMotion extends ViewMotion {
protected _model: CreateMotion; protected _model: CreateMotion;
protected _submitterUsers: ViewUser[];
public get motion(): CreateMotion { public get motion(): CreateMotion {
return this._model; return this._model;
} }
public get submitters(): ViewUser[] { public get submittersAsUsers(): ViewUser[] {
return this._submitters; return this._submitterUsers;
} }
public get submitters_id(): number[] { public set submittersAsUsers(users: ViewUser[]) {
return this.motion ? this.motion.sorted_submitters_id : null; this._submitterUsers = users;
}
public set submitters(users: ViewUser[]) {
this._submitters = users;
this._model.submitters_id = users.map(user => user.id); this._model.submitters_id = users.map(user => user.id);
} }
public constructor( public constructor(motion: CreateMotion) {
motion: CreateMotion, super(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,
category,
submitters,
supporters,
workflow,
state,
item,
listOfSpeakers,
block,
attachments,
tags,
parent,
changeRecommendations,
amendments,
personalNote
);
} }
public getVerboseName = () => { public getVerboseName = () => {
@ -82,13 +38,6 @@ export class ViewCreateMotion extends ViewMotion {
* Duplicate this motion into a copy of itself * Duplicate this motion into a copy of itself
*/ */
public copy(): ViewCreateMotion { public copy(): ViewCreateMotion {
return new ViewCreateMotion( return new ViewCreateMotion(this._model);
this._model,
this._category,
this._submitters,
this._supporters,
this._workflow,
this._state
);
} }
} }

View File

@ -1,7 +1,7 @@
import { ViewUnifiedChange, ViewUnifiedChangeType } from '../../../shared/models/motions/view-unified-change'; import { ViewUnifiedChange, ViewUnifiedChangeType } from '../../../shared/models/motions/view-unified-change';
import { ViewMotion } from './view-motion'; import { ViewMotion } from './view-motion';
import { LineRange } from 'app/core/ui-services/diff.service'; 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. * This represents the Unified Diff part of an amendments.

View File

@ -2,9 +2,7 @@ import { MotionBlock } from 'app/shared/models/motions/motion-block';
import { SearchRepresentation } from 'app/core/ui-services/search.service'; import { SearchRepresentation } from 'app/core/ui-services/search.service';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { Searchable } from 'app/site/base/searchable'; 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 { 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'; import { TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item';
export interface MotionBlockTitleInformation extends TitleInformationWithAgendaItem { export interface MotionBlockTitleInformation extends TitleInformationWithAgendaItem {
@ -31,8 +29,8 @@ export class ViewMotionBlock extends BaseViewModelWithAgendaItemAndListOfSpeaker
return this.motionBlock.internal; return this.motionBlock.internal;
} }
public constructor(motionBlock: MotionBlock, agendaItem?: ViewItem, listOfSpeakers?: ViewListOfSpeakers) { public constructor(motionBlock: MotionBlock) {
super(MotionBlock.COLLECTIONSTRING, motionBlock, agendaItem, listOfSpeakers); super(MotionBlock.COLLECTIONSTRING, motionBlock);
} }
/** /**

View File

@ -24,8 +24,6 @@ export class ViewMotionChangeRecommendation extends BaseViewModel<MotionChangeRe
super(MotionChangeRecommendation.COLLECTIONSTRING, motionChangeRecommendation); super(MotionChangeRecommendation.COLLECTIONSTRING, motionChangeRecommendation);
} }
public updateDependencies(update: BaseViewModel): void {}
public updateChangeReco(type: number, text: string, internal: boolean): void { public updateChangeReco(type: number, text: string, internal: boolean): void {
// @TODO HTML sanitazion // @TODO HTML sanitazion
this.changeRecommendation.type = type; this.changeRecommendation.type = type;

View File

@ -17,8 +17,8 @@ export class ViewMotionCommentSection extends BaseViewModel<MotionCommentSection
implements MotionCommentSectionTitleInformation { implements MotionCommentSectionTitleInformation {
public static COLLECTIONSTRING = MotionCommentSection.COLLECTIONSTRING; public static COLLECTIONSTRING = MotionCommentSection.COLLECTIONSTRING;
private _readGroups: ViewGroup[]; private _read_groups: ViewGroup[];
private _writeGroups: ViewGroup[]; private _write_groups: ViewGroup[];
public get section(): MotionCommentSection { public get section(): MotionCommentSection {
return this._model; return this._model;
@ -41,11 +41,11 @@ export class ViewMotionCommentSection extends BaseViewModel<MotionCommentSection
} }
public get read_groups(): ViewGroup[] { public get read_groups(): ViewGroup[] {
return this._readGroups; return this._read_groups;
} }
public get write_groups(): ViewGroup[] { public get write_groups(): ViewGroup[] {
return this._writeGroups; return this._write_groups;
} }
public get weight(): number { public get weight(): number {
@ -59,10 +59,8 @@ export class ViewMotionCommentSection extends BaseViewModel<MotionCommentSection
this._model.name = name; this._model.name = name;
} }
public constructor(motionCommentSection: MotionCommentSection, readGroups: ViewGroup[], writeGroups: ViewGroup[]) { public constructor(motionCommentSection: MotionCommentSection) {
super(MotionCommentSection.COLLECTIONSTRING, motionCommentSection); super(MotionCommentSection.COLLECTIONSTRING, motionCommentSection);
this._readGroups = readGroups;
this._writeGroups = writeGroups;
} }
/** /**

View File

@ -1,25 +1,22 @@
import { Motion, MotionComment } from 'app/shared/models/motions/motion'; import { Motion, MotionComment } from 'app/shared/models/motions/motion';
import { PersonalNoteContent } from 'app/shared/models/users/personal-note'; import { PersonalNoteContent } from 'app/shared/models/users/personal-note';
import { ViewMotionCommentSection } from './view-motion-comment-section'; import { ViewMotionCommentSection } from './view-motion-comment-section';
import { WorkflowState } from 'app/shared/models/motions/workflow-state';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { SearchRepresentation } from 'app/core/ui-services/search.service'; import { SearchRepresentation } from 'app/core/ui-services/search.service';
import { Searchable } from 'app/site/base/searchable'; import { Searchable } from 'app/site/base/searchable';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
import { ViewTag } from 'app/site/tags/models/view-tag'; import { ViewTag } from 'app/site/tags/models/view-tag';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { ViewItem } from 'app/site/agenda/models/view-item';
import { ViewWorkflow } from './view-workflow'; import { ViewWorkflow } from './view-workflow';
import { ViewCategory } from './view-category'; import { ViewCategory } from './view-category';
import { ViewMotionBlock } from './view-motion-block'; import { ViewMotionBlock } from './view-motion-block';
import { BaseViewModel } from 'app/site/base/base-view-model';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { ViewPersonalNote } from 'app/site/users/models/view-personal-note';
import { ViewMotionChangeRecommendation } from './view-motion-change-recommendation'; import { ViewMotionChangeRecommendation } from './view-motion-change-recommendation';
import { _ } from 'app/core/translate/translation-marker'; import { _ } from 'app/core/translate/translation-marker';
import { BaseViewModelWithAgendaItemAndListOfSpeakers } from 'app/site/base/base-view-model-with-agenda-item-and-list-of-speakers'; 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'; import { TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item';
import { ViewState } from './view-state';
import { ViewSubmitter } from './view-submitter';
/** /**
* The line numbering mode for the motion detail view. * The line numbering mode for the motion detail view.
@ -67,11 +64,12 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
public static COLLECTIONSTRING = Motion.COLLECTIONSTRING; public static COLLECTIONSTRING = Motion.COLLECTIONSTRING;
protected _category?: ViewCategory; protected _category?: ViewCategory;
protected _submitters?: ViewUser[]; protected _submitters?: ViewSubmitter[];
protected _supporters?: ViewUser[]; protected _supporters?: ViewUser[];
protected _workflow?: ViewWorkflow; protected _workflow?: ViewWorkflow;
protected _state?: WorkflowState; protected _state?: ViewState;
protected _block?: ViewMotionBlock; protected _recommendation?: ViewState;
protected _motion_block?: ViewMotionBlock;
protected _attachments?: ViewMediafile[]; protected _attachments?: ViewMediafile[];
protected _tags?: ViewTag[]; protected _tags?: ViewTag[];
protected _parent?: ViewMotion; protected _parent?: ViewMotion;
@ -87,10 +85,22 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
return this._category; return this._category;
} }
public get submitters(): ViewUser[] { public get state(): ViewState | null {
return this._state;
}
public get recommendation(): ViewState | null {
return this._recommendation;
}
public get submitters(): ViewSubmitter[] {
return this._submitters || []; return this._submitters || [];
} }
public get submittersAsUsers(): ViewUser[] {
return this.submitters.map(submitter => submitter.user);
}
public get supporters(): ViewUser[] { public get supporters(): ViewUser[] {
return this._supporters || []; return this._supporters || [];
} }
@ -104,7 +114,7 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
} }
public get motion_block(): ViewMotionBlock | null { public get motion_block(): ViewMotionBlock | null {
return this._block; return this._motion_block;
} }
public get attachments(): ViewMediafile[] { public get attachments(): ViewMediafile[] {
@ -185,21 +195,12 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
return this.motion.workflow_id; return this.motion.workflow_id;
} }
public get state(): WorkflowState | null {
return this._state;
}
public get changeRecommendations(): ViewMotionChangeRecommendation[] { public get changeRecommendations(): ViewMotionChangeRecommendation[] {
return this._changeRecommendations; return this._changeRecommendations;
} }
/** public get change_recommendations_id(): number[] {
* Checks if the current state of thw workflow is final return this.motion.change_recommendations_id;
*
* @returns true if it is final
*/
public get isFinalState(): boolean {
return this._state.isFinalState;
} }
public get state_id(): number { public get state_id(): number {
@ -214,28 +215,14 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
return this.motion.statute_paragraph_id; return this.motion.statute_paragraph_id;
} }
public get recommendation(): WorkflowState { public get possibleRecommendations(): ViewState[] {
return this.workflow ? this.workflow.getStateById(this.recommendation_id) : null; return this.workflow ? this.workflow.states.filter(state => state.recommendation_label !== undefined) : null;
}
public get possibleRecommendations(): WorkflowState[] {
return this.workflow
? this.workflow.states.filter(recommendation => recommendation.recommendation_label !== undefined)
: null;
} }
public get origin(): string { public get origin(): string {
return this.motion.origin; 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 { public get agenda_type(): number {
return this.agendaItem ? this.agendaItem.type : null; return this.agendaItem ? this.agendaItem.type : null;
} }
@ -327,8 +314,6 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
/** /**
* Getter to query the 'favorite'/'star' status of the motions * Getter to query the 'favorite'/'star' status of the motions
*
* @returns the current state
*/ */
public get star(): boolean { public get star(): boolean {
return !!this.personalNote && !!this.personalNote.star; return !!this.personalNote && !!this.personalNote.star;
@ -355,36 +340,8 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
// This is set by the repository // This is set by the repository
public getIdentifierOrTitle: () => string; public getIdentifierOrTitle: () => string;
public constructor( public constructor(motion: Motion) {
motion: Motion, super(Motion.COLLECTIONSTRING, 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;
} }
/** /**
@ -397,7 +354,7 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
if (this.amendment_paragraphs) { if (this.amendment_paragraphs) {
searchValues = searchValues.concat(this.amendment_paragraphs.filter(x => !!x)); searchValues = searchValues.concat(this.amendment_paragraphs.filter(x => !!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.supporters.map(user => user.full_name));
searchValues = searchValues.concat(this.tags.map(tag => tag.getTitle())); searchValues = searchValues.concat(this.tags.map(tag => tag.getTitle()));
searchValues = searchValues.concat(this.motion.comments.map(comment => comment.comment)); searchValues = searchValues.concat(this.motion.comments.map(comment => comment.comment));
@ -429,146 +386,6 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
return this.motion.comments.find(comment => comment.section_id === section.id); return this.motion.comments.find(comment => 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 { public hasSupporters(): boolean {
return !!(this.supporters && this.supporters.length > 0); return !!(this.supporters && this.supporters.length > 0);
} }
@ -589,7 +406,7 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
* Determine if the motion is in its final workflow state * Determine if the motion is in its final workflow state
*/ */
public isInFinalState(): boolean { public isInFinalState(): boolean {
return this.nextStates.length === 0; return this.state ? this.state.isFinalState : false;
} }
/** /**
@ -635,19 +452,6 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
* Duplicate this motion into a copy of itself * Duplicate this motion into a copy of itself
*/ */
public copy(): ViewMotion { public copy(): ViewMotion {
return new ViewMotion( return new ViewMotion(this._model);
this._model,
this._category,
this._submitters,
this._supporters,
this._workflow,
this._state,
this._item,
this._listOfSpeakers,
this._block,
this._attachments,
this._tags,
this._parent
);
} }
} }

View File

@ -0,0 +1,99 @@
import { BaseViewModel } from '../../base/base-view-model';
import { State, MergeAmendment } from 'app/shared/models/motions/state';
import { ViewWorkflow } from './view-workflow';
export interface StateTitleInformation {
name: string;
}
/**
* class for the ViewState.
* @ignore
*/
export class ViewState extends BaseViewModel<State> 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);
}
}

View File

@ -45,10 +45,4 @@ export class ViewStatuteParagraph extends BaseViewModel<StatuteParagraph>
public getDetailStateURL(): string { public getDetailStateURL(): string {
return '/motions/statute-paragraphs'; return '/motions/statute-paragraphs';
} }
/**
* Updates the local objects if required
* @param section
*/
public updateDependencies(update: BaseViewModel): void {}
} }

View File

@ -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<Submitter> {
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();
};
}

View File

@ -1,6 +1,6 @@
import { Workflow } from 'app/shared/models/motions/workflow'; 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 { BaseViewModel } from '../../base/base-view-model';
import { ViewState } from './view-state';
export interface WorkflowTitleInformation { export interface WorkflowTitleInformation {
name: string; name: string;
@ -13,6 +13,9 @@ export interface WorkflowTitleInformation {
export class ViewWorkflow extends BaseViewModel<Workflow> implements WorkflowTitleInformation { export class ViewWorkflow extends BaseViewModel<Workflow> implements WorkflowTitleInformation {
public static COLLECTIONSTRING = Workflow.COLLECTIONSTRING; public static COLLECTIONSTRING = Workflow.COLLECTIONSTRING;
private _states?: ViewState[];
private _first_state?: ViewState;
public get workflow(): Workflow { public get workflow(): Workflow {
return this._model; return this._model;
} }
@ -21,34 +24,23 @@ export class ViewWorkflow extends BaseViewModel<Workflow> implements WorkflowTit
return this.workflow.name; return this.workflow.name;
} }
public get states(): WorkflowState[] { public get states(): ViewState[] {
return this.workflow.states; return this._states || [];
}
public get states_id(): number[] {
return this.workflow.states_id;
} }
public get first_state_id(): number { public get first_state_id(): number {
return this.workflow.first_state_id; return this.workflow.first_state_id;
} }
public get firstState(): WorkflowState { public get first_state(): ViewState | null {
return this.getStateById(this.first_state_id); return this._first_state;
} }
public constructor(workflow: Workflow) { public constructor(workflow: Workflow) {
super(Workflow.COLLECTIONSTRING, 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);
}
} }

View File

@ -13,8 +13,8 @@
</h4> </h4>
<div *ngIf="!isEditMode || !perms.isAllowed('change_metadata')"> <div *ngIf="!isEditMode || !perms.isAllowed('change_metadata')">
<mat-chip-list *ngFor="let submitter of motion.submitters" class="user"> <mat-chip-list *ngFor="let user of motion.submittersAsUsers" class="user">
<mat-chip disableRipple>{{ submitter.full_name }}</mat-chip> <mat-chip disableRipple *ngIf="user">{{ user.getTitle() }}</mat-chip>
</mat-chip-list> </mat-chip-list>
</div> </div>

View File

@ -93,7 +93,7 @@ export class ManageSubmittersComponent extends BaseViewComponent {
*/ */
public onEdit(): void { public onEdit(): void {
this.isEditMode = true; this.isEditMode = true;
this.editSubmitterSubject.next(this.motion.submitters.map(x => x)); this.editSubmitterSubject.next(this.motion.submittersAsUsers);
this.addSubmitterForm.reset(); this.addSubmitterForm.reset();
// get all users for the submitter add form // get all users for the submitter add form

View File

@ -261,13 +261,13 @@
extensionLabel="{{ 'Extension' | translate }}" extensionLabel="{{ 'Extension' | translate }}"
(success)="setStateExtension($event)" (success)="setStateExtension($event)"
> >
<span class="trigger-menu"> <span class="trigger-menu" *ngIf="motion.state">
<button *ngFor="let state of motion.nextStates" mat-menu-item (click)="setState(state.id)"> <button *ngFor="let state of motion.state.next_states" mat-menu-item (click)="setState(state.id)">
{{ state.name | translate }} <span *ngIf="state.show_state_extension_field">&nbsp;...</span> {{ state.name | translate }} <span *ngIf="state.show_state_extension_field">&nbsp;...</span>
</button> </button>
<div> <div>
<mat-divider *ngIf="motion.nextStates.length > 0"></mat-divider> <mat-divider *ngIf="motion.state.next_states.length > 0"></mat-divider>
<button *ngFor="let state of motion.previousStates" mat-menu-item (click)="setState(state.id)"> <button *ngFor="let state of motion.state.previous_states" mat-menu-item (click)="setState(state.id)">
<mat-icon>arrow_back</mat-icon> {{ state.name | translate }} <mat-icon>arrow_back</mat-icon> {{ state.name | translate }}
<span *ngIf="state.show_state_extension_field">&nbsp;...</span> <span *ngIf="state.show_state_extension_field">&nbsp;...</span>
</button> </button>

View File

@ -47,10 +47,7 @@ import {
LineNumberingMode, LineNumberingMode,
verboseChangeRecoMode verboseChangeRecoMode
} from 'app/site/motions/models/view-motion'; } from 'app/site/motions/models/view-motion';
import { import { MotionEditNotification, MotionEditNotificationType } from 'app/site/motions/motion-edit-notification';
ViewMotionNotificationEditMotion,
TypeOfNotificationViewMotion
} from 'app/site/motions/models/view-motion-notify';
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block'; import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
import { ViewCategory } from 'app/site/motions/models/view-category'; import { ViewCategory } from 'app/site/motions/models/view-category';
import { ViewCreateMotion } from 'app/site/motions/models/view-create-motion'; 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 * Sends a notification to user editors of the motion was edited
*/ */
public ngOnDestroy(): void { public ngOnDestroy(): void {
this.unsubscribeEditNotifications(TypeOfNotificationViewMotion.TYPE_CLOSING_EDITING_MOTION); this.unsubscribeEditNotifications(MotionEditNotificationType.TYPE_CLOSING_EDITING_MOTION);
if (this.navigationSubscription) { if (this.navigationSubscription) {
this.navigationSubscription.unsubscribe(); this.navigationSubscription.unsubscribe();
} }
@ -832,7 +829,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
} else { } else {
this.updateMotionFromForm(); this.updateMotionFromForm();
// When saving the changes, notify other users if they edit the same motion. // 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.motionCopy = this.motion.copy();
this.patchForm(this.motionCopy); this.patchForm(this.motionCopy);
this.editNotificationSubscription = this.listenToEditNotification(); this.editNotificationSubscription = this.listenToEditNotification();
this.sendEditNotification(TypeOfNotificationViewMotion.TYPE_BEGIN_EDITING_MOTION); this.sendEditNotification(MotionEditNotificationType.TYPE_BEGIN_EDITING_MOTION);
} }
if (!mode && this.newMotion) { if (!mode && this.newMotion) {
this.router.navigate(['./motions/']); this.router.navigate(['./motions/']);
@ -1173,7 +1170,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
// If the user cancels the work on this motion, // If the user cancels the work on this motion,
// notify the users who are still editing the same motion // notify the users who are still editing the same motion
if (!mode && !this.newMotion) { 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 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. * @param user Optional userId. If set the function will send a notification to the given userId.
*/ */
private sendEditNotification(type: TypeOfNotificationViewMotion, user?: number): void { private sendEditNotification(type: MotionEditNotificationType, user?: number): void {
const content: ViewMotionNotificationEditMotion = { const content: MotionEditNotification = {
motionId: this.motion.id, motionId: this.motion.id,
senderId: this.operator.viewUser.id, senderId: this.operator.viewUser.id,
senderName: this.operator.viewUser.short_name, senderName: this.operator.viewUser.short_name,
@ -1422,7 +1419,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
if (user) { if (user) {
this.notifyService.sendToUsers(this.NOTIFICATION_EDIT_MOTION, content, user); this.notifyService.sendToUsers(this.NOTIFICATION_EDIT_MOTION, content, user);
} else { } else {
this.notifyService.sendToAllUsers<ViewMotionNotificationEditMotion>(this.NOTIFICATION_EDIT_MOTION, content); this.notifyService.sendToAllUsers<MotionEditNotification>(this.NOTIFICATION_EDIT_MOTION, content);
} }
} }
@ -1434,13 +1431,13 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
*/ */
private listenToEditNotification(): Subscription { private listenToEditNotification(): Subscription {
return this.notifyService.getMessageObservable(this.NOTIFICATION_EDIT_MOTION).subscribe(message => { return this.notifyService.getMessageObservable(this.NOTIFICATION_EDIT_MOTION).subscribe(message => {
const content = <ViewMotionNotificationEditMotion>message.content; const content = <MotionEditNotification>message.content;
if (this.operator.viewUser.id !== content.senderId && content.motionId === this.motion.id) { if (this.operator.viewUser.id !== content.senderId && content.motionId === this.motion.id) {
let warning = ''; let warning = '';
switch (content.type) { switch (content.type) {
case TypeOfNotificationViewMotion.TYPE_BEGIN_EDITING_MOTION: case MotionEditNotificationType.TYPE_BEGIN_EDITING_MOTION:
case TypeOfNotificationViewMotion.TYPE_ALSO_EDITING_MOTION: { case MotionEditNotificationType.TYPE_ALSO_EDITING_MOTION: {
if (!this.otherWorkOnMotion.includes(content.senderName)) { if (!this.otherWorkOnMotion.includes(content.senderName)) {
this.otherWorkOnMotion.push(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:')} ${ warning = `${this.translate.instant('Following users are currently editing this motion:')} ${
this.otherWorkOnMotion this.otherWorkOnMotion
}`; }`;
if (content.type === TypeOfNotificationViewMotion.TYPE_BEGIN_EDITING_MOTION) { if (content.type === MotionEditNotificationType.TYPE_BEGIN_EDITING_MOTION) {
this.sendEditNotification( this.sendEditNotification(
TypeOfNotificationViewMotion.TYPE_ALSO_EDITING_MOTION, MotionEditNotificationType.TYPE_ALSO_EDITING_MOTION,
message.senderUserId message.senderUserId
); );
} }
break; break;
} }
case TypeOfNotificationViewMotion.TYPE_CLOSING_EDITING_MOTION: { case MotionEditNotificationType.TYPE_CLOSING_EDITING_MOTION: {
this.recognizeOtherWorkerOnMotion(content.senderName); this.recognizeOtherWorkerOnMotion(content.senderName);
break; break;
} }
case TypeOfNotificationViewMotion.TYPE_SAVING_EDITING_MOTION: { case MotionEditNotificationType.TYPE_SAVING_EDITING_MOTION: {
warning = `${content.senderName} ${this.translate.instant( warning = `${content.senderName} ${this.translate.instant(
'has saved his work on this motion.' '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. * @param unsubscriptionReason The reason for the unsubscription.
*/ */
private unsubscribeEditNotifications(unsubscriptionReason: TypeOfNotificationViewMotion): void { private unsubscribeEditNotifications(unsubscriptionReason: MotionEditNotificationType): void {
if (!!this.editNotificationSubscription && !this.editNotificationSubscription.closed) { if (!!this.editNotificationSubscription && !this.editNotificationSubscription.closed) {
this.sendEditNotification(unsubscriptionReason); this.sendEditNotification(unsubscriptionReason);
this.closeSnackBar(); this.closeSnackBar();

View File

@ -19,7 +19,7 @@
<div class="title-line"> <div class="title-line">
<strong> <strong>
<span translate>First state</span>: <span translate>First state</span>:
<span>{{ workflow.firstState.name | translate }}</span> <span>{{ workflow.first_state.name | translate }}</span>
</strong> </strong>
</div> </div>
@ -70,20 +70,13 @@
{{ state[perm.selector] | translate }} {{ state[perm.selector] | translate }}
</mat-basic-chip> </mat-basic-chip>
</div> </div>
<div <div *ngIf="perm.type === 'state'">
*ngIf="perm.type === 'state'"
>
<div class="inner-table"> <div class="inner-table">
<div *ngIf="!state.next_states_id.length"> <div *ngIf="!state.next_states.length">
- -
</div> </div>
<div *ngIf="state.next_states_id.length"> <div *ngIf="state.next_states.length">
<div <div *ngFor="let nextstate of state.next_states; let last = last">
*ngFor="
let nextstate of state.getNextStates(workflow.workflow);
let last = last
"
>
{{ nextstate.name | translate }}<span *ngIf="!last">,&nbsp;</span> {{ nextstate.name | translate }}<span *ngIf="!last">,&nbsp;</span>
</div> </div>
</div> </div>
@ -92,11 +85,9 @@
class="clickable-cell stretch-to-fill-parent" class="clickable-cell stretch-to-fill-parent"
[matMenuTriggerFor]="nextStatesMenu" [matMenuTriggerFor]="nextStatesMenu"
[matMenuTriggerData]="{ state: state }" [matMenuTriggerData]="{ state: state }"
></div> ></div>
</div> </div>
<div <div *ngIf="perm.type === 'amendment'">
*ngIf="perm.type === 'amendment'"
>
<div class="inner-table"> <div class="inner-table">
{{ getMergeAmendmentLabel(state.merge_amendment_into_final) | translate }} {{ getMergeAmendmentLabel(state.merge_amendment_into_final) | translate }}
</div> </div>
@ -106,9 +97,7 @@
[matMenuTriggerData]="{ state: state }" [matMenuTriggerData]="{ state: state }"
></div> ></div>
</div> </div>
<div <div *ngIf="perm.type === 'restriction'">
*ngIf="perm.type === 'restriction'"
>
<div class="inner-table"> <div class="inner-table">
<div *ngIf="!state.restriction.length"> <div *ngIf="!state.restriction.length">
- -

View File

@ -12,8 +12,10 @@ import { TranslateService } from '@ngx-translate/core';
import { BaseViewComponent } from 'app/site/base/base-view'; import { BaseViewComponent } from 'app/site/base/base-view';
import { ViewWorkflow } from 'app/site/motions/models/view-workflow'; import { ViewWorkflow } from 'app/site/motions/models/view-workflow';
import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service'; 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 { 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 * Declares data for the workflow dialog
@ -153,6 +155,7 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit
private promtService: PromptService, private promtService: PromptService,
private dialog: MatDialog, private dialog: MatDialog,
private workflowRepo: WorkflowRepositoryService, private workflowRepo: WorkflowRepositoryService,
private stateRepo: StateRepositoryService,
private route: ActivatedRoute private route: ActivatedRoute
) { ) {
super(title, translate, matSnackBar); super(title, translate, matSnackBar);
@ -180,17 +183,17 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit
* *
* @param state the selected workflow state * @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 => { this.openEditDialog(state.name, 'Rename state', '', true).subscribe(result => {
if (result) { if (result) {
if (result.action === 'update') { 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') { } else if (result.action === 'delete') {
const content = this.translate.instant('Delete') + ` ${state.name}?`; const content = this.translate.instant('Delete') + ` ${state.name}?`;
this.promtService.open('Are you sure', content).then(promptResult => { this.promtService.open('Are you sure', content).then(promptResult => {
if (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( this.openEditDialog('', this.translate.instant('New state'), this.translate.instant('Name')).subscribe(
result => { result => {
if (result && result.action === 'update') { 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 perm The permission
* @param state The selected workflow state * @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 => { this.openEditDialog(state[perm.selector], 'Edit', perm.name).subscribe(result => {
if (result && result.action === 'update') { 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 perm The states permission that was changed
* @param event The change event. * @param event The change event.
*/ */
public onToggleStatePerm(state: WorkflowState, perm: string, event: MatCheckboxChange): void { public onToggleStatePerm(state: ViewState, perm: string, event: MatCheckboxChange): void {
this.workflowRepo.updateState({ [perm]: event.checked }, state).then(() => {}, this.raiseError); 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 state The selected workflow state
* @param color The selected color * @param color The selected color
*/ */
public onSelectColor(state: WorkflowState, color: string): void { public onSelectColor(state: ViewState, color: string): void {
this.workflowRepo.updateState({ css_class: color }, state).then(() => {}, this.raiseError); 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 nextState the potential next workflow state
* @param state the state to add or remove another state to * @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 ids = state.next_states_id.map(id => id);
const stateIdIndex = ids.findIndex(id => id === nextState.id); const stateIdIndex = ids.findIndex(id => id === nextState.id);
@ -276,7 +283,7 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit
} else { } else {
ids.splice(stateIdIndex, 1); 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 restrictions The new restrictions
* @param state the state to change * @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 restrictions = state.restriction.map(r => r);
const restrictionIndex = restrictions.findIndex(r => r === restriction); const restrictionIndex = restrictions.findIndex(r => r === restriction);
@ -294,7 +301,7 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit
} else { } else {
restrictions.splice(restrictionIndex, 1); 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 amendment determines the amendment
* @param state the state to change * @param state the state to change
*/ */
public setMergeAmendment(amendment: number, state: WorkflowState): void { public setMergeAmendment(amendment: number, state: ViewState): void {
this.workflowRepo.updateState({ merge_amendment_into_final: amendment }, state).then(() => {}, this.raiseError); 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 * @param state the workflow state
* @returns a unique definition * @returns a unique definition
*/ */
public getColumnDef(state: WorkflowState): string { public getColumnDef(state: ViewState): string {
return `${state.name}${state.id}`; return `${state.name}${state.id}`;
} }

View File

@ -26,7 +26,7 @@
</p> </p>
<span> <span>
<!-- The HTML Editor --> <!-- The HTML Editor -->
<h4>Statute paragraph</h4> <h4 translate>Statute paragraph</h4>
<editor formControlName="text" [init]="tinyMceSettings"></editor> <editor formControlName="text" [init]="tinyMceSettings"></editor>
</span> </span>
</form> </form>
@ -67,7 +67,7 @@
</p> </p>
<span> <span>
<!-- The HTML Editor --> <!-- The HTML Editor -->
<h4>Statute paragraph</h4> <h4 translate>Statute paragraph</h4>
<editor formControlName="text" [init]="tinyMceSettings"></editor> <editor formControlName="text" [init]="tinyMceSettings"></editor>
</span> </span>
</form> </form>

View File

@ -1,7 +1,7 @@
/** /**
* Enum to define different types of notifications. * Enum to define different types of notifications.
*/ */
export enum TypeOfNotificationViewMotion { export enum MotionEditNotificationType {
/** /**
* Type to declare editing a motion. * Type to declare editing a motion.
*/ */
@ -25,7 +25,7 @@ export enum TypeOfNotificationViewMotion {
/** /**
* Class to specify the notifications for editing a motion. * 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. * The id of the motion the user wants to edit.
* Necessary to identify if users edit the same motion. * Necessary to identify if users edit the same motion.
@ -48,5 +48,5 @@ export interface ViewMotionNotificationEditMotion {
* The type of the notification. * The type of the notification.
* Separates if the user is beginning the work or closing the edit-view. * Separates if the user is beginning the work or closing the edit-view.
*/ */
type: TypeOfNotificationViewMotion; type: MotionEditNotificationType;
} }

View File

@ -20,6 +20,9 @@ import { ViewMotionBlock } from './models/view-motion-block';
import { ViewStatuteParagraph } from './models/view-statute-paragraph'; import { ViewStatuteParagraph } from './models/view-statute-paragraph';
import { ViewMotion } from './models/view-motion'; import { ViewMotion } from './models/view-motion';
import { ViewWorkflow } from './models/view-workflow'; 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 = { export const MotionsAppConfig: AppConfig = {
name: 'motions', name: 'motions',
@ -44,6 +47,12 @@ export const MotionsAppConfig: AppConfig = {
viewModel: ViewWorkflow, viewModel: ViewWorkflow,
repository: WorkflowRepositoryService repository: WorkflowRepositoryService
}, },
{
collectionString: 'motions/state',
model: State,
viewModel: ViewState,
repository: StateRepositoryService
},
{ {
collectionString: 'motions/motion-comment-section', collectionString: 'motions/motion-comment-section',
model: MotionCommentSection, model: MotionCommentSection,

View File

@ -70,9 +70,9 @@ export class LocalPermissionsService {
motion.state && motion.state &&
motion.state.allow_support && motion.state.allow_support &&
motion.submitters && motion.submitters &&
motion.submitters.indexOf(this.operator.viewUser) === -1 && !motion.submittersAsUsers.includes(this.operator.viewUser) &&
motion.supporters && motion.supporters &&
motion.supporters.indexOf(this.operator.viewUser) === -1 !motion.supporters.includes(this.operator.viewUser)
); );
} }
case 'unsupport': { case 'unsupport': {

View File

@ -152,7 +152,7 @@ export class MotionCsvExportService {
[ [
{ label: 'Called', map: motion => (motion.sort_parent_id ? '' : motion.identifierOrTitle) }, { label: 'Called', map: motion => (motion.sort_parent_id ? '' : motion.identifierOrTitle) },
{ label: 'Called with', 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' }, { property: 'title' },
{ {
label: 'recommendation', label: 'recommendation',

View File

@ -125,12 +125,12 @@ export class MotionImportService extends BaseImportService<ViewMotion> {
newEntry.motion[this.expectedHeader[idx]] = line[idx]; 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<ViewMotion> = { const entry: NewEntry<ViewMotion> = {
newEntry: newEntry, newEntry: newEntry,
duplicates: updateModels, hasDuplicates: hasDuplicates,
status: updateModels.length ? 'error' : 'new', status: hasDuplicates ? 'error' : 'new',
errors: updateModels.length ? ['Duplicates'] : [] errors: hasDuplicates ? ['Duplicates'] : []
}; };
if (!entry.newEntry.title) { if (!entry.newEntry.title) {
this.setError(entry, 'Title'); this.setError(entry, 'Title');

View File

@ -237,9 +237,9 @@ export class MotionPdfService {
// submitters // submitters
if (!infoToExport || infoToExport.includes('submitters')) { if (!infoToExport || infoToExport.includes('submitters')) {
const submitters = motion.submitters const submitters = motion.submittersAsUsers
.map(submitter => { .map(user => {
return submitter.full_name; return user.full_name;
}) })
.join(', '); .join(', ');
@ -793,7 +793,7 @@ export class MotionPdfService {
text: motion.sort_parent_id ? '' : motion.identifierOrTitle text: motion.sort_parent_id ? '' : motion.identifierOrTitle
}, },
{ 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.title },
{ {
text: motion.recommendation ? this.motionRepo.getExtendedRecommendationLabel(motion) : '' text: motion.recommendation ? this.motionRepo.getExtendedRecommendationLabel(motion) : ''

View File

@ -72,12 +72,12 @@ export class StatuteImportService extends BaseImportService<ViewStatuteParagraph
break; break;
} }
} }
const updateModels = this.repo.getViewModelList().filter(paragraph => paragraph.title === newEntry.title); const hasDuplicates = this.repo.getViewModelList().some(paragraph => paragraph.title === newEntry.title);
return { return {
newEntry: newEntry, newEntry: newEntry,
duplicates: updateModels, hasDuplicates: hasDuplicates,
status: updateModels.length ? 'error' : 'new', status: hasDuplicates ? 'error' : 'new',
errors: updateModels.length ? ['Duplicates'] : [] errors: hasDuplicates ? ['Duplicates'] : []
}; };
} }

View File

@ -1,7 +1,6 @@
import { Countdown } from 'app/shared/models/core/countdown'; import { Countdown } from 'app/shared/models/core/countdown';
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model'; import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { BaseViewModel } from 'app/site/base/base-view-model';
export interface CountdownTitleInformation { export interface CountdownTitleInformation {
title: string; title: string;
@ -39,8 +38,6 @@ export class ViewCountdown extends BaseProjectableViewModel<Countdown> implement
super(Countdown.COLLECTIONSTRING, countdown); super(Countdown.COLLECTIONSTRING, countdown);
} }
public updateDependencies(update: BaseViewModel): void {}
public getSlide(): ProjectorElementBuildDeskriptor { public getSlide(): ProjectorElementBuildDeskriptor {
return { return {
getBasicProjectorElement: options => ({ getBasicProjectorElement: options => ({

View File

@ -28,6 +28,4 @@ export class ViewProjectionDefault extends BaseViewModel<ProjectionDefault>
public constructor(projectionDefault: ProjectionDefault) { public constructor(projectionDefault: ProjectionDefault) {
super(ProjectionDefault.COLLECTIONSTRING, projectionDefault); super(ProjectionDefault.COLLECTIONSTRING, projectionDefault);
} }
public updateDependencies(update: BaseViewModel): void {}
} }

View File

@ -1,7 +1,6 @@
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model'; import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { ProjectorMessage } from 'app/shared/models/core/projector-message'; 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'; import { stripHtmlTags } from 'app/shared/utils/strip-html-tags';
export type ProjectorMessageTitleInformation = object; export type ProjectorMessageTitleInformation = object;
@ -22,8 +21,6 @@ export class ViewProjectorMessage extends BaseProjectableViewModel<ProjectorMess
super(ProjectorMessage.COLLECTIONSTRING, projectorMessage); super(ProjectorMessage.COLLECTIONSTRING, projectorMessage);
} }
public updateDependencies(update: BaseViewModel): void {}
public getSlide(): ProjectorElementBuildDeskriptor { public getSlide(): ProjectorElementBuildDeskriptor {
return { return {
getBasicProjectorElement: options => ({ getBasicProjectorElement: options => ({

View File

@ -106,14 +106,7 @@ export class ViewProjector extends BaseViewModel<Projector> {
return this.projector.show_logo; return this.projector.show_logo;
} }
public constructor(projector: Projector, referenceProjector?: ViewProjector) { public constructor(projector: Projector) {
super(Projector.COLLECTIONSTRING, 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;
}
} }
} }

View File

@ -36,10 +36,4 @@ export class ViewTag extends BaseViewModel<Tag> implements TagTitleInformation,
public getDetailStateURL(): string { public getDetailStateURL(): string {
return `/tags`; return `/tags`;
} }
/**
* Updates the local objects if required
* @param update
*/
public updateDependencies(update: BaseViewModel): void {}
} }

View File

@ -2,9 +2,6 @@ import { Topic } from 'app/shared/models/topics/topic';
import { SearchRepresentation } from 'app/core/ui-services/search.service'; import { SearchRepresentation } from 'app/core/ui-services/search.service';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; 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 { 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'; 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; return this.topic.text;
} }
public constructor( public constructor(topic: Topic) {
topic: Topic, super(Topic.COLLECTIONSTRING, topic);
attachments?: ViewMediafile[],
item?: ViewItem,
listOfSpeakers?: ViewListOfSpeakers
) {
super(Topic.COLLECTIONSTRING, topic, item, listOfSpeakers);
this._attachments = attachments;
} }
/** /**
@ -89,16 +80,4 @@ export class ViewTopic extends BaseViewModelWithAgendaItemAndListOfSpeakers impl
public hasAttachments(): boolean { public hasAttachments(): boolean {
return this.attachments && this.attachments.length > 0; 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;
}
}
}
} }

View File

@ -36,6 +36,4 @@ export class ViewGroup extends BaseViewModel<Group> implements GroupTitleInforma
public hasPermission(perm: string): boolean { public hasPermission(perm: string): boolean {
return this.permissions.includes(perm); return this.permissions.includes(perm);
} }
public updateDependencies(update: BaseViewModel): void {}
} }

View File

@ -29,6 +29,4 @@ export class ViewPersonalNote extends BaseViewModel<PersonalNote> implements Per
return null; return null;
} }
} }
public updateDependencies(update: BaseViewModel): void {}
} }

View File

@ -4,7 +4,6 @@ import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { Searchable } from 'app/site/base/searchable'; import { Searchable } from 'app/site/base/searchable';
import { SearchRepresentation } from 'app/core/ui-services/search.service'; import { SearchRepresentation } from 'app/core/ui-services/search.service';
import { ViewGroup } from './view-group'; import { ViewGroup } from './view-group';
import { BaseViewModel } from 'app/site/base/base-view-model';
export interface UserTitleInformation { export interface UserTitleInformation {
username: string; username: string;
@ -121,9 +120,8 @@ export class ViewUser extends BaseProjectableViewModel<User> implements UserTitl
public getFullName: () => string; public getFullName: () => string;
public getShortName: () => string; public getShortName: () => string;
public constructor(user: User, groups?: ViewGroup[]) { public constructor(user: User) {
super(User.COLLECTIONSTRING, user); super(User.COLLECTIONSTRING, user);
this._groups = groups;
} }
/** /**
@ -151,15 +149,4 @@ export class ViewUser extends BaseProjectableViewModel<User> implements UserTitl
getDialogTitle: () => this.getTitle() 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;
}
}
}
} }

View File

@ -269,14 +269,13 @@ export class UserImportService extends BaseImportService<ViewUser> {
private userToEntry(newUser: ViewCsvCreateUser): NewEntry<ViewUser> { private userToEntry(newUser: ViewCsvCreateUser): NewEntry<ViewUser> {
const newEntry: NewEntry<ViewUser> = { const newEntry: NewEntry<ViewUser> = {
newEntry: newUser, newEntry: newUser,
duplicates: [], hasDuplicates: false,
status: 'new', status: 'new',
errors: [] errors: []
}; };
if (newUser.isValid) { if (newUser.isValid) {
const updateModels = this.repo.getUserDuplicates(newUser); newEntry.hasDuplicates = this.repo.getViewModelList().some(user => user.full_name === newUser.full_name);
if (updateModels.length) { if (newEntry.hasDuplicates) {
newEntry.duplicates = updateModels;
this.setError(newEntry, 'Duplicates'); this.setError(newEntry, 'Duplicates');
} }
} else { } else {

View File

@ -499,7 +499,7 @@ class ListOfSpeakersViewSet(
if not isinstance(speaker_id, int) or speakers.get(speaker_id) is None: if not isinstance(speaker_id, int) or speakers.get(speaker_id) is None:
raise ValidationError({"detail": "Invalid data."}) raise ValidationError({"detail": "Invalid data."})
valid_speakers.append(speakers[speaker_id]) valid_speakers.append(speakers[speaker_id])
weight = 0 weight = 1
with transaction.atomic(): with transaction.atomic():
for speaker in valid_speakers: for speaker in valid_speakers:
speaker.weight = weight speaker.weight = weight

View File

@ -171,3 +171,11 @@ class WorkflowAccessPermissions(BaseAccessPermissions):
""" """
base_permission = "motions.can_see" base_permission = "motions.can_see"
class StateAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for State and StateViewSet.
"""
base_permission = "motions.can_see"

View File

@ -89,6 +89,7 @@ class MotionsAppConfig(AppConfig):
"Motion", "Motion",
"MotionBlock", "MotionBlock",
"Workflow", "Workflow",
"State",
"MotionChangeRecommendation", "MotionChangeRecommendation",
"MotionCommentSection", "MotionCommentSection",
): ):

Some files were not shown because too many files have changed in this diff Show More