Merge pull request #4866 from FinnStutzenstein/RepoUpdate

Generic relations for the repos
This commit is contained in:
Finn Stutzenstein 2019-07-30 09:25:50 +02:00 committed by GitHub
commit 1b26c03ef9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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 = [
ModelConstructor<BaseModel>,
ViewModelConstructor<BaseViewModel>,
BaseRepository<BaseViewModel, BaseModel, TitleInformation>
BaseRepository<BaseViewModel<any>, BaseModel<any>, TitleInformation>
];
/**
@ -46,7 +46,7 @@ export class CollectionStringMapperService {
* @param collectionString
* @param model
*/
public registerCollectionElement<V extends BaseViewModel, M extends BaseModel>(
public registerCollectionElement<V extends BaseViewModel<M>, M extends BaseModel>(
collectionString: string,
model: ModelConstructor<M>,
viewModel: ViewModelConstructor<V>,

View File

@ -16,14 +16,23 @@ import {
IBaseViewModelWithAgendaItem
} from 'app/site/base/base-view-model-with-agenda-item';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { BaseViewModel } from 'app/site/base/base-view-model';
import { Motion } from 'app/shared/models/motions/motion';
import { MotionBlock } from 'app/shared/models/motions/motion-block';
import { Topic } from 'app/shared/models/topics/topic';
import { Assignment } from 'app/shared/models/assignments/assignment';
import { BaseIsAgendaItemContentObjectRepository } from '../base-is-agenda-item-content-object-repository';
import { BaseHasContentObjectRepository } from '../base-has-content-object-repository';
import { BaseHasContentObjectRepository, GenericRelationDefinition } from '../base-has-content-object-repository';
import { Identifiable } from 'app/shared/models/base/identifiable';
import { RelationDefinition } from '../base-repository';
import { ViewMotion } from 'app/site/motions/models/view-motion';
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
import { ViewTopic } from 'app/site/topics/models/view-topic';
import { ViewAssignment } from 'app/site/assignments/models/view-assignment';
const ItemRelations: (RelationDefinition | GenericRelationDefinition)[] = [
{
type: 'generic',
possibleModels: [ViewMotion, ViewMotionBlock, ViewTopic, ViewAssignment],
isVForeign: isBaseViewModelWithAgendaItem,
VForeignVerbose: 'BaseViewModelWithAgendaItem'
}
];
/**
* Repository service for items
@ -58,12 +67,7 @@ export class ItemRepositoryService extends BaseHasContentObjectRepository<
private httpService: HttpService,
private config: ConfigService
) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, Item, [
Topic,
Assignment,
Motion,
MotionBlock
]);
super(DS, dataSend, mapperService, viewModelStoreService, translate, Item, ItemRelations);
this.setSortFunction((a, b) => a.weight - b.weight);
}
@ -83,34 +87,6 @@ export class ItemRepositoryService extends BaseHasContentObjectRepository<
}
};
/**
* Creates the viewItem out of a given item
*
* @param item the item that should be converted to view item
* @returns a new view item
*/
public createViewModel(item: Item): ViewItem {
const contentObject = this.getContentObject(item);
return new ViewItem(item, contentObject);
}
/**
* Returns the corresponding content object to a given {@link Item} as an {@link BaseAgendaItemViewModel}
*
* @param agendaItem the target agenda Item
* @returns the content object of the given item. Might be null if it was not found.
*/
public getContentObject(agendaItem: Item): BaseViewModelWithAgendaItem {
const contentObject = this.viewModelStoreService.get<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
*/

View File

@ -12,20 +12,42 @@ import {
BaseViewModelWithListOfSpeakers,
isBaseViewModelWithListOfSpeakers
} from 'app/site/base/base-view-model-with-list-of-speakers';
import { BaseViewModel } from 'app/site/base/base-view-model';
import { ViewSpeaker } from 'app/site/agenda/models/view-speaker';
import { ViewUser } from 'app/site/users/models/view-user';
import { Identifiable } from 'app/shared/models/base/identifiable';
import { HttpService } from 'app/core/core-services/http.service';
import { BaseIsListOfSpeakersContentObjectRepository } from '../base-is-list-of-speakers-content-object-repository';
import { BaseHasContentObjectRepository } from '../base-has-content-object-repository';
import { Topic } from 'app/shared/models/topics/topic';
import { Assignment } from 'app/shared/models/assignments/assignment';
import { Motion } from 'app/shared/models/motions/motion';
import { MotionBlock } from 'app/shared/models/motions/motion-block';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
import { BaseHasContentObjectRepository, GenericRelationDefinition } from '../base-has-content-object-repository';
import { ItemRepositoryService } from './item-repository.service';
import { User } from 'app/shared/models/users/user';
import { ViewMotion } from 'app/site/motions/models/view-motion';
import { RelationDefinition } from '../base-repository';
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
import { ViewTopic } from 'app/site/topics/models/view-topic';
import { ViewAssignment } from 'app/site/assignments/models/view-assignment';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { ViewUser } from 'app/site/users/models/view-user';
const ListOfSpeakersRelations: (RelationDefinition | GenericRelationDefinition)[] = [
{
type: 'generic',
possibleModels: [ViewMotion, ViewMotionBlock, ViewTopic, ViewAssignment, ViewMediafile],
isVForeign: isBaseViewModelWithListOfSpeakers,
VForeignVerbose: 'BaseViewModelWithListOfSpeakers'
},
{
type: 'nested',
ownKey: 'speakers',
foreignModel: ViewSpeaker,
order: 'weight',
relationDefinition: [
{
type: 'O2M',
ownIdKey: 'user_id',
ownKey: 'user',
foreignModel: ViewUser
}
]
}
];
/**
* Repository service for lists of speakers
@ -60,14 +82,7 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit
private httpService: HttpService,
private itemRepo: ItemRepositoryService
) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, ListOfSpeakers, [
Topic,
Assignment,
Motion,
MotionBlock,
Mediafile,
User
]);
super(DS, dataSend, mapperService, viewModelStoreService, translate, ListOfSpeakers, ListOfSpeakersRelations);
}
public getVerboseName = (plural: boolean = false) => {
@ -93,48 +108,6 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit
}
};
/**
* Creates the viewListOfSpeakers out of a given list of speakers
*
* @param listOfSpeakers the list fo speakers that should be converted to view item
* @returns a new view list fo speakers
*/
public createViewModel(listOfSpeakers: ListOfSpeakers): ViewListOfSpeakers {
const contentObject = this.getContentObject(listOfSpeakers);
const speakers = this.getSpeakers(listOfSpeakers);
return new ViewListOfSpeakers(listOfSpeakers, speakers, contentObject);
}
private getSpeakers(listOfSpeakers: ListOfSpeakers): ViewSpeaker[] {
return listOfSpeakers.speakers.map(speaker => {
const user = this.viewModelStoreService.get(ViewUser, speaker.user_id);
return new ViewSpeaker(speaker, user);
});
}
/**
* Returns the corresponding content object to a given {@link ListOfSpeakers} as an {@link BaseListOfSpeakersViewModel}
*
* @param listOfSpeakers the target list fo speakers
* @returns the content object of the given list of sepakers. Might be null if it was not found.
*/
public getContentObject(listOfSpeakers: ListOfSpeakers): BaseViewModelWithListOfSpeakers {
const contentObject = this.viewModelStoreService.get<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.
* 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 { Assignment } from 'app/shared/models/assignments/assignment';
import { AssignmentRelatedUser } from 'app/shared/models/assignments/assignment-related-user';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { DataSendService } from 'app/core/core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';
import { HttpService } from 'app/core/core-services/http.service';
import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
import { Tag } from 'app/shared/models/core/tag';
import { User } from 'app/shared/models/users/user';
import { ViewAssignment, AssignmentTitleInformation } from 'app/site/assignments/models/view-assignment';
import { ViewItem } from 'app/site/agenda/models/view-item';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewTag } from 'app/site/tags/models/view-tag';
import { ViewUser } from 'app/site/users/models/view-user';
import { ViewAssignmentRelatedUser } from 'app/site/assignments/models/view-assignment-related-user';
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
import { ViewAssignmentPollOption } from 'app/site/assignments/models/view-assignment-poll-option';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../base-is-agenda-item-and-list-of-speakers-content-object-repository';
import { RelationDefinition } from '../base-repository';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { ViewAssignmentPollOption } from 'app/site/assignments/models/view-assignment-poll-option';
const AssignmentRelations: RelationDefinition[] = [
{
type: 'M2M',
ownIdKey: 'tags_id',
ownKey: 'tags',
foreignModel: ViewTag
},
{
type: 'M2M',
ownIdKey: 'attachments_id',
ownKey: 'attachments',
foreignModel: ViewMediafile
},
{
type: 'nested',
ownKey: 'assignment_related_users',
foreignModel: ViewAssignmentRelatedUser,
order: 'weight',
relationDefinition: [
{
type: 'O2M',
ownIdKey: 'user_id',
ownKey: 'user',
foreignModel: ViewUser
}
]
},
{
type: 'nested',
ownKey: 'polls',
foreignModel: ViewAssignmentPoll,
relationDefinition: [
{
type: 'nested',
ownKey: 'options',
foreignModel: ViewAssignmentPollOption,
order: 'weight',
relationDefinition: [
{
type: 'O2M',
ownIdKey: 'user_id',
ownKey: 'user',
foreignModel: ViewUser
}
]
}
]
}
];
/**
* Repository Service for Assignments.
@ -62,7 +107,7 @@ export class AssignmentRepositoryService extends BaseIsAgendaItemAndListOfSpeake
protected translate: TranslateService,
private httpService: HttpService
) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, Assignment, [User, Tag, Mediafile]);
super(DS, dataSend, mapperService, viewModelStoreService, translate, Assignment, AssignmentRelations);
}
public getTitle = (titleInformation: AssignmentTitleInformation) => {
@ -73,48 +118,6 @@ export class AssignmentRepositoryService extends BaseIsAgendaItemAndListOfSpeake
return this.translate.instant(plural ? 'Elections' : 'Election');
};
public createViewModel(assignment: Assignment): ViewAssignment {
const item = this.viewModelStoreService.get(ViewItem, assignment.agenda_item_id);
const listOfSpeakers = this.viewModelStoreService.get(ViewListOfSpeakers, assignment.list_of_speakers_id);
const tags = this.viewModelStoreService.getMany(ViewTag, assignment.tags_id);
const attachments = this.viewModelStoreService.getMany(ViewMediafile, assignment.attachments_id);
const assignmentRelatedUsers = this.createViewAssignmentRelatedUsers(assignment.assignment_related_users);
const assignmentPolls = this.createViewAssignmentPolls(assignment.polls);
return new ViewAssignment(
assignment,
assignmentRelatedUsers,
assignmentPolls,
item,
listOfSpeakers,
tags,
attachments
);
}
private createViewAssignmentRelatedUsers(
assignmentRelatedUsers: AssignmentRelatedUser[]
): ViewAssignmentRelatedUser[] {
return assignmentRelatedUsers
.map(aru => {
const user = this.viewModelStoreService.get(ViewUser, aru.user_id);
return new ViewAssignmentRelatedUser(aru, user);
})
.sort((a, b) => a.weight - b.weight);
}
private createViewAssignmentPolls(assignmentPolls: AssignmentPoll[]): ViewAssignmentPoll[] {
return assignmentPolls.map(poll => {
const options = poll.options
.map(option => {
const user = this.viewModelStoreService.get(ViewUser, option.candidate_id);
return new ViewAssignmentPollOption(option, user);
})
.sort((a, b) => a.weight - b.weight);
return new ViewAssignmentPoll(poll, options);
});
}
/**
* Adds/removes another user to/from the candidates list of an assignment
*

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 { BaseViewModelWithContentObject } from 'app/site/base/base-view-model-with-content-object';
import { ContentObject } from 'app/shared/models/base/content-object';
import { BaseViewModel, TitleInformation } from 'app/site/base/base-view-model';
import { BaseViewModel, TitleInformation, ViewModelConstructor } from 'app/site/base/base-view-model';
import { DataStoreService } from '../core-services/data-store.service';
import { DataSendService } from '../core-services/data-send.service';
import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service';
import { ViewModelStoreService } from '../core-services/view-model-store.service';
import { TranslateService } from '@ngx-translate/core';
import { ModelConstructor } from 'app/shared/models/base/base-model';
/**
* A generic relation for models with a content_object
*/
export interface GenericRelationDefinition<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.
@ -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.
*

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { auditTime } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { BaseViewModel, TitleInformation } from '../../site/base/base-view-model';
import { BaseViewModel, TitleInformation, ViewModelConstructor } from '../../site/base/base-view-model';
import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model';
import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service';
import { DataSendService } from '../core-services/data-send.service';
@ -12,6 +12,89 @@ import { ViewModelStoreService } from '../core-services/view-model-store.service
import { OnAfterAppsLoaded } from '../onAfterAppsLoaded';
import { Collection } from 'app/shared/models/base/collection';
// All "standard" relations.
export type RelationDefinition<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>
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 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
*
@ -87,10 +184,12 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
protected viewModelStoreService: ViewModelStoreService,
protected translate: TranslateService,
protected baseModelCtor: ModelConstructor<M>,
protected depsModelCtors?: ModelConstructor<BaseModel>[]
protected relationDefinitions: RelationDefinition<BaseViewModel>[] = []
) {
this._collectionString = baseModelCtor.COLLECTIONSTRING;
this.groupRelationsByCollections();
// All data is piped through an auditTime of 1ms. This is to prevent massive
// updates, if e.g. an autoupdate with a lot motions come in. The result is just one
// update of the new list instead of many unnecessary updates.
@ -103,7 +202,34 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
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 {
this.baseViewModelCtor = this.collectionStringMapperService.getViewModelConstructor(this.collectionString);
this.DS.clearObservable.subscribe(() => this.clear());
this.translate.onLangChange.subscribe(change => {
this.languageCollator = new Intl.Collator(change.lang);
@ -113,6 +239,10 @@ export abstract class BaseRepository<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
* 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.
*
@ -146,7 +343,7 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
* @returns if at least one model was affected.
*/
public updateDependencies(changedModels: CollectionIds): boolean {
if (!this.depsModelCtors || this.depsModelCtors.length === 0) {
if (!this.relationDefinitions.length) {
return;
}
@ -154,20 +351,25 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
const viewModels = this.getViewModelList();
let somethingUpdated = false;
Object.keys(changedModels).forEach(collection => {
const dependencyChanged: boolean = this.depsModelCtors.some(ctor => {
return ctor.COLLECTIONSTRING === collection;
});
const dependencyChanged: boolean = Object.keys(this.relationsByCollection).includes(collection);
if (!dependencyChanged) {
return;
}
// Ok, we are affected by this collection. Update all viewModels from this repo.
viewModels.forEach(ownViewModel => {
changedModels[collection].forEach(id => {
ownViewModel.updateDependencies(this.viewModelStoreService.get(collection, id));
const relations = this.relationsByCollection[collection];
if (!relations || !relations.length) {
return;
}
relations.forEach(relation => {
changedModels[collection].forEach(id => {
if (this.updateSingleDependency(ownViewModel, relation, collection, id)) {
somethingUpdated = true;
}
});
});
});
somethingUpdated = true;
});
if (somethingUpdated) {
viewModels.forEach(ownViewModel => {
@ -177,9 +379,64 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
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
@ -242,27 +499,6 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
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.
*/

View File

@ -134,14 +134,6 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config,
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
*
@ -160,14 +152,6 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config,
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 {
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 { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { BaseIsListOfSpeakersContentObjectRepository } from '../base-is-list-of-speakers-content-object-repository';
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
import { ViewGroup } from 'app/site/users/models/view-group';
import { Group } from 'app/shared/models/users/group';
import { RelationDefinition } from '../base-repository';
const MediafileRelations: RelationDefinition[] = [
{
type: 'O2M',
ownIdKey: 'parent_id',
ownKey: 'parent',
foreignModel: ViewMediafile
},
{
type: 'M2M',
ownIdKey: 'access_groups_id',
ownKey: 'access_groups',
foreignModel: ViewGroup
},
{
type: 'M2M',
ownIdKey: 'inherited_access_groups_id',
ownKey: 'inherited_access_groups',
foreignModel: ViewGroup
}
];
/**
* Repository for MediaFiles
@ -46,7 +66,7 @@ export class MediafileRepositoryService extends BaseIsListOfSpeakersContentObjec
dataSend: DataSendService,
private httpService: HttpService
) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, Mediafile, [Mediafile, Group]);
super(DS, dataSend, mapperService, viewModelStoreService, translate, Mediafile, MediafileRelations);
this.directoryBehaviorSubject = new BehaviorSubject([]);
this.getViewModelListObservable().subscribe(mediafiles => {
if (mediafiles) {
@ -67,25 +87,6 @@ export class MediafileRepositoryService extends BaseIsListOfSpeakersContentObjec
return this.translate.instant(plural ? 'Files' : 'File');
};
/**
* Creates mediafile ViewModels out of given mediafile objects
*
* @param file mediafile to convert
* @returns a new mediafile ViewModel
*/
public createViewModel(file: Mediafile): ViewMediafile {
const listOfSpeakers = this.viewModelStoreService.get(ViewListOfSpeakers, file.list_of_speakers_id);
const parent = this.viewModelStoreService.get(ViewMediafile, file.parent_id);
const accessGroups = this.viewModelStoreService.getMany(ViewGroup, file.access_groups_id);
let inheritedAccessGroups;
if (file.has_inherited_access_groups) {
inheritedAccessGroups = this.viewModelStoreService.getMany(ViewGroup, <number[]>(
file.inherited_access_groups_id
));
}
return new ViewMediafile(file, listOfSpeakers, parent, accessGroups, inheritedAccessGroups);
}
public async getDirectoryIdByPath(pathSegments: string[]): Promise<number | null> {
let parentId = null;

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { BaseRepository } from '../base-repository';
import { BaseRepository, RelationDefinition } from '../base-repository';
import { Category } from 'app/shared/models/motions/category';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { DataSendService } from '../../core-services/data-send.service';
@ -13,6 +13,15 @@ import { ViewModelStoreService } from 'app/core/core-services/view-model-store.s
import { Motion } from 'app/shared/models/motions/motion';
import { TreeIdNode } from 'app/core/ui-services/tree.service';
const CategoryRelations: RelationDefinition[] = [
{
type: 'O2M',
ownIdKey: 'parent_id',
ownKey: 'parent',
foreignModel: ViewCategory
}
];
/**
* Repository Services for Categories
*
@ -47,7 +56,7 @@ export class CategoryRepositoryService extends BaseRepository<ViewCategory, Cate
translate: TranslateService,
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);
}
@ -62,11 +71,6 @@ export class CategoryRepositoryService extends BaseRepository<ViewCategory, Cate
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.
*

View File

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

View File

@ -14,8 +14,6 @@ import { MotionRepositoryService } from './motion-repository.service';
import { ViewMotion } from 'app/site/motions/models/view-motion';
import { ViewMotionBlock, MotionBlockTitleInformation } from 'app/site/motions/models/view-motion-block';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewItem } from 'app/site/agenda/models/view-item';
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../base-is-agenda-item-and-list-of-speakers-content-object-repository';
/**
@ -59,18 +57,6 @@ export class MotionBlockRepositoryService extends BaseIsAgendaItemAndListOfSpeak
return this.translate.instant(plural ? 'Motion blocks' : 'Motion block');
};
/**
* Converts a given motion block into a ViewModel
*
* @param block a motion block
* @returns a new ViewMotionBlock
*/
protected createViewModel(block: MotionBlock): ViewMotionBlock {
const item = this.viewModelStoreService.get(ViewItem, block.agenda_item_id);
const listOfSpeakers = this.viewModelStoreService.get(ViewListOfSpeakers, block.list_of_speakers_id);
return new ViewMotionBlock(block, item, listOfSpeakers);
}
/**
* Removes the motion block id from the given motion
*

View File

@ -4,19 +4,33 @@ import { TranslateService } from '@ngx-translate/core';
import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';
import { BaseRepository } from '../base-repository';
import { BaseRepository, RelationDefinition } from '../base-repository';
import {
ViewMotionCommentSection,
MotionCommentSectionTitleInformation
} from 'app/site/motions/models/view-motion-comment-section';
import { MotionCommentSection } from 'app/shared/models/motions/motion-comment-section';
import { Group } from 'app/shared/models/users/group';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { HttpService } from 'app/core/core-services/http.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewGroup } from 'app/site/users/models/view-group';
import { ViewMotion } from 'app/site/motions/models/view-motion';
const MotionCommentSectionRelations: RelationDefinition[] = [
{
type: 'M2M',
ownIdKey: 'read_groups_id',
ownKey: 'read_groups',
foreignModel: ViewGroup
},
{
type: 'M2M',
ownIdKey: 'write_groups_id',
ownKey: 'write_groups',
foreignModel: ViewGroup
}
];
/**
* Repository Services for Categories
*
@ -53,7 +67,15 @@ export class MotionCommentSectionRepositoryService extends BaseRepository<
translate: TranslateService,
private http: HttpService
) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, MotionCommentSection, [Group]);
super(
DS,
dataSend,
mapperService,
viewModelStoreService,
translate,
MotionCommentSection,
MotionCommentSectionRelations
);
this.viewModelSortFn = (a: ViewMotionCommentSection, b: ViewMotionCommentSection) => {
if (a.weight === b.weight) {
@ -72,18 +94,6 @@ export class MotionCommentSectionRepositoryService extends BaseRepository<
return this.translate.instant(plural ? 'Comment sections' : 'Comment section');
};
/**
* Creates the ViewModel for the MotionComment Section
*
* @param section the MotionCommentSection the View Model should be created of
* @returns the View Model representation of the MotionCommentSection
*/
protected createViewModel(section: MotionCommentSection): ViewMotionCommentSection {
const readGroups = this.viewModelStoreService.getMany(ViewGroup, section.read_groups_id);
const writeGroups = this.viewModelStoreService.getMany(ViewGroup, section.write_groups_id);
return new ViewMotionCommentSection(section, readGroups, writeGroups);
}
/**
* Saves a comment made at a MotionCommentSection. Does an update, if
* there is a comment text. Deletes the comment, if the text is empty.

View File

@ -5,42 +5,35 @@ import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Category } from 'app/shared/models/motions/category';
import { ChangeRecoMode, MotionTitleInformation, ViewMotion } from 'app/site/motions/models/view-motion';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService, CollectionIds } from 'app/core/core-services/data-store.service';
import { DataStoreService } from 'app/core/core-services/data-store.service';
import { DiffService, DiffLinesInParagraph } from 'app/core/ui-services/diff.service';
import { HttpService } from 'app/core/core-services/http.service';
import { LinenumberingService, LineNumberRange } from '../../ui-services/linenumbering.service';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
import { Motion } from 'app/shared/models/motions/motion';
import { MotionBlock } from 'app/shared/models/motions/motion-block';
import { MotionChangeRecommendation } from 'app/shared/models/motions/motion-change-reco';
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
import { TreeIdNode } from 'app/core/ui-services/tree.service';
import { User } from 'app/shared/models/users/user';
import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-motion-change-recommendation';
import { ViewMotionAmendedParagraph } from 'app/site/motions/models/view-motion-amended-paragraph';
import { ViewUnifiedChange } from 'app/shared/models/motions/view-unified-change';
import { ViewStatuteParagraph } from 'app/site/motions/models/view-statute-paragraph';
import { Workflow } from 'app/shared/models/motions/workflow';
import { WorkflowState } from 'app/shared/models/motions/workflow-state';
import { Tag } from 'app/shared/models/core/tag';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewCategory } from 'app/site/motions/models/view-category';
import { ViewUser } from 'app/site/users/models/view-user';
import { ViewWorkflow } from 'app/site/motions/models/view-workflow';
import { ViewItem } from 'app/site/agenda/models/view-item';
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { ViewTag } from 'app/site/tags/models/view-tag';
import { ViewPersonalNote } from 'app/site/users/models/view-personal-note';
import { OperatorService } from 'app/core/core-services/operator.service';
import { PersonalNote, PersonalNoteContent } from 'app/shared/models/users/personal-note';
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
import { PersonalNoteContent } from 'app/shared/models/users/personal-note';
import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../base-is-agenda-item-and-list-of-speakers-content-object-repository';
import { RelationDefinition } from '../base-repository';
import { ViewState } from 'app/site/motions/models/view-state';
import { ViewSubmitter } from 'app/site/motions/models/view-submitter';
type SortProperty = 'weight' | 'identifier';
@ -74,6 +67,84 @@ export interface ParagraphToChoose {
lineTo: number;
}
const MotionRelations: RelationDefinition[] = [
{
type: 'O2M',
ownIdKey: 'state_id',
ownKey: 'state',
foreignModel: ViewState
},
{
type: 'O2M',
ownIdKey: 'recommendation_id',
ownKey: 'recommendation',
foreignModel: ViewState
},
{
type: 'O2M',
ownIdKey: 'workflow_id',
ownKey: 'workflow',
foreignModel: ViewWorkflow
},
{
type: 'O2M',
ownIdKey: 'category_id',
ownKey: 'category',
foreignModel: ViewCategory
},
{
type: 'O2M',
ownIdKey: 'motion_block_id',
ownKey: 'motion_block',
foreignModel: ViewMotionBlock
},
{
type: 'nested',
ownKey: 'submitters',
foreignModel: ViewSubmitter,
order: 'weight',
relationDefinition: [
{
type: 'O2M',
ownIdKey: 'user_id',
ownKey: 'user',
foreignModel: ViewUser
}
]
},
{
type: 'M2M',
ownIdKey: 'supporters_id',
ownKey: 'supporters',
foreignModel: ViewUser
},
{
type: 'M2M',
ownIdKey: 'attachments_id',
ownKey: 'attachments',
foreignModel: ViewMediafile
},
{
type: 'M2M',
ownIdKey: 'tags_id',
ownKey: 'tags',
foreignModel: ViewTag
},
{
type: 'O2M',
ownIdKey: 'parent_id',
ownKey: 'parent',
foreignModel: ViewMotion
},
{
type: 'M2M',
ownIdKey: 'change_recommendations_id',
ownKey: 'changeRecommendations',
foreignModel: ViewMotionChangeRecommendation
}
// Personal notes are dynamically added in the repo.
];
/**
* Repository Services for motions (and potentially categories)
*
@ -126,23 +197,36 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
private readonly diff: DiffService,
private operator: OperatorService
) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, Motion, [
Category,
User,
Workflow,
MotionBlock,
Mediafile,
Tag,
MotionChangeRecommendation,
PersonalNote,
Motion
]);
super(DS, dataSend, mapperService, viewModelStoreService, translate, Motion, MotionRelations);
config.get<SortProperty>('motions_motions_sorting').subscribe(conf => {
this.sortProperty = conf;
this.setConfigSortFn();
});
}
/**
* Adds the personal note custom relation to the relation definitions.
*/
protected groupRelationsByCollections(): void {
this.relationDefinitions.push({
type: 'custom',
foreignModel: ViewPersonalNote,
setRelations: (motion: Motion, viewMotion: ViewMotion) => {
viewMotion.personalNote = this.getPersonalNoteForMotion(motion);
},
updateDependency: (viewMotion: ViewMotion, viewPersonalNote: ViewPersonalNote) => {
const personalNoteContent = viewPersonalNote.getNoteContent(this.collectionString, viewMotion.id);
if (!personalNoteContent) {
return false;
}
viewMotion.personalNote = personalNoteContent;
return true;
}
});
super.groupRelationsByCollections();
}
public getTitle = (titleInformation: MotionTitleInformation) => {
if (titleInformation.identifier) {
return titleInformation.identifier + ': ' + titleInformation.title;
@ -183,64 +267,20 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
return this.translate.instant(plural ? 'Motions' : 'Motion');
};
/**
* Converts a motion to a ViewMotion and adds it to the store.
*
* Foreign references of the motion will be resolved (e.g submitters to users)
* Expandable to all (server side) changes that might occur on the motion object.
*
* @param motion blank motion domain object
*/
protected createViewModel(motion: Motion): ViewMotion {
const category = this.viewModelStoreService.get(ViewCategory, motion.category_id);
const submitters = this.viewModelStoreService.getMany(ViewUser, motion.sorted_submitters_id);
const supporters = this.viewModelStoreService.getMany(ViewUser, motion.supporters_id);
const workflow = this.viewModelStoreService.get(ViewWorkflow, motion.workflow_id);
const item = this.viewModelStoreService.get(ViewItem, motion.agenda_item_id);
const listOfSpeakers = this.viewModelStoreService.get(ViewListOfSpeakers, motion.list_of_speakers_id);
const block = this.viewModelStoreService.get(ViewMotionBlock, motion.motion_block_id);
const attachments = this.viewModelStoreService.getMany(ViewMediafile, motion.attachments_id);
const tags = this.viewModelStoreService.getMany(ViewTag, motion.tags_id);
const parent = this.viewModelStoreService.get(ViewMotion, motion.parent_id);
const amendments = this.viewModelStoreService.filter(ViewMotion, m => m.parent_id && m.parent_id === motion.id);
const changeRecommendations = this.viewModelStoreService.filter(
ViewMotionChangeRecommendation,
cr => cr.motion_id === motion.id
);
let state: WorkflowState = null;
if (workflow) {
state = workflow.getStateById(motion.state_id);
}
const personalNote = this.getPersonalNoteForMotion(motion.id);
const viewMotion = new ViewMotion(
motion,
category,
submitters,
supporters,
workflow,
state,
item,
listOfSpeakers,
block,
attachments,
tags,
parent,
changeRecommendations,
amendments,
personalNote
);
viewMotion.getIdentifierOrTitle = () => this.getIdentifierOrTitle(viewMotion);
viewMotion.getProjectorTitle = () => this.getAgendaSlideTitle(viewMotion);
return viewMotion;
protected createViewModelWithTitles(model: Motion): ViewMotion {
const viewModel = super.createViewModelWithTitles(model);
viewModel.getIdentifierOrTitle = () => this.getIdentifierOrTitle(viewModel);
viewModel.getProjectorTitle = () => this.getAgendaSlideTitle(viewModel);
return viewModel;
}
/**
* Get the personal note content for one motion by their id
*
* @param motionId the id of the motion
* @param motion the motion
* @returns the personal note content for this motion or null
*/
private getPersonalNoteForMotion(motionId: number): PersonalNoteContent | null {
private getPersonalNoteForMotion(motion: Motion): PersonalNoteContent | null {
if (this.operator.isAnonymous) {
return;
}
@ -248,64 +288,17 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
const personalNote = this.viewModelStoreService.find(ViewPersonalNote, pn => {
return pn.userId === this.operator.user.id;
});
if (!personalNote) {
return;
}
const notes = personalNote.notes;
const collection = Motion.COLLECTIONSTRING;
if (notes && notes[collection] && notes[collection][motionId]) {
return notes[collection][motionId];
if (notes && notes[collection] && notes[collection][motion.id]) {
return notes[collection][motion.id];
}
}
/**
* Special handling of updating personal notes.
* @override
*/
public updateDependencies(changedModels: CollectionIds): boolean {
if (!this.depsModelCtors || this.depsModelCtors.length === 0) {
return;
}
const viewModels = this.getViewModelList();
let somethingUpdated = false;
Object.keys(changedModels).forEach(collection => {
const dependencyChanged: boolean = this.depsModelCtors.some(ctor => {
return ctor.COLLECTIONSTRING === collection;
});
if (!dependencyChanged) {
return;
}
// Do not update personal notes, if the operator is anonymous
if (collection === PersonalNote.COLLECTIONSTRING && this.operator.isAnonymous) {
return;
}
viewModels.forEach(ownViewModel => {
changedModels[collection].forEach(id => {
const viewModel = this.viewModelStoreService.get(collection, id);
// Only update the personal note, if the operator is the right user.
if (
collection === PersonalNote.COLLECTIONSTRING &&
(<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
*
@ -757,20 +750,6 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
.filter((para: ViewMotionAmendedParagraph) => para !== null);
}
/**
* Returns motion duplicates (sharing the identifier)
*
* @param viewMotion the ViewMotion to compare against the list of Motions
* in the data
* @returns An Array of ViewMotions with the same identifier of the input, or an empty array
*/
public getMotionDuplicates(motion: ViewMotion): ViewMotion[] {
const duplicates = this.DS.filter(Motion, item => motion.identifier === item.identifier);
const viewMotions: ViewMotion[] = [];
duplicates.forEach(item => viewMotions.push(this.createViewModel(item)));
return viewMotions;
}
/**
* Sends a request to the server, creating a new poll for the motion
*/

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) => {
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 { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';
import { BaseRepository } from '../base-repository';
import { BaseRepository, RelationDefinition } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { WorkflowState } from 'app/shared/models/motions/workflow-state';
import { ViewMotion } from 'app/site/motions/models/view-motion';
import { HttpService } from 'app/core/core-services/http.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewState } from 'app/site/motions/models/view-state';
const WorkflowRelations: RelationDefinition[] = [
{
type: 'M2M',
ownIdKey: 'states_id',
ownKey: 'states',
foreignModel: ViewState
},
{
type: 'O2M',
ownIdKey: 'first_state_id',
ownKey: 'first_state',
foreignModel: ViewState
}
];
/**
* Repository Services for Categories
* Repository Services for Workflows
*
* The repository is meant to process domain objects (those found under
* shared/models), so components can display them and interact with them.
@ -27,11 +41,6 @@ import { ViewModelStoreService } from 'app/core/core-services/view-model-store.s
providedIn: 'root'
})
export class WorkflowRepositoryService extends BaseRepository<ViewWorkflow, Workflow, WorkflowTitleInformation> {
/**
* The url to state on rest
*/
private restStateUrl = '/rest/motions/state/';
/**
* Creates a WorkflowRepository
* Converts existing and incoming workflow to ViewWorkflows
@ -46,16 +55,9 @@ export class WorkflowRepositoryService extends BaseRepository<ViewWorkflow, Work
dataSend: DataSendService,
mapperService: CollectionStringMapperService,
viewModelStoreService: ViewModelStoreService,
translate: TranslateService,
private httpService: HttpService
translate: TranslateService
) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, Workflow);
this.viewModelListSubject.subscribe(models => {
if (models && models.length > 0) {
this.initSorting(models);
}
});
super(DS, dataSend, mapperService, viewModelStoreService, translate, Workflow, WorkflowRelations);
}
public getTitle = (titleInformation: WorkflowTitleInformation) => {
@ -66,86 +68,14 @@ export class WorkflowRepositoryService extends BaseRepository<ViewWorkflow, Work
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
*
* @param motions The motions to get the workflows from
* @returns The workflow states to the given motion
*/
public getWorkflowStatesForMotions(motions: ViewMotion[]): WorkflowState[] {
let states: WorkflowState[] = [];
public getWorkflowStatesForMotions(motions: ViewMotion[]): ViewState[] {
let states: ViewState[] = [];
const workflowIds = motions
.map(motion => motion.workflow_id)
.filter((value, index, self) => self.indexOf(value) === index);

View File

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

View File

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

View File

@ -38,8 +38,4 @@ export class ProjectorMessageRepositoryService extends BaseRepository<
public getVerboseName = (plural: boolean = false) => {
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 { BaseRepository } from '../base-repository';
import { BaseRepository, RelationDefinition } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { DataSendService } from '../../core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service';
@ -21,6 +21,15 @@ export enum ScrollScaleDirection {
Reset = 'reset'
}
const ProjectorRelations: RelationDefinition[] = [
{
type: 'O2M',
ownIdKey: 'reference_projector_id',
ownKey: 'referenceProjector',
foreignModel: ViewProjector
}
];
/**
* Manages all projector instances.
*/
@ -44,7 +53,7 @@ export class ProjectorRepositoryService extends BaseRepository<ViewProjector, Pr
translate: TranslateService,
private http: HttpService
) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, Projector, [Projector]);
super(DS, dataSend, mapperService, viewModelStoreService, translate, Projector, ProjectorRelations);
}
public getTitle = (titleInformation: ProjectorTitleInformation) => {
@ -55,10 +64,6 @@ export class ProjectorRepositoryService extends BaseRepository<ViewProjector, Pr
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
*/

View File

@ -52,10 +52,6 @@ export class TagRepositoryService extends BaseRepository<ViewTag, Tag, TagTitleI
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'
*/

View File

@ -5,14 +5,21 @@ import { TranslateService } from '@ngx-translate/core';
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
import { DataStoreService } from 'app/core/core-services/data-store.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 { ViewTopic, TopicTitleInformation } from 'app/site/topics/models/view-topic';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
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 { RelationDefinition } from '../base-repository';
const TopicRelations: RelationDefinition[] = [
{
type: 'M2M',
ownIdKey: 'attachments_id',
ownKey: 'attachments',
foreignModel: ViewMediafile
}
];
/**
* Repository for topics
@ -39,7 +46,7 @@ export class TopicRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCon
viewModelStoreService: ViewModelStoreService,
translate: TranslateService
) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, Topic, [Mediafile]);
super(DS, dataSend, mapperService, viewModelStoreService, translate, Topic, TopicRelations);
}
public getTitle = (titleInformation: TopicTitleInformation) => {
@ -63,29 +70,4 @@ export class TopicRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCon
public getVerboseName = (plural: boolean = false) => {
return this.translate.instant(plural ? 'Topics' : 'Topic');
};
/**
* Creates a new viewModel out of the given model
*
* @param topic The topic that shall be converted into a view topic
* @returns a new view topic
*/
public createViewModel(topic: Topic): ViewTopic {
const attachments = this.viewModelStoreService.getMany(ViewMediafile, topic.attachments_id);
const item = this.viewModelStoreService.get(ViewItem, topic.agenda_item_id);
const listOfSpeakers = this.viewModelStoreService.get(ViewListOfSpeakers, topic.list_of_speakers_id);
return new ViewTopic(topic, attachments, item, listOfSpeakers);
}
/**
* Returns an array of all duplicates for a topic
*
* @param topic
*/
public getTopicDuplicates(topic: ViewTopic): ViewTopic[] {
const duplicates = this.DS.filter(Topic, item => topic.title === item.title);
const viewTopics: ViewTopic[] = [];
duplicates.forEach(item => viewTopics.push(this.createViewModel(item)));
return viewTopics;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 { 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 motion_block_id: number;
public origin: string;
public submitters: MotionSubmitter[];
public submitters: Submitter[];
public supporters_id: number[];
public comments: MotionComment[];
public workflow_id: number;
@ -48,6 +48,7 @@ export class Motion extends BaseModelWithAgendaItemAndListOfSpeakers<Motion> {
public sort_parent_id: number;
public created: string;
public last_modified: string;
public change_recommendations_id: number[];
public constructor(input?: any) {
super(Motion.COLLECTIONSTRING, input);
@ -58,9 +59,9 @@ export class Motion extends BaseModelWithAgendaItemAndListOfSpeakers<Motion> {
*/
public get sorted_submitters_id(): number[] {
return this.submitters
.sort((a: MotionSubmitter, b: MotionSubmitter) => {
.sort((a: Submitter, b: Submitter) => {
return a.weight - b.weight;
})
.map((submitter: MotionSubmitter) => submitter.user_id);
.map((submitter: Submitter) => submitter.user_id);
}
}

View File

@ -1,5 +1,4 @@
import { Deserializer } from '../base/deserializer';
import { Workflow } from './workflow';
import { BaseModel } from '../base/base-model';
/**
* Specifies if an amendment of this state/recommendation should be merged into the motion
@ -16,7 +15,9 @@ export enum MergeAmendment {
* Part of the 'states'-array in motion/workflow
* @ignore
*/
export class WorkflowState extends Deserializer {
export class State extends BaseModel<State> {
public static COLLECTIONSTRING = 'motions/state';
public id: number;
public name: string;
public recommendation_label: string;
@ -37,23 +38,7 @@ export class WorkflowState extends Deserializer {
* @param input If given, it will be deserialized
*/
public constructor(input?: any) {
super(input);
}
/**
* return a list of the next possible states.
* Also adds the current state.
*/
public getNextStates(workflow: Workflow): WorkflowState[] {
return workflow.states.filter(state => {
return this.next_states_id.includes(state.id);
});
}
public getPreviousStates(workflow: Workflow): WorkflowState[] {
return workflow.states.filter(state => {
return state.next_states_id.includes(this.id);
});
super(State.COLLECTIONSTRING, input);
}
public toString = (): string => {

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 { WorkflowState } from './workflow-state';
/**
* Representation of a motion workflow. Has the nested property 'states'
@ -10,40 +9,10 @@ export class Workflow extends BaseModel<Workflow> {
public id: number;
public name: string;
public states: WorkflowState[];
public states_id: number[];
public first_state_id: number;
public constructor(input?: any) {
super(Workflow.COLLECTIONSTRING, input);
}
/**
* Check if the containing @link{WorkflowState}s contain a given ID
* @param id The State ID
*/
public isStateContained(obj: number | WorkflowState): boolean {
let id: number;
if (obj instanceof WorkflowState) {
id = obj.id;
} else {
id = obj;
}
return this.states.some(state => state.id === id);
}
/**
* Sorts the states of this workflow
*/
public sortStates(): void {
this.states.sort((a, b) => (a.id > b.id ? 1 : -1));
}
public deserialize(input: any): void {
Object.assign(this, input);
if (input.states instanceof Array) {
this.states = input.states.map(workflowStateData => new WorkflowState(workflowStateData));
}
}
}

View File

@ -1,8 +1,5 @@
import { Item, ItemVisibilityChoices } from 'app/shared/models/agenda/item';
import {
BaseViewModelWithAgendaItem,
isBaseViewModelWithAgendaItem
} from 'app/site/base/base-view-model-with-agenda-item';
import { BaseViewModelWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item';
import { BaseViewModelWithContentObject } from 'app/site/base/base-view-model-with-content-object';
import { ContentObject } from 'app/shared/models/base/content-object';
@ -87,7 +84,7 @@ export class ViewItem extends BaseViewModelWithContentObject<Item, BaseViewModel
return this.item.parent_id;
}
public constructor(item: Item, contentObject?: BaseViewModelWithAgendaItem) {
super(Item.COLLECTIONSTRING, item, isBaseViewModelWithAgendaItem, 'BaseViewModelWithAgendaItem', contentObject);
public constructor(item: Item) {
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 { ProjectorElementBuildDeskriptor, Projectable } from 'app/site/base/projectable';
import { ListOfSpeakers } from 'app/shared/models/agenda/list-of-speakers';
import { ViewSpeaker, SpeakerState } from './view-speaker';
import {
BaseViewModelWithListOfSpeakers,
isBaseViewModelWithListOfSpeakers
} from 'app/site/base/base-view-model-with-list-of-speakers';
import { ViewUser } from 'app/site/users/models/view-user';
import { BaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers';
import { BaseViewModelWithContentObject } from 'app/site/base/base-view-model-with-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[] {
return this._speakers;
return this._speakers || [];
}
public get title_information(): object {
@ -53,19 +48,8 @@ export class ViewListOfSpeakers extends BaseViewModelWithContentObject<ListOfSpe
return `/agenda/speakers/${this.id}`;
}
public constructor(
listOfSpeakers: ListOfSpeakers,
speakers: ViewSpeaker[],
contentObject?: BaseViewModelWithListOfSpeakers
) {
super(
Item.COLLECTIONSTRING,
listOfSpeakers,
isBaseViewModelWithListOfSpeakers,
'BaseViewModelWithListOfSpeakers',
contentObject
);
this._speakers = speakers;
public constructor(listOfSpeakers: ListOfSpeakers) {
super(Item.COLLECTIONSTRING, listOfSpeakers);
}
public getProjectorTitle(): string {
@ -84,12 +68,4 @@ export class ViewListOfSpeakers extends BaseViewModelWithContentObject<ListOfSpe
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 { Speaker } from 'app/shared/models/agenda/speaker';
import { ViewUser } from 'app/site/users/models/view-user';
import { Updateable } from 'app/site/base/updateable';
import { Identifiable } from 'app/shared/models/base/identifiable';
/**
* Determine the state of the speaker
@ -16,15 +14,15 @@ export enum SpeakerState {
/**
* Provides "safe" access to a speaker with all it's components
*/
export class ViewSpeaker implements Updateable, Identifiable {
private _speaker: Speaker;
export class ViewSpeaker extends BaseViewModel<Speaker> {
public static COLLECTIONSTRING = Speaker.COLLECTIONSTRING;
private _user?: ViewUser;
public get speaker(): Speaker {
return this._speaker;
return this._model;
}
public get user(): ViewUser {
public get user(): ViewUser | null {
return this._user;
}
@ -82,20 +80,11 @@ export class ViewSpeaker implements Updateable, Identifiable {
return this.user ? this.user.gender : '';
}
public constructor(speaker: Speaker, user?: ViewUser) {
this._speaker = speaker;
this._user = user;
public constructor(speaker: Speaker) {
super(Speaker.COLLECTIONSTRING, speaker);
}
public getTitle = () => {
return this.name;
};
public updateDependencies(update: BaseViewModel): boolean {
if (update instanceof ViewUser && update.id === this.speaker.user_id) {
this._user = update;
return true;
}
return false;
}
}

View File

@ -100,7 +100,7 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
newEntry[this.expectedHeader[idx]] = line[idx];
}
}
const updateModels = this.repo.getTopicDuplicates(newEntry) as ViewCreateTopic[];
const hasDuplicates = this.repo.getViewModelList().some(topic => topic.title === newEntry.title);
// set type to 'public' if none is given in import
if (!newEntry.type) {
@ -108,12 +108,11 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
}
const mappedEntry: NewEntry<ViewCreateTopic> = {
newEntry: newEntry,
duplicates: [],
hasDuplicates: hasDuplicates,
status: 'new',
errors: []
};
if (updateModels.length) {
mappedEntry.duplicates = updateModels;
if (hasDuplicates) {
this.setError(mappedEntry, 'Duplicates');
}
if (hasErrors) {
@ -198,17 +197,14 @@ export class AgendaImportService extends BaseImportService<ViewCreateTopic> {
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> = {
newEntry: newTopic,
duplicates: [],
hasDuplicates: hasDuplicates,
status: 'new',
errors: []
};
const duplicates = this.repo.getTopicDuplicates(newTopic);
if (duplicates.length) {
// TODO duplicates are not really ViewCreateTopics, but ViewTopics.
// TODO this should be fine as the duplicates will not be created
newEntry.duplicates = duplicates as ViewCreateTopic[];
if (hasDuplicates) {
this.setError(newEntry, 'Duplicates');
}
newEntries.push(newEntry);

View File

@ -162,10 +162,10 @@
<div>
<div
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
[input]="assignment.assignmentRelatedUsers"
[input]="assignment.assignment_related_users"
[live]="true"
[count]="true"
[enable]="hasPerms('addOthers')"

View File

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

View File

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

View File

@ -169,7 +169,8 @@
<div *ngIf="!pollData">
<h4 translate>Candidates</h4>
<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>

View File

@ -236,7 +236,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
}
// TODO additional conditions: assignment not finished?
const viewAssignmentRelatedUser = this.assignment.assignmentRelatedUsers.find(
const viewAssignmentRelatedUser = this.assignment.assignment_related_users.find(
user => user.user_id === option.user_id
);
if (viewAssignmentRelatedUser) {

View File

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

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

View File

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

View File

@ -2,14 +2,11 @@ import { Assignment } from 'app/shared/models/assignments/assignment';
import { SearchRepresentation } from 'app/core/ui-services/search.service';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { ViewUser } from 'app/site/users/models/view-user';
import { ViewItem } from 'app/site/agenda/models/view-item';
import { ViewTag } from 'app/site/tags/models/view-tag';
import { BaseViewModel } from 'app/site/base/base-view-model';
import { ViewAssignmentRelatedUser } from './view-assignment-related-user';
import { ViewAssignmentPoll } from './view-assignment-poll';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { BaseViewModelWithAgendaItemAndListOfSpeakers } from 'app/site/base/base-view-model-with-agenda-item-and-list-of-speakers';
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
import { TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item';
export interface AssignmentTitleInformation extends TitleInformationWithAgendaItem {
@ -43,8 +40,8 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers
implements AssignmentTitleInformation {
public static COLLECTIONSTRING = Assignment.COLLECTIONSTRING;
private _assignmentRelatedUsers: ViewAssignmentRelatedUser[];
private _assignmentPolls: ViewAssignmentPoll[];
private _assignment_related_users?: ViewAssignmentRelatedUser[];
private _polls?: ViewAssignmentPoll[];
private _tags?: ViewTag[];
private _attachments?: ViewMediafile[];
@ -53,7 +50,7 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers
}
public get polls(): ViewAssignmentPoll[] {
return this._assignmentPolls;
return this._polls || [];
}
public get title(): string {
@ -69,21 +66,29 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers
}
public get candidates(): ViewUser[] {
return this._assignmentRelatedUsers.map(aru => aru.user);
return this.assignment_related_users.map(aru => aru.user).filter(x => !!x);
}
public get assignmentRelatedUsers(): ViewAssignmentRelatedUser[] {
return this._assignmentRelatedUsers;
public get assignment_related_users(): ViewAssignmentRelatedUser[] {
return this._assignment_related_users || [];
}
public get tags(): ViewTag[] {
return this._tags || [];
}
public get tags_id(): number[] {
return this.assignment.tags_id;
}
public get attachments(): ViewMediafile[] {
return this._attachments || [];
}
public get attachments_id(): number[] {
return this.assignment.attachments_id;
}
/**
* unknown where the identifier to the phase is get
*/
@ -117,46 +122,11 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers
* @returns the amount of candidates in the assignment's candidate list
*/
public get candidateAmount(): number {
return this._assignmentRelatedUsers ? this._assignmentRelatedUsers.length : 0;
return this._assignment_related_users ? this._assignment_related_users.length : 0;
}
public constructor(
assignment: Assignment,
assignmentRelatedUsers: ViewAssignmentRelatedUser[],
assignmentPolls: ViewAssignmentPoll[],
item?: ViewItem,
listOfSpeakers?: ViewListOfSpeakers,
tags?: ViewTag[],
attachments?: ViewMediafile[]
) {
super(Assignment.COLLECTIONSTRING, assignment, item, listOfSpeakers);
this._assignmentRelatedUsers = assignmentRelatedUsers;
this._assignmentPolls = assignmentPolls;
this._tags = tags;
this._attachments = attachments;
}
public updateDependencies(update: BaseViewModel): void {
super.updateDependencies(update);
if (update instanceof ViewTag && this.assignment.tags_id.includes(update.id)) {
const tagIndex = this._tags.findIndex(_tag => _tag.id === update.id);
if (tagIndex < 0) {
this._tags.push(update);
} else {
this._tags[tagIndex] = update;
}
} else if (update instanceof ViewUser) {
this.assignmentRelatedUsers.forEach(aru => aru.updateDependencies(update));
this.polls.forEach(poll => poll.updateDependencies(update));
} else if (update instanceof ViewMediafile && this.assignment.attachments_id.includes(update.id)) {
const mediafileIndex = this._attachments.findIndex(_mediafile => _mediafile.id === update.id);
if (mediafileIndex < 0) {
this._attachments.push(update);
} else {
this._attachments[mediafileIndex] = update;
}
}
public constructor(assignment: Assignment) {
super(Assignment.COLLECTIONSTRING, assignment);
}
public formatForSearch(): SearchRepresentation {

View File

@ -127,7 +127,7 @@ export class AssignmentPdfService {
*/
private createCandidateList(assignment: ViewAssignment): object {
if (assignment.phase !== 2) {
const candidates = assignment.assignmentRelatedUsers.sort((a, b) => a.weight - b.weight);
const candidates = assignment.assignment_related_users.sort((a, b) => a.weight - b.weight);
const candidatesText = `${this.translate.instant('Candidates')}: `;
const userList = candidates.map(candidate => {

View File

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

View File

@ -8,7 +8,6 @@ import {
isBaseViewModelWithListOfSpeakers,
IBaseViewModelWithListOfSpeakers
} from './base-view-model-with-list-of-speakers';
import { BaseViewModel } from './base-view-model';
export function isBaseViewModelWithAgendaItemAndListOfSpeakers(
obj: any
@ -23,7 +22,7 @@ export abstract class BaseViewModelWithAgendaItemAndListOfSpeakers<
M extends BaseModelWithAgendaItemAndListOfSpeakers = any
> extends BaseProjectableViewModel implements IBaseViewModelWithAgendaItem<M>, IBaseViewModelWithListOfSpeakers<M> {
protected _item?: ViewItem;
protected _listOfSpeakers?: ViewListOfSpeakers;
protected _list_of_speakers?: ViewListOfSpeakers;
public get agendaItem(): ViewItem | null {
return this._item;
@ -38,7 +37,7 @@ export abstract class BaseViewModelWithAgendaItemAndListOfSpeakers<
}
public get listOfSpeakers(): ViewListOfSpeakers | null {
return this._listOfSpeakers;
return this._list_of_speakers;
}
public get list_of_speakers_id(): number {
@ -50,11 +49,8 @@ export abstract class BaseViewModelWithAgendaItemAndListOfSpeakers<
public getListOfSpeakersTitle: () => string;
public getListOfSpeakersSlideTitle: () => string;
public constructor(collectionString: string, model: M, item?: ViewItem, listOfSpeakers?: ViewListOfSpeakers) {
public constructor(collectionString: string, model: M) {
super(collectionString, model);
// Explicit set to null instead of undefined, if not given
this._item = item || null;
this._listOfSpeakers = listOfSpeakers || null;
}
/**
@ -71,12 +67,4 @@ export abstract class BaseViewModelWithAgendaItemAndListOfSpeakers<
* Should return a string representation of the object, so there can be searched for.
*/
public abstract formatForSearch(): SearchRepresentation;
public updateDependencies(update: BaseViewModel): void {
if (update instanceof ViewItem && update.id === this.agenda_item_id) {
this._item = update;
} else if (update instanceof ViewListOfSpeakers && update.id === this.list_of_speakers_id) {
this._listOfSpeakers = update;
}
}
}

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 { isSearchable, Searchable } from './searchable';
import { BaseModelWithAgendaItem } from 'app/shared/models/base/base-model-with-agenda-item';
import { BaseViewModel, TitleInformation } from './base-view-model';
import { Item } from 'app/shared/models/agenda/item';
import { TitleInformation } from './base-view-model';
export function isBaseViewModelWithAgendaItem(obj: any): obj is BaseViewModelWithAgendaItem {
const model = <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.
*/
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 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(
collectionString: string,
model: M,
private isC: (obj: any) => obj is C,
private CVerbose: string,
contentObject?: C
) {
public constructor(collectionString: string, model: M) {
super(collectionString, model);
this._contentObject = contentObject;
}
/**
* Check, if the given model mathces the content object definition. If so, the function
* returns true, else false.
*/
public updateDependencies(update: BaseViewModel): boolean {
if (
update &&
update.collectionString === this.contentObjectData.collection &&
update.id === this.contentObjectData.id
) {
if (this.isC(update)) {
this._contentObject = update;
return true;
} else {
throw new Error(`The object is not an ${this.CVerbose}:` + update);
}
}
return false;
}
}

View File

@ -1,8 +1,6 @@
import { BaseProjectableViewModel } from './base-projectable-view-model';
import { isDetailNavigable, DetailNavigable } from 'app/shared/models/base/detail-navigable';
import { BaseModelWithListOfSpeakers } from 'app/shared/models/base/base-model-with-list-of-speakers';
import { BaseViewModel } from './base-view-model';
import { ListOfSpeakers } from 'app/shared/models/agenda/list-of-speakers';
export function isBaseViewModelWithListOfSpeakers(obj: any): obj is BaseViewModelWithListOfSpeakers {
const model = <BaseViewModelWithListOfSpeakers>obj;
@ -38,10 +36,10 @@ export interface IBaseViewModelWithListOfSpeakers<M extends BaseModelWithListOfS
export abstract class BaseViewModelWithListOfSpeakers<M extends BaseModelWithListOfSpeakers = any>
extends BaseProjectableViewModel<M>
implements IBaseViewModelWithListOfSpeakers<M> {
protected _listOfSpeakers?: any;
protected _list_of_speakers?: any;
public get listOfSpeakers(): any | null {
return this._listOfSpeakers;
return this._list_of_speakers;
}
public get list_of_speakers_id(): number {
@ -53,15 +51,8 @@ export abstract class BaseViewModelWithListOfSpeakers<M extends BaseModelWithLis
public constructor(collectionString: string, model: M, listOfSpeakers?: any) {
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 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 { Collection } from 'app/shared/models/base/collection';
import { BaseModel } from 'app/shared/models/base/base-model';
import { Updateable } from './updateable';
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.
*/
export abstract class BaseViewModel<M extends BaseModel = any>
implements Displayable, Identifiable, Collection, Updateable {
export abstract class BaseViewModel<M extends BaseModel = any> implements Displayable, Identifiable, Collection {
protected _model: M;
public get id(): number {
@ -72,8 +70,6 @@ export abstract class BaseViewModel<M extends BaseModel = any>
return this._model;
}
public abstract updateDependencies(update: BaseViewModel): void;
public toString(): string {
return this.getTitle();
}

View File

@ -94,8 +94,6 @@ export class ViewConfig extends BaseViewModel<Config> implements ConfigTitleInfo
super(Config.COLLECTIONSTRING, config);
}
public updateDependencies(update: BaseViewModel): void {}
/**
* Returns the time this config field needs to debounce before sending a request to the server.
* A little debounce time for all inputs is given here and is usefull, if inputs sends multiple onChange-events,

View File

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

View File

@ -1,10 +1,8 @@
import { BaseViewModel } from '../../base/base-view-model';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
import { Searchable } from 'app/site/base/searchable';
import { SearchRepresentation } from 'app/core/ui-services/search.service';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { BaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers';
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
import { ViewGroup } from 'app/site/users/models/view-group';
export const IMAGE_MIMETYPES = ['image/png', 'image/jpeg', 'image/gif'];
@ -95,17 +93,8 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
return this.mediafile.create_timestamp ? this.mediafile.create_timestamp : null;
}
public constructor(
mediafile: Mediafile,
listOfSpeakers?: ViewListOfSpeakers,
parent?: ViewMediafile,
access_groups?: ViewGroup[],
inherited_access_groups?: ViewGroup[]
) {
super(Mediafile.COLLECTIONSTRING, mediafile, listOfSpeakers);
this._parent = parent;
this._access_groups = access_groups;
this._inherited_access_groups = inherited_access_groups;
public constructor(mediafile: Mediafile) {
super(Mediafile.COLLECTIONSTRING, mediafile);
}
public formatForSearch(): SearchRepresentation {
@ -202,29 +191,4 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
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);
this._parent = parent;
}
public formatForSearch(): SearchRepresentation {
@ -102,14 +101,4 @@ export class ViewCategory extends BaseViewModel<Category> implements CategoryTit
return [];
}
}
/**
* Updates the local objects if required
* @param update
*/
public updateDependencies(update: BaseViewModel): void {
if (update instanceof ViewCategory && update.id === this.parent_id) {
this._parent = update;
}
}
}

View File

@ -1,16 +1,6 @@
import { WorkflowState } from 'app/shared/models/motions/workflow-state';
import { ViewMotion } from './view-motion';
import { CreateMotion } from './create-motion';
import { ViewUser } from 'app/site/users/models/view-user';
import { ViewMotionBlock } from './view-motion-block';
import { ViewItem } from 'app/site/agenda/models/view-item';
import { ViewCategory } from './view-category';
import { ViewWorkflow } from './view-workflow';
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { ViewTag } from 'app/site/tags/models/view-tag';
import { ViewMotionChangeRecommendation } from './view-motion-change-recommendation';
import { PersonalNoteContent } from 'app/shared/models/users/personal-note';
/**
* Create motion class for the View. Its different to ViewMotion in fact that the submitter handling is different
@ -21,57 +11,23 @@ import { PersonalNoteContent } from 'app/shared/models/users/personal-note';
export class ViewCreateMotion extends ViewMotion {
protected _model: CreateMotion;
protected _submitterUsers: ViewUser[];
public get motion(): CreateMotion {
return this._model;
}
public get submitters(): ViewUser[] {
return this._submitters;
public get submittersAsUsers(): ViewUser[] {
return this._submitterUsers;
}
public get submitters_id(): number[] {
return this.motion ? this.motion.sorted_submitters_id : null;
}
public set submitters(users: ViewUser[]) {
this._submitters = users;
public set submittersAsUsers(users: ViewUser[]) {
this._submitterUsers = users;
this._model.submitters_id = users.map(user => user.id);
}
public constructor(
motion: CreateMotion,
category?: ViewCategory,
submitters?: ViewUser[],
supporters?: ViewUser[],
workflow?: ViewWorkflow,
state?: WorkflowState,
item?: ViewItem,
listOfSpeakers?: ViewListOfSpeakers,
block?: ViewMotionBlock,
attachments?: ViewMediafile[],
tags?: ViewTag[],
parent?: ViewMotion,
changeRecommendations?: ViewMotionChangeRecommendation[],
amendments?: ViewMotion[],
personalNote?: PersonalNoteContent
) {
super(
motion,
category,
submitters,
supporters,
workflow,
state,
item,
listOfSpeakers,
block,
attachments,
tags,
parent,
changeRecommendations,
amendments,
personalNote
);
public constructor(motion: CreateMotion) {
super(motion);
}
public getVerboseName = () => {
@ -82,13 +38,6 @@ export class ViewCreateMotion extends ViewMotion {
* Duplicate this motion into a copy of itself
*/
public copy(): ViewCreateMotion {
return new ViewCreateMotion(
this._model,
this._category,
this._submitters,
this._supporters,
this._workflow,
this._state
);
return new ViewCreateMotion(this._model);
}
}

View File

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

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 { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { Searchable } from 'app/site/base/searchable';
import { ViewItem } from 'app/site/agenda/models/view-item';
import { BaseViewModelWithAgendaItemAndListOfSpeakers } from 'app/site/base/base-view-model-with-agenda-item-and-list-of-speakers';
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
import { TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item';
export interface MotionBlockTitleInformation extends TitleInformationWithAgendaItem {
@ -31,8 +29,8 @@ export class ViewMotionBlock extends BaseViewModelWithAgendaItemAndListOfSpeaker
return this.motionBlock.internal;
}
public constructor(motionBlock: MotionBlock, agendaItem?: ViewItem, listOfSpeakers?: ViewListOfSpeakers) {
super(MotionBlock.COLLECTIONSTRING, motionBlock, agendaItem, listOfSpeakers);
public constructor(motionBlock: MotionBlock) {
super(MotionBlock.COLLECTIONSTRING, motionBlock);
}
/**

View File

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

View File

@ -17,8 +17,8 @@ export class ViewMotionCommentSection extends BaseViewModel<MotionCommentSection
implements MotionCommentSectionTitleInformation {
public static COLLECTIONSTRING = MotionCommentSection.COLLECTIONSTRING;
private _readGroups: ViewGroup[];
private _writeGroups: ViewGroup[];
private _read_groups: ViewGroup[];
private _write_groups: ViewGroup[];
public get section(): MotionCommentSection {
return this._model;
@ -41,11 +41,11 @@ export class ViewMotionCommentSection extends BaseViewModel<MotionCommentSection
}
public get read_groups(): ViewGroup[] {
return this._readGroups;
return this._read_groups;
}
public get write_groups(): ViewGroup[] {
return this._writeGroups;
return this._write_groups;
}
public get weight(): number {
@ -59,10 +59,8 @@ export class ViewMotionCommentSection extends BaseViewModel<MotionCommentSection
this._model.name = name;
}
public constructor(motionCommentSection: MotionCommentSection, readGroups: ViewGroup[], writeGroups: ViewGroup[]) {
public constructor(motionCommentSection: 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 { PersonalNoteContent } from 'app/shared/models/users/personal-note';
import { ViewMotionCommentSection } from './view-motion-comment-section';
import { WorkflowState } from 'app/shared/models/motions/workflow-state';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { SearchRepresentation } from 'app/core/ui-services/search.service';
import { Searchable } from 'app/site/base/searchable';
import { ViewUser } from 'app/site/users/models/view-user';
import { ViewTag } from 'app/site/tags/models/view-tag';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { ViewItem } from 'app/site/agenda/models/view-item';
import { ViewWorkflow } from './view-workflow';
import { ViewCategory } from './view-category';
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 { ViewPersonalNote } from 'app/site/users/models/view-personal-note';
import { ViewMotionChangeRecommendation } from './view-motion-change-recommendation';
import { _ } from 'app/core/translate/translation-marker';
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 { ViewState } from './view-state';
import { ViewSubmitter } from './view-submitter';
/**
* The line numbering mode for the motion detail view.
@ -67,11 +64,12 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
public static COLLECTIONSTRING = Motion.COLLECTIONSTRING;
protected _category?: ViewCategory;
protected _submitters?: ViewUser[];
protected _submitters?: ViewSubmitter[];
protected _supporters?: ViewUser[];
protected _workflow?: ViewWorkflow;
protected _state?: WorkflowState;
protected _block?: ViewMotionBlock;
protected _state?: ViewState;
protected _recommendation?: ViewState;
protected _motion_block?: ViewMotionBlock;
protected _attachments?: ViewMediafile[];
protected _tags?: ViewTag[];
protected _parent?: ViewMotion;
@ -87,10 +85,22 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
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 || [];
}
public get submittersAsUsers(): ViewUser[] {
return this.submitters.map(submitter => submitter.user);
}
public get supporters(): ViewUser[] {
return this._supporters || [];
}
@ -104,7 +114,7 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
}
public get motion_block(): ViewMotionBlock | null {
return this._block;
return this._motion_block;
}
public get attachments(): ViewMediafile[] {
@ -185,21 +195,12 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
return this.motion.workflow_id;
}
public get state(): WorkflowState | null {
return this._state;
}
public get changeRecommendations(): ViewMotionChangeRecommendation[] {
return this._changeRecommendations;
}
/**
* Checks if the current state of thw workflow is final
*
* @returns true if it is final
*/
public get isFinalState(): boolean {
return this._state.isFinalState;
public get change_recommendations_id(): number[] {
return this.motion.change_recommendations_id;
}
public get state_id(): number {
@ -214,28 +215,14 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
return this.motion.statute_paragraph_id;
}
public get recommendation(): WorkflowState {
return this.workflow ? this.workflow.getStateById(this.recommendation_id) : null;
}
public get possibleRecommendations(): WorkflowState[] {
return this.workflow
? this.workflow.states.filter(recommendation => recommendation.recommendation_label !== undefined)
: null;
public get possibleRecommendations(): ViewState[] {
return this.workflow ? this.workflow.states.filter(state => state.recommendation_label !== undefined) : null;
}
public get origin(): string {
return this.motion.origin;
}
public get nextStates(): WorkflowState[] {
return this.state && this.workflow ? this.state.getNextStates(this.workflow.workflow) : [];
}
public get previousStates(): WorkflowState[] {
return this.state && this.workflow ? this.state.getPreviousStates(this.workflow.workflow) : [];
}
public get agenda_type(): number {
return this.agendaItem ? this.agendaItem.type : null;
}
@ -327,8 +314,6 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
/**
* Getter to query the 'favorite'/'star' status of the motions
*
* @returns the current state
*/
public get star(): boolean {
return !!this.personalNote && !!this.personalNote.star;
@ -355,36 +340,8 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
// This is set by the repository
public getIdentifierOrTitle: () => string;
public constructor(
motion: Motion,
category?: ViewCategory,
submitters?: ViewUser[],
supporters?: ViewUser[],
workflow?: ViewWorkflow,
state?: WorkflowState,
item?: ViewItem,
listOfSpeakers?: ViewListOfSpeakers,
block?: ViewMotionBlock,
attachments?: ViewMediafile[],
tags?: ViewTag[],
parent?: ViewMotion,
changeRecommendations?: ViewMotionChangeRecommendation[],
amendments?: ViewMotion[],
personalNote?: PersonalNoteContent
) {
super(Motion.COLLECTIONSTRING, motion, item, listOfSpeakers);
this._category = category;
this._submitters = submitters;
this._supporters = supporters;
this._workflow = workflow;
this._state = state;
this._block = block;
this._attachments = attachments;
this._tags = tags;
this._parent = parent;
this._amendments = amendments;
this._changeRecommendations = changeRecommendations;
this.personalNote = personalNote;
public constructor(motion: Motion) {
super(Motion.COLLECTIONSTRING, motion);
}
/**
@ -397,7 +354,7 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
if (this.amendment_paragraphs) {
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.tags.map(tag => tag.getTitle()));
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);
}
/**
* Updates the local objects if required
*
* @param update
*/
public updateDependencies(update: BaseViewModel): void {
super.updateDependencies(update);
if (update instanceof ViewWorkflow) {
this.updateWorkflow(update);
} else if (update instanceof ViewCategory) {
this.updateCategory(update);
} else if (update instanceof ViewMotionBlock) {
this.updateMotionBlock(update);
} else if (update instanceof ViewUser) {
this.updateUser(update);
} else if (update instanceof ViewMediafile) {
this.updateAttachments(update);
} else if (update instanceof ViewTag) {
this.updateTags(update);
} else if (update instanceof ViewMotion && update.id !== this.id) {
this.updateMotion(update);
} else if (update instanceof ViewMotionChangeRecommendation) {
this.updateChangeRecommendation(update);
} else if (update instanceof ViewPersonalNote) {
this.updatePersonalNote(update);
}
}
/**
* Update routine for the workflow
*
* @param workflow potentially the (changed workflow (state). Needs manual verification
*/
private updateWorkflow(workflow: ViewWorkflow): void {
if (workflow.id === this.motion.workflow_id) {
this._workflow = workflow;
this._state = workflow.getStateById(this.state_id);
}
}
/**
* Update routine for the category
*
* @param category potentially the changed category. Needs manual verification
*/
private updateCategory(category: ViewCategory): void {
if (this.category_id && category.id === this.motion.category_id) {
this._category = category;
}
}
/**
* Update routine for the motion block
*
* @param block potentially the changed motion block. Needs manual verification
*/
private updateMotionBlock(block: ViewMotionBlock): void {
if (this.motion_block_id && block.id === this.motion.motion_block_id) {
this._block = block;
}
}
/**
* Update routine for supporters and submitters
*
* @param update potentially the changed user. Needs manual verification
*/
private updateUser(update: ViewUser): void {
if (this.motion.submitters && this.motion.submitters.find(user => user.user_id === update.id)) {
const userIndex = this.motion.submitters.findIndex(submitter => submitter.user_id === update.id);
this.submitters[userIndex] = update;
}
if (this.motion.supporters_id && this.motion.supporters_id.includes(update.id)) {
const userIndex = this.supporters.findIndex(user => user.id === update.id);
if (userIndex < 0) {
this.supporters.push(update);
} else {
this.supporters[userIndex] = update;
}
}
}
/**
* Update routine for attachments
*
* @param mediafile
*/
private updateAttachments(mediafile: ViewMediafile): void {
if (this.attachments_id && this.attachments_id.includes(mediafile.id)) {
const attachmentIndex = this.attachments.findIndex(_mediafile => _mediafile.id === mediafile.id);
if (attachmentIndex < 0) {
this.attachments.push(mediafile);
} else {
this.attachments[attachmentIndex] = mediafile;
}
}
}
private updateTags(tag: ViewTag): void {
if (this.tags_id && this.tags_id.includes(tag.id)) {
const tagIndex = this.tags.findIndex(_tag => _tag.id === tag.id);
if (tagIndex < 0) {
this.tags.push(tag);
} else {
this.tags[tagIndex] = tag;
}
}
}
/**
* The update cen be the parent or a child motion (=amendment).
*/
private updateMotion(update: ViewMotion): void {
if (this.parent_id && this.parent_id === update.id) {
this._parent = update;
} else if (update.parent_id && update.parent_id === this.id) {
const index = this._amendments.findIndex(m => m.id === update.id);
if (index >= 0) {
this._amendments[index] = update;
} else {
this._amendments.push(update);
}
}
}
private updateChangeRecommendation(cr: ViewMotionChangeRecommendation): void {
if (cr.motion_id === this.id) {
const index = this.changeRecommendations.findIndex(_cr => _cr.id === cr.id);
if (index < 0) {
this.changeRecommendations.push(cr);
} else {
this.changeRecommendations[index] = cr;
}
}
}
private updatePersonalNote(personalNote: ViewPersonalNote): void {
this.personalNote = personalNote.getNoteContent(this.collectionString, this.id);
}
public hasSupporters(): boolean {
return !!(this.supporters && this.supporters.length > 0);
}
@ -589,7 +406,7 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
* Determine if the motion is in its final workflow state
*/
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
*/
public copy(): ViewMotion {
return new ViewMotion(
this._model,
this._category,
this._submitters,
this._supporters,
this._workflow,
this._state,
this._item,
this._listOfSpeakers,
this._block,
this._attachments,
this._tags,
this._parent
);
return new ViewMotion(this._model);
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@
<div class="title-line">
<strong>
<span translate>First state</span>:
<span>{{ workflow.firstState.name | translate }}</span>
<span>{{ workflow.first_state.name | translate }}</span>
</strong>
</div>
@ -70,20 +70,13 @@
{{ state[perm.selector] | translate }}
</mat-basic-chip>
</div>
<div
*ngIf="perm.type === 'state'"
>
<div *ngIf="perm.type === 'state'">
<div class="inner-table">
<div *ngIf="!state.next_states_id.length">
<div *ngIf="!state.next_states.length">
-
</div>
<div *ngIf="state.next_states_id.length">
<div
*ngFor="
let nextstate of state.getNextStates(workflow.workflow);
let last = last
"
>
<div *ngIf="state.next_states.length">
<div *ngFor="let nextstate of state.next_states; let last = last">
{{ nextstate.name | translate }}<span *ngIf="!last">,&nbsp;</span>
</div>
</div>
@ -92,11 +85,9 @@
class="clickable-cell stretch-to-fill-parent"
[matMenuTriggerFor]="nextStatesMenu"
[matMenuTriggerData]="{ state: state }"
></div>
></div>
</div>
<div
*ngIf="perm.type === 'amendment'"
>
<div *ngIf="perm.type === 'amendment'">
<div class="inner-table">
{{ getMergeAmendmentLabel(state.merge_amendment_into_final) | translate }}
</div>
@ -106,9 +97,7 @@
[matMenuTriggerData]="{ state: state }"
></div>
</div>
<div
*ngIf="perm.type === 'restriction'"
>
<div *ngIf="perm.type === 'restriction'">
<div class="inner-table">
<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 { ViewWorkflow } from 'app/site/motions/models/view-workflow';
import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service';
import { WorkflowState, MergeAmendment } from 'app/shared/models/motions/workflow-state';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { MergeAmendment, State } from 'app/shared/models/motions/state';
import { ViewState } from 'app/site/motions/models/view-state';
import { StateRepositoryService } from 'app/core/repositories/motions/state-repository.service';
/**
* Declares data for the workflow dialog
@ -153,6 +155,7 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit
private promtService: PromptService,
private dialog: MatDialog,
private workflowRepo: WorkflowRepositoryService,
private stateRepo: StateRepositoryService,
private route: ActivatedRoute
) {
super(title, translate, matSnackBar);
@ -180,17 +183,17 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit
*
* @param state the selected workflow state
*/
public onClickStateName(state: WorkflowState): void {
public onClickStateName(state: ViewState): void {
this.openEditDialog(state.name, 'Rename state', '', true).subscribe(result => {
if (result) {
if (result.action === 'update') {
this.workflowRepo.updateState({ name: result.value }, state).then(() => {}, this.raiseError);
this.stateRepo.update({ name: result.value }, state).then(() => {}, this.raiseError);
} else if (result.action === 'delete') {
const content = this.translate.instant('Delete') + ` ${state.name}?`;
this.promtService.open('Are you sure', content).then(promptResult => {
if (promptResult) {
this.workflowRepo.deleteState(state).then(() => {}, this.raiseError);
this.stateRepo.delete(state).then(() => {}, this.raiseError);
}
});
}
@ -206,7 +209,11 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit
this.openEditDialog('', this.translate.instant('New state'), this.translate.instant('Name')).subscribe(
result => {
if (result && result.action === 'update') {
this.workflowRepo.addState(result.value, this.workflow).then(() => {}, this.raiseError);
const state = new State({
name: result.value,
workflow_id: this.workflow.id
});
this.stateRepo.create(state).then(() => {}, this.raiseError);
}
}
);
@ -231,10 +238,10 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit
* @param perm The permission
* @param state The selected workflow state
*/
public onClickInputPerm(perm: StatePerm, state: WorkflowState): void {
public onClickInputPerm(perm: StatePerm, state: ViewState): void {
this.openEditDialog(state[perm.selector], 'Edit', perm.name).subscribe(result => {
if (result && result.action === 'update') {
this.workflowRepo.updateState({ [perm.selector]: result.value }, state).then(() => {}, this.raiseError);
this.stateRepo.update({ [perm.selector]: result.value }, state).then(() => {}, this.raiseError);
}
});
}
@ -246,8 +253,8 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit
* @param perm The states permission that was changed
* @param event The change event.
*/
public onToggleStatePerm(state: WorkflowState, perm: string, event: MatCheckboxChange): void {
this.workflowRepo.updateState({ [perm]: event.checked }, state).then(() => {}, this.raiseError);
public onToggleStatePerm(state: ViewState, perm: string, event: MatCheckboxChange): void {
this.stateRepo.update({ [perm]: event.checked }, state).then(() => {}, this.raiseError);
}
/**
@ -257,8 +264,8 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit
* @param state The selected workflow state
* @param color The selected color
*/
public onSelectColor(state: WorkflowState, color: string): void {
this.workflowRepo.updateState({ css_class: color }, state).then(() => {}, this.raiseError);
public onSelectColor(state: ViewState, color: string): void {
this.stateRepo.update({ css_class: color }, state).then(() => {}, this.raiseError);
}
/**
@ -267,7 +274,7 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit
* @param nextState the potential next workflow state
* @param state the state to add or remove another state to
*/
public onSetNextState(nextState: WorkflowState, state: WorkflowState): void {
public onSetNextState(nextState: ViewState, state: ViewState): void {
const ids = state.next_states_id.map(id => id);
const stateIdIndex = ids.findIndex(id => id === nextState.id);
@ -276,7 +283,7 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit
} else {
ids.splice(stateIdIndex, 1);
}
this.workflowRepo.updateState({ next_states_id: ids }, state).then(() => {}, this.raiseError);
this.stateRepo.update({ next_states_id: ids }, state).then(() => {}, this.raiseError);
}
/**
@ -285,7 +292,7 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit
* @param restrictions The new restrictions
* @param state the state to change
*/
public onSetRestriction(restriction: string, state: WorkflowState): void {
public onSetRestriction(restriction: string, state: ViewState): void {
const restrictions = state.restriction.map(r => r);
const restrictionIndex = restrictions.findIndex(r => r === restriction);
@ -294,7 +301,7 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit
} else {
restrictions.splice(restrictionIndex, 1);
}
this.workflowRepo.updateState({ restriction: restrictions }, state).then(() => {}, this.raiseError);
this.stateRepo.update({ restriction: restrictions }, state).then(() => {}, this.raiseError);
}
/**
@ -311,8 +318,8 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit
* @param amendment determines the amendment
* @param state the state to change
*/
public setMergeAmendment(amendment: number, state: WorkflowState): void {
this.workflowRepo.updateState({ merge_amendment_into_final: amendment }, state).then(() => {}, this.raiseError);
public setMergeAmendment(amendment: number, state: ViewState): void {
this.stateRepo.update({ merge_amendment_into_final: amendment }, state).then(() => {}, this.raiseError);
}
/**
@ -383,7 +390,7 @@ export class WorkflowDetailComponent extends BaseViewComponent implements OnInit
* @param state the workflow state
* @returns a unique definition
*/
public getColumnDef(state: WorkflowState): string {
public getColumnDef(state: ViewState): string {
return `${state.name}${state.id}`;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -152,7 +152,7 @@ export class MotionCsvExportService {
[
{ label: 'Called', map: motion => (motion.sort_parent_id ? '' : motion.identifierOrTitle) },
{ label: 'Called with', map: motion => (!motion.sort_parent_id ? '' : motion.identifierOrTitle) },
{ label: 'submitters', map: motion => motion.submitters.map(s => s.short_name).join(',') },
{ label: 'submitters', map: motion => motion.submittersAsUsers.map(s => s.short_name).join(',') },
{ property: 'title' },
{
label: 'recommendation',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -106,14 +106,7 @@ export class ViewProjector extends BaseViewModel<Projector> {
return this.projector.show_logo;
}
public constructor(projector: Projector, referenceProjector?: ViewProjector) {
public constructor(projector: Projector) {
super(Projector.COLLECTIONSTRING, projector);
this._referenceProjector = referenceProjector;
}
public updateDependencies(update: BaseViewModel): void {
if (update instanceof ViewProjector && this.reference_projector_id === update.id) {
this._referenceProjector = update;
}
}
}

View File

@ -36,10 +36,4 @@ export class ViewTag extends BaseViewModel<Tag> implements TagTitleInformation,
public getDetailStateURL(): string {
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 { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { ViewItem } from '../../agenda/models/view-item';
import { BaseViewModel } from 'app/site/base/base-view-model';
import { ViewListOfSpeakers } from '../../agenda/models/view-list-of-speakers';
import { BaseViewModelWithAgendaItemAndListOfSpeakers } from 'app/site/base/base-view-model-with-agenda-item-and-list-of-speakers';
import { TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item';
@ -42,14 +39,8 @@ export class ViewTopic extends BaseViewModelWithAgendaItemAndListOfSpeakers impl
return this.topic.text;
}
public constructor(
topic: Topic,
attachments?: ViewMediafile[],
item?: ViewItem,
listOfSpeakers?: ViewListOfSpeakers
) {
super(Topic.COLLECTIONSTRING, topic, item, listOfSpeakers);
this._attachments = attachments;
public constructor(topic: Topic) {
super(Topic.COLLECTIONSTRING, topic);
}
/**
@ -89,16 +80,4 @@ export class ViewTopic extends BaseViewModelWithAgendaItemAndListOfSpeakers impl
public hasAttachments(): boolean {
return this.attachments && this.attachments.length > 0;
}
public updateDependencies(update: BaseViewModel): void {
super.updateDependencies(update);
if (update instanceof ViewMediafile && this.attachments_id.includes(update.id)) {
const attachmentIndex = this.attachments.findIndex(mediafile => mediafile.id === update.id);
if (attachmentIndex < 0) {
this.attachments.push(update);
} else {
this.attachments[attachmentIndex] = update;
}
}
}
}

View File

@ -36,6 +36,4 @@ export class ViewGroup extends BaseViewModel<Group> implements GroupTitleInforma
public hasPermission(perm: string): boolean {
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;
}
}
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 { SearchRepresentation } from 'app/core/ui-services/search.service';
import { ViewGroup } from './view-group';
import { BaseViewModel } from 'app/site/base/base-view-model';
export interface UserTitleInformation {
username: string;
@ -121,9 +120,8 @@ export class ViewUser extends BaseProjectableViewModel<User> implements UserTitl
public getFullName: () => string;
public getShortName: () => string;
public constructor(user: User, groups?: ViewGroup[]) {
public constructor(user: User) {
super(User.COLLECTIONSTRING, user);
this._groups = groups;
}
/**
@ -151,15 +149,4 @@ export class ViewUser extends BaseProjectableViewModel<User> implements UserTitl
getDialogTitle: () => this.getTitle()
};
}
public updateDependencies(update: BaseViewModel): void {
if (update instanceof ViewGroup && this.user.groups_id.includes(update.id)) {
const groupIndex = this.groups.findIndex(group => group.id === update.id);
if (groupIndex < 0) {
this.groups.push(update);
} else {
this.groups[groupIndex] = update;
}
}
}
}

View File

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

View File

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

View File

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

View File

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

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