diff --git a/client/src/app/core/core-services/autoupdate.service.ts b/client/src/app/core/core-services/autoupdate.service.ts index 75ea1e2f7..14a6a4813 100644 --- a/client/src/app/core/core-services/autoupdate.service.ts +++ b/client/src/app/core/core-services/autoupdate.service.ts @@ -97,9 +97,9 @@ export class AutoupdateService { elements = elements.concat(this.mapObjectsToBaseModels(collection, autoupdate.changed[collection])); }); - const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS, true); + const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS); await this.DS.set(elements, autoupdate.to_change_id); - this.DSUpdateManager.commit(updateSlot); + this.DSUpdateManager.commit(updateSlot, autoupdate.to_change_id, true); } /** @@ -130,7 +130,7 @@ export class AutoupdateService { await this.DS.flushToStorage(autoupdate.to_change_id); - this.DSUpdateManager.commit(updateSlot); + this.DSUpdateManager.commit(updateSlot, autoupdate.to_change_id); } else { // autoupdate fully in the future. we are missing something! this.requestChanges(); @@ -172,7 +172,7 @@ export class AutoupdateService { const oldChangeId = this.DS.maxChangeId; const response = await this.websocketService.sendAndGetResponse<{}, AutoupdateFormat>('getElements', {}); - const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS, true); + const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS); let allModels: BaseModel[] = []; for (const collection of Object.keys(response.changed)) { if (this.modelMapper.isCollectionRegistered(collection)) { @@ -183,7 +183,7 @@ export class AutoupdateService { } await this.DS.set(allModels, response.to_change_id); - this.DSUpdateManager.commit(updateSlot); + this.DSUpdateManager.commit(updateSlot, response.to_change_id, true); console.log(`Full update done from ${oldChangeId} to ${response.to_change_id}`); } diff --git a/client/src/app/core/core-services/data-store.service.ts b/client/src/app/core/core-services/data-store.service.ts index 9c31da88e..1db3b254b 100644 --- a/client/src/app/core/core-services/data-store.service.ts +++ b/client/src/app/core/core-services/data-store.service.ts @@ -3,9 +3,9 @@ import { EventEmitter, Injectable } from '@angular/core'; import { Observable, Subject } from 'rxjs'; import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model'; -import { BaseRepository } from '../repositories/base-repository'; import { CollectionStringMapperService } from './collection-string-mapper.service'; import { Deferred } from '../promises/deferred'; +import { RelationCacheService } from './relation-cache.service'; import { StorageService } from './storage.service'; /** @@ -49,7 +49,7 @@ export class UpdateSlot { /** * @param DS Carries the DataStore: TODO (see below `DataStoreUpdateManagerService.getNewUpdateSlot`) */ - public constructor(public readonly DS: DataStoreService, public readonly initialLoading: boolean) { + public constructor(public readonly DS: DataStoreService) { this._id = UpdateSlot.ID_COUTNER++; } @@ -169,7 +169,10 @@ export class DataStoreUpdateManagerService { /** * @param mapperService */ - public constructor(private mapperService: CollectionStringMapperService) {} + public constructor( + private mapperService: CollectionStringMapperService, + private relationCacheService: RelationCacheService + ) {} /** * Retrieve the current update slot. @@ -185,13 +188,13 @@ export class DataStoreUpdateManagerService { * @param DS The DataStore. This is a hack, becuase we cannot use the DataStore * here, because these are cyclic dependencies... --> TODO */ - public async getNewUpdateSlot(DS: DataStoreService, initialLoading: boolean = false): Promise { + public async getNewUpdateSlot(DS: DataStoreService): Promise { if (this.currentUpdateSlot) { const request = new Deferred(); this.updateSlotRequests.push(request); await request; } - this.currentUpdateSlot = new UpdateSlot(DS, initialLoading); + this.currentUpdateSlot = new UpdateSlot(DS); return this.currentUpdateSlot; } @@ -204,7 +207,7 @@ export class DataStoreUpdateManagerService { * * @param slot The slot to commit */ - public commit(slot: UpdateSlot): void { + public commit(slot: UpdateSlot, changeId: number, resetCache: boolean = false): void { if (!this.currentUpdateSlot || !this.currentUpdateSlot.equal(slot)) { throw new Error('No or wrong update slot to be finished!'); } @@ -212,36 +215,26 @@ export class DataStoreUpdateManagerService { // notify repositories in two phases const repositories = this.mapperService.getAllRepositories(); - // just commit the update in a repository, if something was changed. Save - // this information in this mapping. the boolean is not evaluated; if there is an - const affectedRepos: { [collection: string]: BaseRepository } = {}; + + if (resetCache) { + this.relationCacheService.reset(); + } // Phase 1: deleting and creating of view models (in this order) repositories.forEach(repo => { const deletedModelIds = slot.getDeletedModelIdsForCollection(repo.collectionString); repo.deleteModels(deletedModelIds); + this.relationCacheService.registerDeletedModels(repo.collectionString, deletedModelIds); const changedModelIds = slot.getChangedModelIdsForCollection(repo.collectionString); - repo.changedModels(changedModelIds, slot.initialLoading); - - if (deletedModelIds.length || changedModelIds.length) { - affectedRepos[repo.collectionString] = repo; - } + repo.changedModels(changedModelIds); + this.relationCacheService.registerChangedModels(repo.collectionString, changedModelIds, changeId); }); - // Phase 2: updating dependencies (deleting ad changing in this order) + // Phase 2: updating all repositories repositories.forEach(repo => { - if (repo.updateDependenciesForDeletedModels(slot.getDeletedModels())) { - affectedRepos[repo.collectionString] = repo; - } - if (repo.updateDependenciesForChangedModels(slot.getChangedModels())) { - affectedRepos[repo.collectionString] = repo; - } + repo.commitUpdate(); }); - // Phase 3: committing the update to all affected repos. This will trigger all - // list observables/subjects to emit the new list. - Object.values(affectedRepos).forEach(repo => repo.commitUpdate()); - slot.DS.triggerModifiedObservable(); // serve next slot request @@ -350,7 +343,7 @@ export class DataStoreService { // This promise will be resolved with cached datastore. const store = await this.storageService.get(DataStoreService.cachePrefix + 'DS'); if (store) { - const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this, true); + const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this); // There is a store. Deserialize it this.jsonStore = store; @@ -370,7 +363,7 @@ export class DataStoreService { }); }); - this.DSUpdateManager.commit(updateSlot); + this.DSUpdateManager.commit(updateSlot, maxChangeId, true); } else { await this.clear(); } diff --git a/client/src/app/core/core-services/relation-cache.service.spec.ts b/client/src/app/core/core-services/relation-cache.service.spec.ts new file mode 100644 index 000000000..2dd6680f2 --- /dev/null +++ b/client/src/app/core/core-services/relation-cache.service.spec.ts @@ -0,0 +1,16 @@ +import { inject, TestBed } from '@angular/core/testing'; + +import { E2EImportsModule } from '../../../e2e-imports.module'; +import { RelationCacheService } from './relation-cache.service'; + +describe('RelationCacheService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule], + providers: [RelationCacheService] + }); + }); + it('should be created', inject([RelationCacheService], (service: RelationCacheService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/client/src/app/core/core-services/relation-cache.service.ts b/client/src/app/core/core-services/relation-cache.service.ts new file mode 100644 index 000000000..9df8037b5 --- /dev/null +++ b/client/src/app/core/core-services/relation-cache.service.ts @@ -0,0 +1,84 @@ +import { Injectable } from '@angular/core'; + +export interface CacheChangeIds { + [elementId: string]: number; +} + +/** + * Handles caching metadata for the relation manager. + * + * Mainly holds an object mapping element ids to their last updates change ids. + * The manager can detect invalid caches, if the change id in the cache's metadata + * diverges from the change ids in this service. + */ +@Injectable({ + providedIn: 'root' +}) +export class RelationCacheService { + private cache: { + [elementId: string]: number; + } = {}; + + public constructor() {} + + /** + * Reset the cache. + */ + public reset(): void { + this.cache = {}; + } + + /** + * Deletes models from this cache. + * + * @param collection Collection + * @param ids Ids from all models in the collection + */ + public registerDeletedModels(collection: string, ids: number[]): void { + ids.forEach(id => { + const elementId = collection + ':' + id; + delete this.cache[elementId]; + }); + } + + /** + * Adds models to the cache with the given change id. + * + * @param collection Collection + * @param ids Ids from all models in the collection + * @param changeId The change id to put into the cache + */ + public registerChangedModels(collection: string, ids: number[], changeId: number): void { + ids.forEach(id => { + const elementId = collection + ':' + id; + this.cache[elementId] = changeId; + }); + } + + /** + * Queries the change id for one element. + * + * @param elementId The element to query. + */ + public query(elementId: string): number | null { + return this.cache[elementId] || null; + } + + /** + * Checks, if all given change ids are valid. + */ + public checkCacheValidity(changeIds: CacheChangeIds): boolean { + if (!changeIds) { + return false; + } + + const elementIds = Object.keys(changeIds); + if (!elementIds.length) { + return false; + } + + return elementIds.every(elementId => { + return this.query(elementId) === changeIds[elementId]; + }); + } +} diff --git a/client/src/app/core/core-services/relation-manager.service.ts b/client/src/app/core/core-services/relation-manager.service.ts index 4bc4f1f54..d3de55fa7 100644 --- a/client/src/app/core/core-services/relation-manager.service.ts +++ b/client/src/app/core/core-services/relation-manager.service.ts @@ -2,74 +2,35 @@ import { Injectable } from '@angular/core'; import { BaseModel } from 'app/shared/models/base/base-model'; import { BaseViewModel, ViewModelConstructor } from 'app/site/base/base-view-model'; +import { ModelDescriptor, NestedModelDescriptors } from '../repositories/base-repository'; +import { CacheChangeIds, RelationCacheService } from './relation-cache.service'; import { - BaseOrderedRelation, isCustomRelationDefinition, isGenericRelationDefinition, - isNestedRelationDefinition, isNormalRelationDefinition, isReverseRelationDefinition, - RelationDefinition, - ReverseRelationDefinition + RelationDefinition } from '../definitions/relations'; import { ViewModelStoreService } from './view-model-store.service'; /** * Manages relations between view models. This service is and should only used by the - * base repository to offload maanging relations between view models. + * base repository to offload managing relations between view models. */ @Injectable({ providedIn: 'root' }) export class RelationManagerService { - public constructor(private viewModelStoreService: ViewModelStoreService) {} + public constructor( + private viewModelStoreService: ViewModelStoreService, + private relationCacheService: RelationCacheService + ) {} - /** - * Sorts the array of foreign view models in the given view models for the given relation. - */ - public sortByRelation( - relation: BaseOrderedRelation, - viewModel: V - ): void { - const order = relation.order; - viewModel['_' + relation.ownKey].sort((a: BaseViewModel, b: BaseViewModel) => { - if (!order || a[order] === b[order]) { - return a.id - b.id; - } else { - return a[order] - b[order]; - } - }); - } - - /** - * Creates a view model from the given model and model ctor. All dependencies will be - * set accorting to relations. - */ - public createViewModel( - model: M, - modelCtor: ViewModelConstructor, - relations: RelationDefinition[], - initialLoading: boolean - ): V { - const viewModel = new modelCtor(model) as V; - - relations.forEach(relation => { - this.setRelationsInViewModel(model, viewModel, relation, initialLoading); - }); - - return viewModel; - } - - /** - * Sets one foreign view model in the view model according to the relation and the information - * from the model. - */ - protected setRelationsInViewModel( + public handleRelation( model: M, viewModel: V, - relation: RelationDefinition, - initialLoading: boolean - ): void { + relation: RelationDefinition + ): any { if (isNormalRelationDefinition(relation)) { if ( (relation.type === 'M2M' || relation.type === 'O2M') && @@ -80,22 +41,16 @@ export class RelationManagerService { relation.foreignViewModel, model[relation.ownIdKey] ); - viewModel['_' + relation.ownKey] = foreignViewModels; - this.sortByRelation(relation, viewModel); - if (relation.afterSetRelation) { - relation.afterSetRelation(viewModel, foreignViewModels); - } + this.sortViewModels(foreignViewModels, relation.order); + return foreignViewModels; } else if (relation.type === 'M2O') { const foreignViewModel = this.viewModelStoreService.get( relation.foreignViewModel, model[relation.ownIdKey] ); - viewModel['_' + relation.ownKey] = foreignViewModel; - if (relation.afterSetRelation) { - relation.afterSetRelation(viewModel, foreignViewModel); - } + return foreignViewModel; } - } else if (isReverseRelationDefinition(relation) && !initialLoading) { + } else if (isReverseRelationDefinition(relation)) { if (relation.type === 'M2M') { const foreignViewModels = this.viewModelStoreService.filter( relation.foreignViewModel, @@ -104,16 +59,16 @@ export class RelationManagerService { foreignViewModel[relation.foreignIdKey].constructor === Array && foreignViewModel[relation.foreignIdKey].includes(model.id) ); - viewModel['_' + relation.ownKey] = foreignViewModels; - this.sortByRelation(relation, viewModel); + this.sortViewModels(foreignViewModels, relation.order); + return foreignViewModels; } else if (relation.type === 'O2M') { const foreignViewModels = this.viewModelStoreService.filter( relation.foreignViewModel, foreignViewModel => foreignViewModel[relation.foreignIdKey] && foreignViewModel[relation.foreignIdKey] === model.id ); - viewModel['_' + relation.ownKey] = foreignViewModels; - this.sortByRelation(relation, viewModel); + this.sortViewModels(foreignViewModels, relation.order); + return foreignViewModels; } else if (relation.type === 'M2O') { const foreignViewModel = this.viewModelStoreService.find( relation.foreignViewModel, @@ -121,236 +76,156 @@ export class RelationManagerService { _foreignViewModel[relation.foreignIdKey] && _foreignViewModel[relation.foreignIdKey] === model.id ); - viewModel['_' + relation.ownKey] = foreignViewModel; + return foreignViewModel; } - } else if (isNestedRelationDefinition(relation)) { - const foreignModels = model[relation.ownKey].map(m => new relation.foreignModel(m)); - const foreignViewModels: BaseViewModel[] = foreignModels.map((foreignModel: BaseModel) => - this.createViewModel( - foreignModel, - relation.foreignViewModel, - relation.relationDefinition || [], - initialLoading - ) - ); - viewModel['_' + relation.ownKey] = foreignViewModels; - this.sortByRelation(relation, viewModel); } else if (isGenericRelationDefinition(relation)) { const contentObject = this.viewModelStoreService.get( model[relation.ownContentObjectDataKey].collection, model[relation.ownContentObjectDataKey].id ); if (contentObject && relation.isVForeign(contentObject)) { - viewModel['_' + relation.ownKey] = contentObject; + return contentObject; } } else if (isCustomRelationDefinition(relation)) { - relation.setRelations(model, viewModel); + return relation.get(model, viewModel); } } - /** - * Updates an own view model with an deleted model implicit given by the deletedId and - * the collection via the relation. - * - * @return true, if something was updated. - */ - public updateSingleDependencyForDeletedModel( - ownViewModel: BaseViewModel, - relation: ReverseRelationDefinition, - deletedId: number - ): boolean { - // In both relations, the ownViewModel holds an array of foreignViewModels. Try to find the deleted - // foreignViewModel in this array and remove it. - if (relation.type === 'O2M' || relation.type === 'M2M') { - const ownModelArray = ownViewModel['_' + relation.ownKey]; - if (!ownModelArray) { - return false; - } - // We have the array of foreign view models for our own view model. Put the foreignViewModel - // into it (replace or push). - const index = ownModelArray.findIndex(foreignViewModel => foreignViewModel.id === deletedId); - if (index > -1) { - ownModelArray.splice(index, 1); - return true; - } + public handleCachedRelation( + property: string, + target: V, + model: BaseModel, + viewModel: BaseViewModel, + relation: RelationDefinition + ): any { + let result: any; + + const cacheProperty = '__' + property; + const cachePropertyChangeIds = cacheProperty + '_cids'; + let cached: boolean = cacheProperty in target; + let changeIds: CacheChangeIds | null = null; + if (cached) { + result = target[cacheProperty]; + changeIds = target[cachePropertyChangeIds]; } - // The ownViewModel holds one foreignViewModel. Check, if it is the deleted one. - else if (relation.type === 'M2O') { - if (ownViewModel['_' + relation.ownKey] && ownViewModel['_' + relation.ownKey].id === deletedId) { - ownViewModel['_' + relation.ownKey] = null; - return true; + if (!isCustomRelationDefinition(relation)) { + if (cached) { + cached = this.relationCacheService.checkCacheValidity(changeIds); } - } - return false; - } + if (!cached) { + result = this.handleRelation(model, viewModel, relation) as BaseViewModel | BaseViewModel[]; - /** - * Updates an own view model with an implicit given model by the collection and changedId. - * - * @return true, if something was updated. - */ - public updateSingleDependencyForChangedModel( - ownViewModel: BaseViewModel, - relation: RelationDefinition, - collection: string, - changedId: number - ): boolean { - if (isNormalRelationDefinition(relation)) { - if (relation.type === 'M2M' || relation.type === 'O2M') { - // For the side of the ownViewModel these relations are the same: - // the ownViewModel does have may foreign models and we do have a normal relation (not a - // reverse one), we just set the many-part of the relation in the ownViewModel. - if ( - ownViewModel[relation.ownIdKey] && - ownViewModel[relation.ownIdKey].constructor === Array && - ownViewModel[relation.ownIdKey].includes(changedId) // The foreign view model belongs to us. - ) { - const foreignViewModel = this.viewModelStoreService.get(collection, changedId); - this.setForeingViewModelInOwnViewModelArray(foreignViewModel, ownViewModel, relation.ownKey); - if (relation.afterDependencyChange) { - relation.afterDependencyChange(ownViewModel, foreignViewModel); - } - return true; - } - } else if (relation.type === 'M2O') { - if (ownViewModel[relation.ownIdKey] === changedId) { - // Check, if this is the matching foreign view model. - const foreignViewModel = this.viewModelStoreService.get(collection, changedId); - ownViewModel['_' + relation.ownKey] = foreignViewModel; - if (relation.afterDependencyChange) { - relation.afterDependencyChange(ownViewModel, foreignViewModel); - } - return true; - } - } - } else if (isReverseRelationDefinition(relation)) { - const foreignViewModel = this.viewModelStoreService.get(collection, changedId); - - // The foreign model has one id. Check, if the ownViewModel is the matching view model. - // If so, add the foreignViewModel to the array from the ownViewModel (with many foreignViewModels) - // If not, check, if the model _was_ in our foreignViewModel array and remove it. - if (relation.type === 'O2M') { - if (foreignViewModel[relation.foreignIdKey] === ownViewModel.id) { - this.setForeingViewModelInOwnViewModelArray(foreignViewModel, ownViewModel, relation.ownKey); - return true; - } else { - const ownViewModelArray = ownViewModel['_' + relation.ownKey]; - if (ownViewModelArray) { - // We have the array of foreign view models for our own view model. Remove the foreignViewModel (if it was there). - const index = ownViewModelArray.findIndex( - _foreignViewModel => _foreignViewModel.id === foreignViewModel.id + if (result) { + // Cache it: + target[cacheProperty] = result; + const newChangeIds = {}; + if (Array.isArray(result)) { + result.forEach( + (_vm: BaseViewModel) => + (newChangeIds[_vm.elementId] = this.relationCacheService.query(_vm.elementId)) ); - if (index > -1) { - ownViewModelArray.splice(index, 1); - return true; - } + } else { + newChangeIds[result.elementId] = this.relationCacheService.query(result.elementId); } - } - } - - // The foreign model should hold an array of ids. If the ownViewModel is in it, the foreignViewModel must - // be included into the array from the ownViewModel (with many foreignViewModels). - // If not, check, if the model _was_ in our foreignViewModel array and remove it. - else if (relation.type === 'M2M') { - if ( - foreignViewModel[relation.foreignIdKey] && - foreignViewModel[relation.foreignIdKey].constructor === Array && - foreignViewModel[relation.foreignIdKey].includes(ownViewModel.id) - ) { - this.setForeingViewModelInOwnViewModelArray(foreignViewModel, ownViewModel, relation.ownKey); - return true; + target[cachePropertyChangeIds] = newChangeIds; } else { - const ownViewModelArray = ownViewModel['_' + relation.ownKey]; - if (ownViewModelArray) { - // We have the array of foreign view models for our own view model. Remove the foreignViewModel (if it was there). - const index = ownViewModelArray.findIndex( - _foreignViewModel => _foreignViewModel.id === foreignViewModel.id - ); - if (index > -1) { - ownViewModelArray.splice(index, 1); - return true; - } - } + delete target[cacheProperty]; } } - - // The foreign model should hold an array of ids. If the ownViewModel is in it, the foreignViewModel is the - // one and only matching model for the ownViewModel. If the ownViewModel is not in it, check if the - // foreignViewModel _was_ the matching model. If so, set the reference to null. - else if (relation.type === 'M2O') { - if ( - foreignViewModel[relation.foreignIdKey] && - foreignViewModel[relation.foreignIdKey].constructor === Array && - foreignViewModel[relation.foreignIdKey].includes(ownViewModel.id) - ) { - ownViewModel['_' + relation.ownKey] = foreignViewModel; - return true; - } else if ( - ownViewModel['_' + relation.ownKey] && - ownViewModel['_' + relation.ownKey].id === foreignViewModel.id - ) { - ownViewModel['_' + relation.ownKey] = null; - } - } - } else if (isNestedRelationDefinition(relation)) { - let updated = false; - (relation.relationDefinition || []).forEach(nestedRelation => { - const nestedViewModels = ownViewModel[relation.ownKey] as BaseViewModel[]; - nestedViewModels.forEach(nestedViewModel => { - if ( - this.updateSingleDependencyForChangedModel( - nestedViewModel, - nestedRelation, - collection, - changedId - ) - ) { - updated = true; - } - }); - }); - return updated; - } else if (isCustomRelationDefinition(relation)) { - const foreignViewModel = this.viewModelStoreService.get(collection, changedId); - return relation.updateDependency(ownViewModel, foreignViewModel); - } else if (isGenericRelationDefinition(relation)) { - const foreignModel = this.viewModelStoreService.get(collection, changedId); - if ( - foreignModel && - foreignModel.collectionString === ownViewModel[relation.ownContentObjectDataKey].collection && - foreignModel.id === ownViewModel[relation.ownContentObjectDataKey].id - ) { - if (relation.isVForeign(foreignModel)) { - ownViewModel['_' + relation.ownKey] = foreignModel; - return true; - } else { - console.warn(`The object is not an ${relation.VForeignVerbose}:` + foreignModel); - } - } - } - - return false; - } - - private setForeingViewModelInOwnViewModelArray( - foreignViewModel: BaseViewModel, - ownViewModel: BaseViewModel, - ownKey: string - ): void { - let ownViewModelArray = ownViewModel['_' + ownKey]; - if (!ownViewModelArray) { - ownViewModel['_' + ownKey] = []; - ownViewModelArray = ownViewModel['_' + ownKey]; // get the new reference - } - // We have the array of foreign view models for our own view model. Put the foreignViewModel - // into it (replace or push). - const index = ownViewModelArray.findIndex(_foreignViewModel => _foreignViewModel.id === foreignViewModel.id); - if (index < 0) { - ownViewModelArray.push(foreignViewModel); } else { - ownViewModelArray[index] = foreignViewModel; + // Custom relations + const obj = relation.getCacheObjectToCheck(viewModel); + if (cached) { + if (obj && changeIds && changeIds[obj.elementId]) { + cached = this.relationCacheService.query(obj.elementId) === changeIds[obj.elementId]; + } else { + cached = false; + } + } + + if (!cached) { + result = this.handleRelation(model, viewModel, relation); + + if (result && obj) { + target[cacheProperty] = result; + target[cachePropertyChangeIds] = {}; + target[cachePropertyChangeIds][obj.elementId] = this.relationCacheService.query(obj.elementId); + } else { + delete target[cachePropertyChangeIds]; + } + } } + + return result; + } + + /** + * Sorts the array of foreign view models in the given view models for the given relation. + */ + public sortViewModels(viewModels: BaseViewModel[], order?: string): void { + viewModels.sort((a: BaseViewModel, b: BaseViewModel) => { + if (!order || a[order] === b[order]) { + return a.id - b.id; + } else { + return a[order] - b[order]; + } + }); + } + + public createViewModel( + model: M, + viewModelCtor: ViewModelConstructor, + relationsByKey: { [key: string]: RelationDefinition }, + nestedModelDescriptors: NestedModelDescriptors + ): V { + let viewModel = new viewModelCtor(model); + viewModel = new Proxy(viewModel, { + get: (target: V, property) => { + let result: any; + const _model: M = target.getModel(); + const relation = typeof property === 'string' ? relationsByKey[property] : null; + + if (property in target) { + const descriptor = Object.getOwnPropertyDescriptor(viewModelCtor.prototype, property); + if (descriptor && descriptor.get) { + result = descriptor.get.bind(viewModel)(); + } else { + result = target[property]; + } + } else if (property in _model) { + result = _model[property]; + } else if (relation) { + result = this.handleCachedRelation(property, target, _model, viewModel, relation); + } + return result; + } + }); + + // set nested models + (nestedModelDescriptors[model.collectionString] || []).forEach( + (modelDescriptor: ModelDescriptor) => { + const nestedModels = (model[modelDescriptor.ownKey] || []).map((nestedModel: object) => { + return new modelDescriptor.foreignModel(nestedModel); + }); + const nestedViewModels = nestedModels.map(nestedModel => { + const nestedViewModel = this.createViewModel( + nestedModel, + modelDescriptor.foreignViewModel, + modelDescriptor.relationDefinitionsByKey, + nestedModelDescriptors + ); + Object.keys(modelDescriptor.titles || {}).forEach(name => { + nestedViewModel[name] = () => modelDescriptor.titles[name](nestedViewModel); + }); + return nestedViewModel; + }); + this.sortViewModels(nestedViewModels, modelDescriptor.order); + + viewModel[modelDescriptor.ownKey] = nestedViewModels; + } + ); + return viewModel; } } diff --git a/client/src/app/core/core-services/time-travel.service.ts b/client/src/app/core/core-services/time-travel.service.ts index a8ac3fdc7..3e68f77d7 100644 --- a/client/src/app/core/core-services/time-travel.service.ts +++ b/client/src/app/core/core-services/time-travel.service.ts @@ -53,7 +53,7 @@ export class TimeTravelService { * @param history the desired point in the history of OpenSlides */ public async loadHistoryPoint(history: History): Promise { - const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS, true); + const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS); await this.stopTime(history); const historyData: HistoryData = await this.getHistoryData(history); @@ -67,7 +67,7 @@ export class TimeTravelService { }); await this.DS.set(allModels, 0); - this.DSUpdateManager.commit(updateSlot); + this.DSUpdateManager.commit(updateSlot, 1, true); } /** diff --git a/client/src/app/core/definitions/relations.ts b/client/src/app/core/definitions/relations.ts index 32666c4c2..9d94ce903 100644 --- a/client/src/app/core/definitions/relations.ts +++ b/client/src/app/core/definitions/relations.ts @@ -1,12 +1,11 @@ -import { BaseModel, ModelConstructor } from 'app/shared/models/base/base-model'; +import { BaseModel } from 'app/shared/models/base/base-model'; import { BaseViewModel, ViewModelConstructor } from 'app/site/base/base-view-model'; // All "standard" relations. export type RelationDefinition = | NormalRelationDefinition | ReverseRelationDefinition - | NestedRelationDefinition - | CustomRelationDefinition + | CustomRelationDefinition | GenericRelationDefinition; interface BaseRelationDefinition { @@ -39,8 +38,6 @@ interface BaseNormalRelationDefinition extends B * the model and view model. E.g. `category_id` in a motion. */ ownIdKey: string; - - afterDependencyChange?: (ownViewModel: BaseViewModel, foreignViewModel: BaseViewModel) => void; } /** @@ -58,19 +55,16 @@ interface NormalM2MRelationDefinition extends BaseNormalRelationDefinition, BaseOrderedRelation { type: 'M2M'; - afterSetRelation?: (ownViewModel: BaseViewModel, foreignViewModels: BaseViewModel[]) => void; } interface NormalO2MRelationDefinition extends BaseNormalRelationDefinition, BaseOrderedRelation { type: 'O2M'; - afterSetRelation?: (ownViewModel: BaseViewModel, foreignViewModels: BaseViewModel[]) => void; } interface NormalM2ORelationDefinition extends BaseNormalRelationDefinition { type: 'M2O'; - afterSetRelation?: (ownViewModel: BaseViewModel, foreignViewModel: BaseViewModel | null) => void; } export type NormalRelationDefinition = @@ -131,29 +125,6 @@ export function isReverseRelationDefinition(obj: RelationDefinition): obj is Rev return (relation.type === 'M2O' || relation.type === 'O2M' || relation.type === 'M2M') && !!relation.foreignIdKey; } -/** - * Nested relations in the REST-API. For the most values see - * `NormalRelationDefinition`. - */ -export interface NestedRelationDefinition extends BaseOrderedRelation { - type: 'nested'; - ownKey: string; - - /** - * The nested relations. - */ - relationDefinition?: RelationDefinition[]; - - /** - * The matching model for the foreignViewModel. - */ - foreignModel: ModelConstructor; -} - -export function isNestedRelationDefinition(obj: RelationDefinition): obj is NestedRelationDefinition { - return obj.type === 'nested'; -} - interface GenericRelationDefinition { type: 'generic'; @@ -180,21 +151,19 @@ export function isGenericRelationDefinition(obj: RelationDefinition): obj is Gen /** * A custom relation with callbacks with things todo. */ -interface CustomRelationDefinition { +interface CustomRelationDefinition { type: 'custom'; - foreignViewModel: ViewModelConstructor; /** - * Called, when the view model is created from the model. + * The key to access the custom relation. */ - setRelations: (model: BaseModel, viewModel: BaseViewModel) => void; + ownKey: string; - /** - * Called, when the dependency was updated. - */ - updateDependency: (ownViewModel: BaseViewModel, foreignViewModel: VForeign) => boolean; + get: (model: BaseModel, viewModel: BaseViewModel) => any; + + getCacheObjectToCheck: (viewModel: BaseViewModel) => BaseViewModel | null; } -export function isCustomRelationDefinition(obj: RelationDefinition): obj is CustomRelationDefinition { +export function isCustomRelationDefinition(obj: RelationDefinition): obj is CustomRelationDefinition { return obj.type === 'custom'; } diff --git a/client/src/app/core/mixins.ts b/client/src/app/core/mixins.ts new file mode 100644 index 000000000..15f116a3b --- /dev/null +++ b/client/src/app/core/mixins.ts @@ -0,0 +1,15 @@ +/** + * Mixes all own properties of all baseCtors into the derivedCtor. + * See https://www.typescriptlang.org/docs/handbook/mixins.html + */ +export function applyMixins(derivedCtor: any, baseCtors: any[]): void { + baseCtors.forEach(baseCtor => { + Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => { + Object.defineProperty( + derivedCtor.prototype, + name, + Object.getOwnPropertyDescriptor(baseCtor.prototype, name) + ); + }); + }); +} diff --git a/client/src/app/core/pdf-services/pdf-document.service.ts b/client/src/app/core/pdf-services/pdf-document.service.ts index 8a0527c3e..1f2240d70 100644 --- a/client/src/app/core/pdf-services/pdf-document.service.ts +++ b/client/src/app/core/pdf-services/pdf-document.service.ts @@ -507,7 +507,7 @@ export class PdfDocumentService { * Cancel the pdf generation */ private cancelPdfCreation(): void { - if (!!this.pdfWorker) { + if (this.pdfWorker) { this.pdfWorker.terminate(); this.pdfWorker = null; } diff --git a/client/src/app/core/pdf-services/pdf-worker.worker.ts b/client/src/app/core/pdf-services/pdf-worker.worker.ts index f1377cd69..ab15c5bba 100644 --- a/client/src/app/core/pdf-services/pdf-worker.worker.ts +++ b/client/src/app/core/pdf-services/pdf-worker.worker.ts @@ -35,7 +35,7 @@ function applyLayout(content: any): void { if (Array.isArray(section)) { applyLayout(section); } else { - if (!!section.layout) { + if (section.layout) { let layout: object; switch (section.layout) { case 'switchColorTableLayout': { @@ -48,7 +48,7 @@ function applyLayout(content: any): void { } } - if (!!layout) { + if (layout) { section.layout = layout; } } @@ -94,7 +94,7 @@ addEventListener('message', ({ data }) => { applyLayout(data.doc.content); - if (!!data.doc.tmpfooter) { + if (data.doc.tmpfooter) { addPageNumbers(data); } diff --git a/client/src/app/core/repositories/agenda/item-repository.service.ts b/client/src/app/core/repositories/agenda/item-repository.service.ts index 0c7995475..66b22f596 100644 --- a/client/src/app/core/repositories/agenda/item-repository.service.ts +++ b/client/src/app/core/repositories/agenda/item-repository.service.ts @@ -15,7 +15,6 @@ import { ItemTitleInformation, ViewItem } from 'app/site/agenda/models/view-item import { ViewAssignment } from 'app/site/assignments/models/view-assignment'; import { BaseViewModelWithAgendaItem, - IBaseViewModelWithAgendaItem, isBaseViewModelWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item'; import { ViewMotion } from 'app/site/motions/models/view-motion'; @@ -129,8 +128,8 @@ export class ItemRepositoryService extends BaseHasContentObjectRepository< * * @returns {ViewItem} The modified item extended with the `getSubtitle()`-function. */ - protected createViewModelWithTitles(model: Item, initialLoading: boolean): ViewItem { - const viewModel = super.createViewModelWithTitles(model, initialLoading); + protected createViewModelWithTitles(model: Item): ViewItem { + const viewModel = super.createViewModelWithTitles(model); viewModel.getSubtitle = () => this.getSubtitle(viewModel); return viewModel; } @@ -153,17 +152,13 @@ export class ItemRepositoryService extends BaseHasContentObjectRepository< * @param viewModel the view model that the update is based on */ public async update(update: Partial, viewModel: ViewItem): Promise { - const sendUpdate = new this.baseModelCtor(); - sendUpdate.patchValues(viewModel.getModel()); - sendUpdate.patchValues(update); - + const sendUpdate = viewModel.getUpdatedModel(update); const clone = JSON.parse(JSON.stringify(sendUpdate)); clone.item_number = clone._itemNumber; - const restPath = `/rest/${sendUpdate.collectionString}/${sendUpdate.id}/`; - return await this.httpService.put(restPath, clone); + return await this.dataSend.updateModel(clone); } - public async addItemToAgenda(contentObject: IBaseViewModelWithAgendaItem): Promise { + public async addItemToAgenda(contentObject: BaseViewModelWithAgendaItem): Promise { return await this.httpService.post('/rest/agenda/item/', { collection: contentObject.collectionString, id: contentObject.id diff --git a/client/src/app/core/repositories/agenda/list-of-speakers-repository.service.ts b/client/src/app/core/repositories/agenda/list-of-speakers-repository.service.ts index f0b8eb091..c3f918d8b 100644 --- a/client/src/app/core/repositories/agenda/list-of-speakers-repository.service.ts +++ b/client/src/app/core/repositories/agenda/list-of-speakers-repository.service.ts @@ -24,6 +24,7 @@ import { ViewTopic } from 'app/site/topics/models/view-topic'; import { ViewUser } from 'app/site/users/models/view-user'; import { BaseHasContentObjectRepository } from '../base-has-content-object-repository'; import { BaseIsListOfSpeakersContentObjectRepository } from '../base-is-list-of-speakers-content-object-repository'; +import { NestedModelDescriptors } from '../base-repository'; import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service'; import { DataStoreService } from '../../core-services/data-store.service'; import { ItemRepositoryService } from './item-repository.service'; @@ -36,24 +37,31 @@ const ListOfSpeakersRelations: RelationDefinition[] = [ VForeignVerbose: 'BaseViewModelWithListOfSpeakers', ownContentObjectDataKey: 'contentObjectData', ownKey: 'contentObject' - }, - { - type: 'nested', - ownKey: 'speakers', - foreignViewModel: ViewSpeaker, - foreignModel: Speaker, - order: 'weight', - relationDefinition: [ - { - type: 'M2O', - ownIdKey: 'user_id', - ownKey: 'user', - foreignViewModel: ViewUser - } - ] } ]; +const ListOfSpeakersNestedModelDescriptors: NestedModelDescriptors = { + 'agenda/list-of-speakers': [ + { + ownKey: 'speakers', + foreignViewModel: ViewSpeaker, + foreignModel: Speaker, + order: 'weight', + relationDefinitionsByKey: { + user: { + type: 'M2O', + ownIdKey: 'user_id', + ownKey: 'user', + foreignViewModel: ViewUser + } + }, + titles: { + getTitle: (viewSpeaker: ViewSpeaker) => viewSpeaker.name + } + } + ] +}; + /** * Repository service for lists of speakers * @@ -96,7 +104,8 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit translate, relationManager, ListOfSpeakers, - ListOfSpeakersRelations + ListOfSpeakersRelations, + ListOfSpeakersNestedModelDescriptors ); } @@ -116,7 +125,7 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit // TODO: This can be resolved with #4738 const item = this.itemRepo.findByContentObject(titleInformation.contentObjectData); if (item) { - (titleInformation.title_information).agenda_item_number = item.itemNumber; + (titleInformation.title_information).agenda_item_number = item.item_number; } return repo.getListOfSpeakersTitle(titleInformation.title_information); diff --git a/client/src/app/core/repositories/assignments/assignment-repository.service.ts b/client/src/app/core/repositories/assignments/assignment-repository.service.ts index a16f6c91a..794f7da79 100644 --- a/client/src/app/core/repositories/assignments/assignment-repository.service.ts +++ b/client/src/app/core/repositories/assignments/assignment-repository.service.ts @@ -19,6 +19,7 @@ import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; import { ViewTag } from 'app/site/tags/models/view-tag'; import { ViewUser } from 'app/site/users/models/view-user'; import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../base-is-agenda-item-and-list-of-speakers-content-object-repository'; +import { NestedModelDescriptors } from '../base-repository'; import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service'; import { DataStoreService } from '../../core-services/data-store.service'; @@ -34,47 +35,54 @@ const AssignmentRelations: RelationDefinition[] = [ ownIdKey: 'attachments_id', ownKey: 'attachments', foreignViewModel: ViewMediafile - }, - { - type: 'nested', - ownKey: 'assignment_related_users', - foreignViewModel: ViewAssignmentRelatedUser, - foreignModel: AssignmentRelatedUser, - order: 'weight', - relationDefinition: [ - { - type: 'M2O', - ownIdKey: 'user_id', - ownKey: 'user', - foreignViewModel: ViewUser - } - ] - }, - { - type: 'nested', - ownKey: 'polls', - foreignViewModel: ViewAssignmentPoll, - foreignModel: AssignmentPoll, - relationDefinition: [ - { - type: 'nested', - ownKey: 'options', - foreignViewModel: ViewAssignmentPollOption, - foreignModel: AssignmentPollOption, - order: 'weight', - relationDefinition: [ - { - type: 'M2O', - ownIdKey: 'user_id', - ownKey: 'user', - foreignViewModel: ViewUser - } - ] - } - ] } ]; +const AssignmentNestedModelDescriptors: NestedModelDescriptors = { + 'assignments/assignment': [ + { + ownKey: 'assignment_related_users', + foreignViewModel: ViewAssignmentRelatedUser, + foreignModel: AssignmentRelatedUser, + order: 'weight', + relationDefinitionsByKey: { + user: { + type: 'M2O', + ownIdKey: 'user_id', + ownKey: 'user', + foreignViewModel: ViewUser + } + }, + titles: { + getTitle: (viewAssignmentRelatedUser: ViewAssignmentRelatedUser) => + viewAssignmentRelatedUser.user ? viewAssignmentRelatedUser.user.getFullName() : '' + } + }, + { + ownKey: 'polls', + foreignViewModel: ViewAssignmentPoll, + foreignModel: AssignmentPoll, + relationDefinitionsByKey: {} + } + ], + 'assignments/assignment-poll': [ + { + ownKey: 'options', + foreignViewModel: ViewAssignmentPollOption, + foreignModel: AssignmentPollOption, + order: 'weight', + relationDefinitionsByKey: { + user: { + type: 'M2O', + ownIdKey: 'candidate_id', + ownKey: 'user', + foreignViewModel: ViewUser + } + } + } + ] +}; + /** * Repository Service for Assignments. * @@ -122,7 +130,8 @@ export class AssignmentRepositoryService extends BaseIsAgendaItemAndListOfSpeake translate, relationManager, Assignment, - AssignmentRelations + AssignmentRelations, + AssignmentNestedModelDescriptors ); } diff --git a/client/src/app/core/repositories/base-has-content-object-repository.ts b/client/src/app/core/repositories/base-has-content-object-repository.ts index 8a654fe80..283e9c1e4 100644 --- a/client/src/app/core/repositories/base-has-content-object-repository.ts +++ b/client/src/app/core/repositories/base-has-content-object-repository.ts @@ -40,9 +40,9 @@ export abstract class BaseHasContentObjectRepository< /** * @override */ - public changedModels(ids: number[], initialLoading: boolean): void { + public changedModels(ids: number[]): void { ids.forEach(id => { - const v = this.createViewModelWithTitles(this.DS.get(this.collectionString, id), initialLoading); + const v = this.createViewModelWithTitles(this.DS.get(this.collectionString, id)); this.viewModelStore[id] = v; const contentObject = v.contentObjectData; diff --git a/client/src/app/core/repositories/base-is-agenda-item-and-list-of-speakers-content-object-repository.ts b/client/src/app/core/repositories/base-is-agenda-item-and-list-of-speakers-content-object-repository.ts index 7145a8769..d9481cdbb 100644 --- a/client/src/app/core/repositories/base-is-agenda-item-and-list-of-speakers-content-object-repository.ts +++ b/client/src/app/core/repositories/base-is-agenda-item-and-list-of-speakers-content-object-repository.ts @@ -3,10 +3,10 @@ import { ViewItem } from 'app/site/agenda/models/view-item'; import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers'; import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model'; import { - IBaseViewModelWithAgendaItem, + BaseViewModelWithAgendaItem, TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item'; -import { IBaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers'; +import { BaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers'; import { IBaseIsAgendaItemContentObjectRepository, isBaseIsAgendaItemContentObjectRepository @@ -30,14 +30,14 @@ export function isBaseIsAgendaItemAndListOfSpeakersContentObjectRepository( * multi-inheritance by implementing both inherit classes again... */ export abstract class BaseIsAgendaItemAndListOfSpeakersContentObjectRepository< - V extends BaseProjectableViewModel & IBaseViewModelWithAgendaItem & IBaseViewModelWithListOfSpeakers & T, + V extends BaseProjectableViewModel & BaseViewModelWithAgendaItem & BaseViewModelWithListOfSpeakers & T, M extends BaseModel, T extends TitleInformationWithAgendaItem > extends BaseRepository implements IBaseIsAgendaItemContentObjectRepository, IBaseIsListOfSpeakersContentObjectRepository { - protected groupRelationsByCollections(): void { + protected extendRelations(): void { this.relationDefinitions.push({ type: 'M2O', ownIdKey: 'agenda_item_id', @@ -47,10 +47,9 @@ export abstract class BaseIsAgendaItemAndListOfSpeakersContentObjectRepository< this.relationDefinitions.push({ type: 'M2O', ownIdKey: 'list_of_speakers_id', - ownKey: 'list_of_speakers', + ownKey: 'listOfSpeakers', foreignViewModel: ViewListOfSpeakers }); - super.groupRelationsByCollections(); } public getAgendaListTitle(titleInformation: T): string { @@ -87,8 +86,8 @@ export abstract class BaseIsAgendaItemAndListOfSpeakersContentObjectRepository< return this.getAgendaSlideTitle(titleInformation); }; - protected createViewModelWithTitles(model: M, initialLoading: boolean): V { - const viewModel = super.createViewModelWithTitles(model, initialLoading); + protected createViewModelWithTitles(model: M): V { + const viewModel = super.createViewModelWithTitles(model); viewModel.getAgendaListTitle = () => this.getAgendaListTitle(viewModel); viewModel.getAgendaListTitleWithoutItemNumber = () => this.getAgendaListTitleWithoutItemNumber(viewModel); viewModel.getAgendaSlideTitle = () => this.getAgendaSlideTitle(viewModel); diff --git a/client/src/app/core/repositories/base-is-agenda-item-content-object-repository.ts b/client/src/app/core/repositories/base-is-agenda-item-content-object-repository.ts index d72c36f7b..3255dc42a 100644 --- a/client/src/app/core/repositories/base-is-agenda-item-content-object-repository.ts +++ b/client/src/app/core/repositories/base-is-agenda-item-content-object-repository.ts @@ -64,14 +64,13 @@ export abstract class BaseIsAgendaItemContentObjectRepository< ); } - protected groupRelationsByCollections(): void { + protected extendRelations(): void { this.relationDefinitions.push({ type: 'M2O', ownIdKey: 'agenda_item_id', ownKey: 'item', foreignViewModel: ViewItem }); - super.groupRelationsByCollections(); } /** @@ -115,8 +114,8 @@ export abstract class BaseIsAgendaItemContentObjectRepository< /** * Adds the agenda titles to the viewmodel. */ - protected createViewModelWithTitles(model: M, initialLoading: boolean): V { - const viewModel = super.createViewModelWithTitles(model, initialLoading); + protected createViewModelWithTitles(model: M): V { + const viewModel = super.createViewModelWithTitles(model); viewModel.getAgendaListTitle = () => this.getAgendaListTitle(viewModel); viewModel.getAgendaListTitleWithoutItemNumber = () => this.getAgendaListTitleWithoutItemNumber(viewModel); viewModel.getAgendaSlideTitle = () => this.getAgendaSlideTitle(viewModel); diff --git a/client/src/app/core/repositories/base-is-list-of-speakers-content-object-repository.ts b/client/src/app/core/repositories/base-is-list-of-speakers-content-object-repository.ts index 41bfd22cc..3a32e4595 100644 --- a/client/src/app/core/repositories/base-is-list-of-speakers-content-object-repository.ts +++ b/client/src/app/core/repositories/base-is-list-of-speakers-content-object-repository.ts @@ -61,14 +61,13 @@ export abstract class BaseIsListOfSpeakersContentObjectRepository< ); } - protected groupRelationsByCollections(): void { + protected extendRelations(): void { this.relationDefinitions.push({ type: 'M2O', ownIdKey: 'list_of_speakers_id', ownKey: 'list_of_speakers', foreignViewModel: ViewListOfSpeakers }); - super.groupRelationsByCollections(); } public getListOfSpeakersTitle(titleInformation: T): string { @@ -82,8 +81,8 @@ export abstract class BaseIsListOfSpeakersContentObjectRepository< /** * Adds the list of speakers titles to the view model */ - protected createViewModelWithTitles(model: M, initialLoading: boolean): V { - const viewModel = super.createViewModelWithTitles(model, initialLoading); + protected createViewModelWithTitles(model: M): V { + const viewModel = super.createViewModelWithTitles(model); viewModel.getListOfSpeakersTitle = () => this.getListOfSpeakersTitle(viewModel); viewModel.getListOfSpeakersSlideTitle = () => this.getListOfSpeakersSlideTitle(viewModel); return viewModel; diff --git a/client/src/app/core/repositories/base-repository.ts b/client/src/app/core/repositories/base-repository.ts index d7aa45357..e7a799acc 100644 --- a/client/src/app/core/repositories/base-repository.ts +++ b/client/src/app/core/repositories/base-repository.ts @@ -7,18 +7,28 @@ import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model import { BaseViewModel, TitleInformation, ViewModelConstructor } from '../../site/base/base-view-model'; import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service'; import { DataSendService } from '../core-services/data-send.service'; -import { CollectionIds, DataStoreService } from '../core-services/data-store.service'; +import { DataStoreService } from '../core-services/data-store.service'; import { Identifiable } from '../../shared/models/base/identifiable'; import { OnAfterAppsLoaded } from '../definitions/on-after-apps-loaded'; import { RelationManagerService } from '../core-services/relation-manager.service'; -import { - isNormalRelationDefinition, - isReverseRelationDefinition, - RelationDefinition, - ReverseRelationDefinition -} from '../definitions/relations'; +import { RelationDefinition, ReverseRelationDefinition } from '../definitions/relations'; import { ViewModelStoreService } from '../core-services/view-model-store.service'; +export interface ModelDescriptor { + relationDefinitionsByKey: { [key: string]: RelationDefinition }; + ownKey: string; + foreignViewModel: ViewModelConstructor; + foreignModel: ModelConstructor; + order?: string; + titles?: { + [key: string]: (viewModel: V) => string; + }; +} + +export interface NestedModelDescriptors { + [collection: string]: ModelDescriptor[]; +} + export abstract class BaseRepository implements OnAfterAppsLoaded, Collection { /** @@ -89,6 +99,8 @@ export abstract class BaseRepository[] } = {}; + protected relationsByKey: { [key: string]: RelationDefinition } = {}; + /** * The view model ctor of the encapsulated view model. */ @@ -111,12 +123,16 @@ export abstract class BaseRepository, - protected relationDefinitions: RelationDefinition[] = [] + protected relationDefinitions: RelationDefinition[] = [], + protected nestedModelDescriptors: NestedModelDescriptors = {} ) { this._collectionString = baseModelCtor.COLLECTIONSTRING; - this.groupRelationsByCollections(); - this.buildReverseRelationsGrouping(); + this.extendRelations(); + + this.relationDefinitions.forEach(relation => { + this.relationsByKey[relation.ownKey] = relation; + }); // 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 @@ -130,55 +146,7 @@ export abstract class BaseRepository { - this._groupRelationsByCollections(relation, relation); - }); - } - - /** - * Recursive function for reorderung the relations. - */ - protected _groupRelationsByCollections(relation: RelationDefinition, baseRelation: RelationDefinition): void { - if (relation.type === 'nested') { - (relation.relationDefinition || []).forEach(nestedRelation => { - this._groupRelationsByCollections(nestedRelation, baseRelation); - }); - } else if ( - relation.type === 'M2O' || - relation.type === 'M2M' || - relation.type === 'O2M' || - relation.type === 'custom' - ) { - const collection = relation.foreignViewModel.COLLECTIONSTRING; - if (!this.relationsByCollection[collection]) { - this.relationsByCollection[collection] = []; - } - this.relationsByCollection[collection].push(baseRelation); - } else if (relation.type === 'generic') { - relation.possibleModels.forEach(ctor => { - const collection = ctor.COLLECTIONSTRING; - if (!this.relationsByCollection[collection]) { - this.relationsByCollection[collection] = []; - } - this.relationsByCollection[collection].push(baseRelation); - }); - } - } - - protected buildReverseRelationsGrouping(): void { - Object.keys(this.relationsByCollection).forEach(collection => { - const reverseRelations = this.relationsByCollection[collection].filter(relation => - isReverseRelationDefinition(relation) - ) as ReverseRelationDefinition[]; - if (reverseRelations.length) { - this.reverseRelationsByCollection[collection] = reverseRelations; - } - }); - } + protected extendRelations(): void {} public onAfterAppsLoaded(): void { this.baseViewModelCtor = this.collectionStringMapperService.getViewModelConstructor(this.collectionString); @@ -214,12 +182,9 @@ export abstract class BaseRepository { - this.viewModelStore[id] = this.createViewModelWithTitles( - this.DS.get(this.collectionString, id), - initialLoading - ); + this.viewModelStore[id] = this.createViewModelWithTitles(this.DS.get(this.collectionString, id)); this.updateViewModelObservable(id); }); } @@ -228,115 +193,20 @@ export abstract class BaseRepository this.getTitle(viewModel); viewModel.getListTitle = () => this.getListTitle(viewModel); viewModel.getVerboseName = this.getVerboseName; return viewModel; } - /** - * Updates all models in this repository with all changed models. - * - * @param changedModels A mapping of collections to ids of all changed models. - * @returns if at least one model was affected. - */ - public updateDependenciesForChangedModels(changedModels: CollectionIds): boolean { - if (!this.relationDefinitions.length) { - return false; - } - - // Get all viewModels from this repo once. - const ownViewModels = this.getViewModelList(); - const updatedIds = []; - Object.keys(changedModels).forEach(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. - const relations = this.relationsByCollection[collection]; - ownViewModels.forEach(ownViewModel => { - relations.forEach(relation => { - changedModels[collection].forEach(id => { - if ( - this.relationManager.updateSingleDependencyForChangedModel( - ownViewModel, - relation, - collection, - id - ) - ) { - updatedIds.push(ownViewModel.id); - } - }); - }); - }); - // Order all relations, if neeed. - if (updatedIds.length) { - relations.forEach(relation => { - if ( - (isNormalRelationDefinition(relation) || isReverseRelationDefinition(relation)) && - (relation.type === 'M2M' || relation.type === 'O2M') && - relation.order - ) { - ownViewModels.forEach(ownViewModel => { - if (ownViewModel['_' + relation.ownKey]) { - this.relationManager.sortByRelation(relation, ownViewModel); - } - }); - } - }); - } - - // Inform about changes. (List updates is done in `commitUpdate` via `DataStoreUpdateManagerService`) - updatedIds.forEach(id => { - this.updateViewModelObservable(id); - }); - }); - return !!updatedIds.length; - } - - public updateDependenciesForDeletedModels(deletedModels: CollectionIds): boolean { - if (!Object.keys(this.reverseRelationsByCollection).length) { - return false; - } - - // Get all viewModels from this repo once. - const ownViewModels = this.getViewModelList(); - let somethingChanged = false; - Object.keys(deletedModels).forEach(collection => { - const dependencyChanged: boolean = Object.keys(this.reverseRelationsByCollection).includes(collection); - if (!dependencyChanged) { - return; - } - - // Ok, we are affected by this collection. Update all viewModels from this repo. - const relations = this.reverseRelationsByCollection[collection]; - ownViewModels.forEach(ownViewModel => { - relations.forEach(relation => { - deletedModels[collection].forEach(id => { - if (this.relationManager.updateSingleDependencyForDeletedModel(ownViewModel, relation, id)) { - // Inform about changes. (List updates is done in `commitUpdate` via `DataStoreUpdateManagerService`) - this.updateViewModelObservable(id); - somethingChanged = true; - } - }); - }); - }); - // Ordering all relations is not needed, because just deleting things out of arrays - // will not unorder them. - }); - return somethingChanged; - } - /** * Saves the (full) update to an existing model. So called "update"-function * Provides a default procedure, but can be overwritten if required @@ -345,10 +215,8 @@ export abstract class BaseRepository, viewModel: V): Promise { - const sendUpdate = new this.baseModelCtor(); - sendUpdate.patchValues(viewModel.getModel()); - sendUpdate.patchValues(update); - return await this.dataSend.updateModel(sendUpdate); + const data = viewModel.getUpdatedModel(update); + return await this.dataSend.updateModel(data); } /** @@ -359,9 +227,8 @@ export abstract class BaseRepository, viewModel: V): Promise { - const patch = new this.baseModelCtor(); + const patch = new this.baseModelCtor(update); patch.id = viewModel.id; - patch.patchValues(update); return await this.dataSend.partialUpdateModel(patch); } @@ -384,8 +251,7 @@ export abstract class BaseRepository { // this ensures we get a valid base model, even if the view was just // sending an object with "as MyModelClass" - const sendModel = new this.baseModelCtor(); - sendModel.patchValues(model); + const sendModel = new this.baseModelCtor(model); // Strips empty fields from the sending mode data (except false) // required for i.e. users, since group list is mandatory diff --git a/client/src/app/core/repositories/config/config-repository.service.ts b/client/src/app/core/repositories/config/config-repository.service.ts index 8324ee948..bc07e2f04 100644 --- a/client/src/app/core/repositories/config/config-repository.service.ts +++ b/client/src/app/core/repositories/config/config-repository.service.ts @@ -154,8 +154,8 @@ export class ConfigRepositoryService extends BaseRepository { this.updateConfigStructure(false, this.viewModelStore[id]); @@ -246,10 +246,7 @@ export class ConfigRepositoryService extends BaseRepository, viewConfig: ViewConfig): Promise { - const updatedConfig = new Config(); - updatedConfig.patchValues(viewConfig.config); - updatedConfig.patchValues(config); - // TODO: Use datasendService, if it can switch correctly between put, post and patch + const updatedConfig = viewConfig.getUpdatedModel(config); await this.http.put(`/rest/${updatedConfig.collectionString}/${updatedConfig.key}/`, updatedConfig); } diff --git a/client/src/app/core/repositories/motions/change-recommendation-repository.service.ts b/client/src/app/core/repositories/motions/change-recommendation-repository.service.ts index 0149ad894..e3c35f77d 100644 --- a/client/src/app/core/repositories/motions/change-recommendation-repository.service.ts +++ b/client/src/app/core/repositories/motions/change-recommendation-repository.service.ts @@ -112,41 +112,29 @@ export class ChangeRecommendationRepositoryService extends BaseRepository< /** * Sets a change recommendation to accepted. * - * @param {ViewMotionChangeRecommendation} change + * @param {ViewMotionChangeRecommendation} changeRecommendation */ - public async setAccepted(change: ViewMotionChangeRecommendation): Promise { - const changeReco = change.changeRecommendation; - changeReco.patchValues({ - rejected: false - }); - await this.dataSend.partialUpdateModel(changeReco); + public async setAccepted(changeRecommendation: ViewMotionChangeRecommendation): Promise { + await this.patch({ rejected: false }, changeRecommendation); } /** * Sets a change recommendation to rejected. * - * @param {ViewMotionChangeRecommendation} change + * @param {ViewMotionChangeRecommendation} changeRecommendation */ - public async setRejected(change: ViewMotionChangeRecommendation): Promise { - const changeReco = change.changeRecommendation; - changeReco.patchValues({ - rejected: true - }); - await this.dataSend.partialUpdateModel(changeReco); + public async setRejected(changeRecommendation: ViewMotionChangeRecommendation): Promise { + await this.patch({ rejected: true }, changeRecommendation); } /** * Sets if a change recommendation is internal (for the administrators) or not. * - * @param {ViewMotionChangeRecommendation} change + * @param {ViewMotionChangeRecommendation} changeRecommendation * @param {boolean} internal */ - public async setInternal(change: ViewMotionChangeRecommendation, internal: boolean): Promise { - const changeReco = change.changeRecommendation; - changeReco.patchValues({ - internal: internal - }); - await this.dataSend.partialUpdateModel(changeReco); + public async setInternal(changeRecommendation: ViewMotionChangeRecommendation, internal: boolean): Promise { + await this.patch({ internal: internal }, changeRecommendation); } public getTitleWithChanges = (originalTitle: string, change: ViewUnifiedChange, crMode: ChangeRecoMode): string => { diff --git a/client/src/app/core/repositories/motions/motion-repository.service.ts b/client/src/app/core/repositories/motions/motion-repository.service.ts index aaab70c6e..47db7dc5b 100644 --- a/client/src/app/core/repositories/motions/motion-repository.service.ts +++ b/client/src/app/core/repositories/motions/motion-repository.service.ts @@ -32,6 +32,7 @@ import { ViewTag } from 'app/site/tags/models/view-tag'; import { ViewPersonalNote } from 'app/site/users/models/view-personal-note'; import { ViewUser } from 'app/site/users/models/view-user'; import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../base-is-agenda-item-and-list-of-speakers-content-object-repository'; +import { NestedModelDescriptors } from '../base-repository'; import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service'; import { DataSendService } from '../../core-services/data-send.service'; import { LinenumberingService, LineNumberRange } from '../../ui-services/linenumbering.service'; @@ -94,21 +95,6 @@ const MotionRelations: RelationDefinition[] = [ ownKey: 'motion_block', foreignViewModel: ViewMotionBlock }, - { - type: 'nested', - ownKey: 'submitters', - foreignViewModel: ViewSubmitter, - foreignModel: Submitter, - order: 'weight', - relationDefinition: [ - { - type: 'M2O', - ownIdKey: 'user_id', - ownKey: 'user', - foreignViewModel: ViewUser - } - ] - }, { type: 'M2M', ownIdKey: 'supporters_id', @@ -138,10 +124,39 @@ const MotionRelations: RelationDefinition[] = [ foreignIdKey: 'parent_id', ownKey: 'amendments', foreignViewModel: ViewMotion + }, + // TMP: + { + type: 'M2O', + ownIdKey: 'parent_id', + ownKey: 'parent', + foreignViewModel: ViewMotion } // Personal notes are dynamically added in the repo. ]; +const MotionNestedModelDescriptors: NestedModelDescriptors = { + 'motions/motion': [ + { + ownKey: 'submitters', + foreignViewModel: ViewSubmitter, + foreignModel: Submitter, + order: 'weight', + relationDefinitionsByKey: { + user: { + type: 'M2O', + ownIdKey: 'user_id', + ownKey: 'user', + foreignViewModel: ViewUser + } + }, + titles: { + getTitle: (viewSubmitter: ViewSubmitter) => (viewSubmitter.user ? viewSubmitter.user.getTitle() : '') + } + } + ] +}; + /** * Repository Services for motions (and potentially categories) * @@ -198,7 +213,17 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo private readonly diff: DiffService, private operator: OperatorService ) { - super(DS, dataSend, mapperService, viewModelStoreService, translate, relationManager, Motion, MotionRelations); + super( + DS, + dataSend, + mapperService, + viewModelStoreService, + translate, + relationManager, + Motion, + MotionRelations, + MotionNestedModelDescriptors + ); config.get('motions_motions_sorting').subscribe(conf => { this.sortProperty = conf; this.setConfigSortFn(); @@ -209,54 +234,6 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo }); } - /** - * Adds the personal note custom relation to the relation definitions. - * Also adds the parent relation here to get access to methods in this repo. - */ - protected groupRelationsByCollections(): void { - this.relationDefinitions.push({ - type: 'custom', - foreignViewModel: 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; - } - }); - this.relationDefinitions.push({ - type: 'M2O', - ownIdKey: 'parent_id', - ownKey: 'parent', - foreignViewModel: ViewMotion, - afterSetRelation: (motion: ViewMotion, foreignViewModel: ViewMotion | null) => { - if (foreignViewModel) { - try { - motion.diffLines = this.getAmendmentParagraphs(motion, this.motionLineLength, false); - } catch (e) { - console.warn('Error with motion or amendment ', motion); - } - } - }, - afterDependencyChange: (motion: ViewMotion, parent: ViewMotion) => { - if (motion.parent) { - try { - motion.diffLines = this.getAmendmentParagraphs(motion, this.motionLineLength, false); - } catch (e) { - console.warn('Error with motion or amendment: ', motion); - } - } - } - }); - super.groupRelationsByCollections(); - } - public getTitle = (titleInformation: MotionTitleInformation) => { if (titleInformation.identifier) { return titleInformation.identifier + ': ' + titleInformation.title; @@ -321,8 +298,8 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo return this.translate.instant(plural ? 'Motions' : 'Motion'); }; - protected createViewModelWithTitles(model: Motion, initialLoading: boolean): ViewMotion { - const viewModel = super.createViewModelWithTitles(model, initialLoading); + protected createViewModelWithTitles(model: Motion): ViewMotion { + const viewModel = super.createViewModelWithTitles(model); viewModel.getIdentifierOrTitle = () => this.getIdentifierOrTitle(viewModel); viewModel.getProjectorTitle = () => this.getAgendaSlideTitle(viewModel); @@ -330,6 +307,37 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo return viewModel; } + protected extendRelations(): void { + this.relationDefinitions.push({ + type: 'custom', + ownKey: 'personalNote', + get: (motion: Motion, viewMotion: ViewMotion) => { + return this.getPersonalNoteForMotion(motion); + }, + getCacheObjectToCheck: (viewMotion: ViewMotion) => this.getPersonalNote() + }); + this.relationDefinitions.push({ + type: 'custom', + ownKey: 'diffLines', + get: (motion: Motion, viewMotion: ViewMotion) => { + if (viewMotion.parent) { + return this.getAmendmentParagraphs(viewMotion, this.motionLineLength, false); + } + }, + getCacheObjectToCheck: (viewMotion: ViewMotion) => viewMotion.parent + }); + super.extendRelations(); + } + + /** + * @returns the personal note for the operator. + */ + private getPersonalNote(): ViewPersonalNote | null { + return this.viewModelStoreService.find(ViewPersonalNote, pn => { + return pn.user_id === this.operator.user.id; + }); + } + /** * Get the personal note content for one motion by their id * @@ -341,9 +349,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo return; } - const personalNote = this.viewModelStoreService.find(ViewPersonalNote, pn => { - return pn.userId === this.operator.user.id; - }); + const personalNote = this.getPersonalNote(); if (!personalNote) { return; } @@ -735,7 +741,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo const motion = amendment.parent; const baseParagraphs = this.getTextParagraphs(motion, true, lineLength); - return amendment.amendment_paragraphs + return (amendment.amendment_paragraphs || []) .map( (newText: string, paraNo: number): DiffLinesInParagraph => { if (newText !== null) { @@ -779,7 +785,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo const motion = amendment.parent; const baseParagraphs = this.getTextParagraphs(motion, true, lineLength); - return amendment.amendment_paragraphs + return (amendment.amendment_paragraphs || []) .map( (newText: string, paraNo: number): ViewMotionAmendedParagraph => { if (newText === null) { diff --git a/client/src/app/core/repositories/motions/state-repository.service.ts b/client/src/app/core/repositories/motions/state-repository.service.ts index 0786f6cc4..975b2c17d 100644 --- a/client/src/app/core/repositories/motions/state-repository.service.ts +++ b/client/src/app/core/repositories/motions/state-repository.service.ts @@ -25,6 +25,12 @@ const StateRelations: RelationDefinition[] = [ ownIdKey: 'next_states_id', ownKey: 'next_states', foreignViewModel: ViewState + }, + { + type: 'M2M', + foreignIdKey: 'next_states_id', + ownKey: 'previous_states', + foreignViewModel: ViewState } ]; diff --git a/client/src/app/core/repositories/projector/projector-repository.service.ts b/client/src/app/core/repositories/projector/projector-repository.service.ts index 0e1deb597..57004ba46 100644 --- a/client/src/app/core/repositories/projector/projector-repository.service.ts +++ b/client/src/app/core/repositories/projector/projector-repository.service.ts @@ -80,8 +80,7 @@ export class ProjectorRepositoryService extends BaseRepository): Promise { - const projector = new Projector(); - projector.patchValues(projectorData); + const projector = new Projector(projectorData); projector.elements = [{ name: 'core/clock', stable: true }]; return await this.dataSend.createModel(projector); } diff --git a/client/src/app/core/repositories/users/user-repository.service.ts b/client/src/app/core/repositories/users/user-repository.service.ts index ce2076b6e..699af2609 100644 --- a/client/src/app/core/repositories/users/user-repository.service.ts +++ b/client/src/app/core/repositories/users/user-repository.service.ts @@ -147,8 +147,8 @@ export class UserRepositoryService extends BaseRepository this.getFullName(viewModel); viewModel.getShortName = () => this.getShortName(viewModel); return viewModel; @@ -163,23 +163,19 @@ export class UserRepositoryService extends BaseRepository, viewUser: ViewUser): Promise { - const updateUser = new User(); - updateUser.patchValues(viewUser.user); - updateUser.patchValues(update); - // if the user deletes the username, reset // prevents the server of generating ' +1' as username - if (updateUser.username === '') { - updateUser.username = viewUser.username; + if (update.username === '') { + update.username = viewUser.username; } // if the update user does not have a gender-field, send gender as empty string. // This allow to delete a previously selected gender - if (!updateUser.gender) { - updateUser.gender = ''; + if (!update.gender) { + update.gender = ''; } - return await this.dataSend.updateModel(updateUser); + return super.update(update, viewUser); } /** diff --git a/client/src/app/core/ui-services/base-filter-list.service.ts b/client/src/app/core/ui-services/base-filter-list.service.ts index 5caf3c8f0..455ea51b1 100644 --- a/client/src/app/core/ui-services/base-filter-list.service.ts +++ b/client/src/app/core/ui-services/base-filter-list.service.ts @@ -309,7 +309,7 @@ export abstract class BaseFilterListService { }; }); - if (!!noneOptionLabel) { + if (noneOptionLabel) { filterProperties.push('-'); filterProperties.push({ condition: null, diff --git a/client/src/app/core/ui-services/base-sort.service.ts b/client/src/app/core/ui-services/base-sort.service.ts index 547885f5e..9e99a3705 100644 --- a/client/src/app/core/ui-services/base-sort.service.ts +++ b/client/src/app/core/ui-services/base-sort.service.ts @@ -95,7 +95,7 @@ export abstract class BaseSortService { case 'number': return firstProperty > secondProperty ? 1 : -1; case 'string': - if (!!firstProperty && !secondProperty) { + if (firstProperty && !secondProperty) { return -1; } else if (!firstProperty && !!secondProperty) { return 1; diff --git a/client/src/app/shared/components/extension-field/extension-field.component.ts b/client/src/app/shared/components/extension-field/extension-field.component.ts index 38232228b..a37b72426 100644 --- a/client/src/app/shared/components/extension-field/extension-field.component.ts +++ b/client/src/app/shared/components/extension-field/extension-field.component.ts @@ -156,7 +156,7 @@ export class ExtensionFieldComponent implements OnInit, OnDestroy { this.searchValueSubscription = this.extensionFieldForm .get('list') .valueChanges.subscribe((value: number) => { - if (!!value) { + if (value) { if (this.listSubmitOnChange) { this.listChange.emit(value); } diff --git a/client/src/app/shared/components/list-view-table/list-view-table.component.ts b/client/src/app/shared/components/list-view-table/list-view-table.component.ts index 427bfc381..378eaf7f1 100644 --- a/client/src/app/shared/components/list-view-table/list-view-table.component.ts +++ b/client/src/app/shared/components/list-view-table/list-view-table.component.ts @@ -531,7 +531,7 @@ export class ListViewTableComponent { - const scrollIndex = await this.getScrollIndex(this.listStorageKey); - this.ngrid.viewport.scrollToIndex(scrollIndex); + if (this.ngrid) { + const scrollIndex = await this.getScrollIndex(this.listStorageKey); + this.ngrid.viewport.scrollToIndex(scrollIndex); + } } /** diff --git a/client/src/app/shared/components/rounded-input/rounded-input.component.ts b/client/src/app/shared/components/rounded-input/rounded-input.component.ts index 903ef16f5..bb55f73b9 100644 --- a/client/src/app/shared/components/rounded-input/rounded-input.component.ts +++ b/client/src/app/shared/components/rounded-input/rounded-input.component.ts @@ -47,7 +47,7 @@ export class RoundedInputComponent implements OnInit, OnDestroy { */ @Input() public set model(value: string) { - if (!!value) { + if (value) { this.modelForm.setValue(value); } } diff --git a/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts b/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts index 1113b064f..d8340b574 100644 --- a/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts +++ b/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts @@ -633,7 +633,7 @@ export class SortingTreeComponent implemen break; case Direction.RIGHT: - if (!!possibleParent) { + if (possibleParent) { const nextLevel = this.nextNode.level + direction.steps; if (nextLevel <= possibleParent.level) { this.placeholderLevel = nextLevel; @@ -646,7 +646,7 @@ export class SortingTreeComponent implemen break; case Direction.NOWAY: - if (!!possibleParent) { + if (!possibleParent) { if (this.nextNode.level <= possibleParent.level + 1) { this.placeholderLevel = this.nextNode.level; } else { @@ -678,7 +678,7 @@ export class SortingTreeComponent implemen case Direction.UPWARDS: for (let i = 0; i < steps; ++i) { const parent = this.getExpandedParentNode(this.osTreeData[currentPosition - 1]); - if (!!parent) { + if (parent) { currentPosition = parent.position; } else { break; @@ -693,7 +693,7 @@ export class SortingTreeComponent implemen currentPosition -= 1; for (let i = 0; i < steps; ++i) { const parent = this.getExpandedParentNode(this.osTreeData[currentPosition]); - if (!!parent) { + if (parent) { currentPosition = parent.position - 1; } else { break; diff --git a/client/src/app/shared/components/speaker-button/speaker-button.component.ts b/client/src/app/shared/components/speaker-button/speaker-button.component.ts index f1d2c922c..523c9e4e7 100644 --- a/client/src/app/shared/components/speaker-button/speaker-button.component.ts +++ b/client/src/app/shared/components/speaker-button/speaker-button.component.ts @@ -35,7 +35,7 @@ export class SpeakerButtonComponent implements OnDestroy { this.cleanLosSub(); - if (!!listOfSpeakers) { + if (listOfSpeakers) { this.losSub = this.listOfSpeakersRepo .getViewModelObservable(listOfSpeakers.id) .pipe(distinctUntilChanged()) diff --git a/client/src/app/shared/models/agenda/list-of-speakers.ts b/client/src/app/shared/models/agenda/list-of-speakers.ts index e85c121ae..d84d05c0e 100644 --- a/client/src/app/shared/models/agenda/list-of-speakers.ts +++ b/client/src/app/shared/models/agenda/list-of-speakers.ts @@ -2,6 +2,12 @@ import { BaseModelWithContentObject } from '../base/base-model-with-content-obje import { ContentObject } from '../base/content-object'; import { Speaker } from './speaker'; +export interface ListOfSpeakersWithoutNestedModels extends BaseModelWithContentObject { + id: number; + title_information: object; + closed: boolean; +} + /** * Representations of agenda Item * @ignore @@ -19,3 +25,5 @@ export class ListOfSpeakers extends BaseModelWithContentObject { super(ListOfSpeakers.COLLECTIONSTRING, input); } } + +export interface ListOfSpeakers extends ListOfSpeakersWithoutNestedModels {} diff --git a/client/src/app/shared/models/assignments/assignment-poll-option.ts b/client/src/app/shared/models/assignments/assignment-poll-option.ts index 943a9a259..43bdc5a3b 100644 --- a/client/src/app/shared/models/assignments/assignment-poll-option.ts +++ b/client/src/app/shared/models/assignments/assignment-poll-option.ts @@ -17,10 +17,6 @@ export class AssignmentPollOption extends BaseModel { public id: number; // The AssignmentPollOption id public candidate_id: number; // the user id of the candidate - public get user_id(): number { - // to be consistent with user... - return this.candidate_id; - } public is_elected: boolean; public votes: AssignmentOptionVote[]; public poll_id: number; diff --git a/client/src/app/shared/models/assignments/assignment-poll.ts b/client/src/app/shared/models/assignments/assignment-poll.ts index 51d5abf98..eef94c237 100644 --- a/client/src/app/shared/models/assignments/assignment-poll.ts +++ b/client/src/app/shared/models/assignments/assignment-poll.ts @@ -2,6 +2,20 @@ import { AssignmentPollMethod } from 'app/site/assignments/services/assignment-p import { AssignmentPollOption } from './assignment-poll-option'; import { BaseModel } from '../base/base-model'; +export interface AssignmentPollWithoutNestedModels extends BaseModel { + id: number; + pollmethod: AssignmentPollMethod; + description: string; + published: boolean; + votesvalid: number; + votesno: number; + votesabstain: number; + votesinvalid: number; + votescast: number; + has_votes: boolean; + assignment_id: number; +} + /** * Content of the 'polls' property of assignments * @ignore @@ -11,21 +25,8 @@ export class AssignmentPoll extends BaseModel { private static DECIMAL_FIELDS = ['votesvalid', 'votesinvalid', 'votescast', 'votesno', 'votesabstain']; public id: number; - public pollmethod: AssignmentPollMethod; - public description: string; - public published: boolean; public options: AssignmentPollOption[]; - public votesvalid: number; - public votesno: number; - public votesabstain: number; - public votesinvalid: number; - public votescast: number; - public has_votes: boolean; - public assignment_id: number; - /** - * @param input - */ public constructor(input?: any) { // cast stringify numbers if (input) { @@ -38,3 +39,4 @@ export class AssignmentPoll extends BaseModel { super(AssignmentPoll.COLLECTIONSTRING, input); } } +export interface AssignmentPoll extends AssignmentPollWithoutNestedModels {} diff --git a/client/src/app/shared/models/assignments/assignment.ts b/client/src/app/shared/models/assignments/assignment.ts index 260e5491b..d05a90f4a 100644 --- a/client/src/app/shared/models/assignments/assignment.ts +++ b/client/src/app/shared/models/assignments/assignment.ts @@ -2,6 +2,17 @@ import { AssignmentPoll } from './assignment-poll'; import { AssignmentRelatedUser } from './assignment-related-user'; import { BaseModelWithAgendaItemAndListOfSpeakers } from '../base/base-model-with-agenda-item-and-list-of-speakers'; +export interface AssignmentWithoutNestedModels extends BaseModelWithAgendaItemAndListOfSpeakers { + id: number; + title: string; + description: string; + open_posts: number; + phase: number; // see Openslides constants + poll_description_default: number; + tags_id: number[]; + attachments_id: number[]; +} + /** * Representation of an assignment. * @ignore @@ -10,15 +21,8 @@ export class Assignment extends BaseModelWithAgendaItemAndListOfSpeakers candidate.user_id); } - - public deserialize(input: any): void { - Object.assign(this, input); - - this.polls = []; - if (input.polls instanceof Array) { - this.polls = input.polls.map(pollData => new AssignmentPoll(pollData)); - } - } } +export interface Assignment extends AssignmentWithoutNestedModels {} diff --git a/client/src/app/shared/models/base/base-model-with-content-object.ts b/client/src/app/shared/models/base/base-model-with-content-object.ts index b0958ff23..8fd3ff1b9 100644 --- a/client/src/app/shared/models/base/base-model-with-content-object.ts +++ b/client/src/app/shared/models/base/base-model-with-content-object.ts @@ -4,7 +4,7 @@ import { ContentObject } from './content-object'; /** * A base model which has a content object, like items of list of speakers. */ -export abstract class BaseModelWithContentObject extends BaseModel { +export abstract class BaseModelWithContentObject extends BaseModel { public abstract content_object: ContentObject; public get contentObjectData(): ContentObject { diff --git a/client/src/app/shared/models/base/base-model.ts b/client/src/app/shared/models/base/base-model.ts index ebcf52131..22e1ce5e7 100644 --- a/client/src/app/shared/models/base/base-model.ts +++ b/client/src/app/shared/models/base/base-model.ts @@ -1,5 +1,5 @@ import { Collection } from './collection'; -import { Deserializable } from './deserializable'; +import { Deserializer } from './deserializer'; import { Identifiable } from './identifiable'; export interface ModelConstructor> { @@ -11,69 +11,21 @@ export interface ModelConstructor> { * Abstract parent class to set rules and functions for all models. * When inherit from this class, give the subclass as the type. E.g. `class Motion extends BaseModel` */ -export abstract class BaseModel implements Deserializable, Identifiable, Collection { - /** - * force children of BaseModel to have a collectionString. - * - * Has a getter but no setter. - */ - protected _collectionString: string; - - /** - * returns the collectionString. - * - * The server and the dataStore use it to identify the collection. - */ - public get collectionString(): string { - return this._collectionString; +export abstract class BaseModel extends Deserializer implements Identifiable, Collection { + public get elementId(): string { + return `${this.collectionString}:${this.id}`; } - /** * force children of BaseModel to have an id */ public abstract id: number; - /** - * constructor that calls super from parent class - * - * @param collectionString - * @param verboseName - * @param input - */ - protected constructor(collectionString: string, input?: any) { - this._collectionString = collectionString; - - if (input) { - this.changeNullValuesToUndef(input); - this.deserialize(input); - } + protected constructor(public readonly collectionString: string, input?: Partial) { + super(input); } - /** - * Prevent to send literally "null" if should be send - * @param input object to deserialize - */ - public changeNullValuesToUndef(input: any): void { - Object.keys(input).forEach(key => { - if (input[key] === null) { - input[key] = undefined; - } - }); - } - - /** - * update the values of the base model with new values - */ - public patchValues(update: Partial): void { - Object.assign(this, update); - } - - /** - * Most simple and most commonly used deserialize function. - * Inherited to children, can be overwritten for special use cases - * @param input JSON data for deserialization. - */ - public deserialize(input: any): void { - Object.assign(this, input); + public getUpdatedVersion(update: Partial): T { + const copy: T = (Object.assign({}, this)) as T; + return Object.assign(copy, update); } } diff --git a/client/src/app/shared/models/base/deserializer.ts b/client/src/app/shared/models/base/deserializer.ts index a21b40373..44bad2986 100644 --- a/client/src/app/shared/models/base/deserializer.ts +++ b/client/src/app/shared/models/base/deserializer.ts @@ -11,28 +11,16 @@ export abstract class Deserializer implements Deserializable { */ protected constructor(input?: any) { if (input) { - this.changeNullValuesToUndef(input); this.deserialize(input); } } /** - * should be used to assign JSON values to the object itself. - * @param input + * Most simple and most commonly used deserialize function. + * Inherited to children, can be overwritten for special use cases + * @param input JSON data for deserialization. */ public deserialize(input: any): void { Object.assign(this, input); } - - /** - * Prevent to send literally "null" if should be send - * @param input object to deserialize - */ - public changeNullValuesToUndef(input: any): void { - Object.keys(input).forEach(key => { - if (input[key] === null) { - input[key] = undefined; - } - }); - } } diff --git a/client/src/app/shared/models/mediafiles/mediafile.ts b/client/src/app/shared/models/mediafiles/mediafile.ts index d8495d421..7973f64c9 100644 --- a/client/src/app/shared/models/mediafiles/mediafile.ts +++ b/client/src/app/shared/models/mediafiles/mediafile.ts @@ -9,6 +9,21 @@ interface FileMetadata { encrypted?: boolean; } +export interface MediafileWithoutNestedModels extends BaseModelWithListOfSpeakers { + id: number; + title: string; + media_url_prefix: string; + filesize?: string; + access_groups_id: number[]; + create_timestamp: string; + parent_id: number | null; + is_directory: boolean; + path: string; + inherited_access_groups_id: boolean | number[]; + + has_inherited_access_groups: boolean; +} + /** * Representation of MediaFile. Has the nested property "File" * @ignore @@ -16,25 +31,14 @@ interface FileMetadata { export class Mediafile extends BaseModelWithListOfSpeakers { public static COLLECTIONSTRING = 'mediafiles/mediafile'; public id: number; - public title: string; public mediafile?: FileMetadata; - public media_url_prefix: string; - public filesize?: string; - public access_groups_id: number[]; - public create_timestamp: string; - public parent_id: number | null; - public is_directory: boolean; - public path: string; - public inherited_access_groups_id: boolean | number[]; public get has_inherited_access_groups(): boolean { return typeof this.inherited_access_groups_id !== 'boolean'; } public constructor(input?: any) { - super(Mediafile.COLLECTIONSTRING); - // Do not change null to undefined... - this.deserialize(input); + super(Mediafile.COLLECTIONSTRING, input); } /** @@ -46,3 +50,4 @@ export class Mediafile extends BaseModelWithListOfSpeakers { return `${this.media_url_prefix}${this.path}`; } } +export interface Mediafile extends MediafileWithoutNestedModels {} diff --git a/client/src/app/shared/models/motions/motion.ts b/client/src/app/shared/models/motions/motion.ts index 9e727ecb6..6aa35620e 100644 --- a/client/src/app/shared/models/motions/motion.ts +++ b/client/src/app/shared/models/motions/motion.ts @@ -9,6 +9,40 @@ export interface MotionComment { read_groups_id: number[]; } +export interface MotionWithoutNestedModels extends BaseModelWithAgendaItemAndListOfSpeakers { + id: number; + identifier: string; + title: string; + text: string; + reason: string; + amendment_paragraphs: string[] | null; + modified_final_version: string; + parent_id: number; + category_id: number; + category_weight: number; + motion_block_id: number; + origin: string; + supporters_id: number[]; + comments: MotionComment[]; + workflow_id: number; + state_id: number; + state_extension: string; + state_required_permission_to_see: string; + statute_paragraph_id: number; + recommendation_id: number; + recommendation_extension: string; + tags_id: number[]; + attachments_id: number[]; + polls: MotionPoll[]; + weight: number; + sort_parent_id: number; + created: string; + last_modified: string; + change_recommendations_id: number[]; + + sorted_submitter_ids: number[]; +} + /** * Representation of Motion. * @@ -20,35 +54,7 @@ export class Motion extends BaseModelWithAgendaItemAndListOfSpeakers { public static COLLECTIONSTRING = 'motions/motion'; public id: number; - public identifier: string; - public title: string; - public text: string; - public reason: string; - public amendment_paragraphs: string[]; - public modified_final_version: string; - public parent_id: number; - public category_id: number; - public category_weight: number; - public motion_block_id: number; - public origin: string; public submitters: Submitter[]; - public supporters_id: number[]; - public comments: MotionComment[]; - public workflow_id: number; - public state_id: number; - public state_extension: string; - public state_required_permission_to_see: string; - public statute_paragraph_id: number; - public recommendation_id: number; - public recommendation_extension: string; - public tags_id: number[]; - public attachments_id: number[]; - public polls: MotionPoll[]; - public weight: number; - 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); @@ -57,7 +63,7 @@ export class Motion extends BaseModelWithAgendaItemAndListOfSpeakers { /** * returns the motion submitters user ids */ - public get sorted_submitters_id(): number[] { + public get sorted_submitter_ids(): number[] { return this.submitters .sort((a: Submitter, b: Submitter) => { return a.weight - b.weight; @@ -65,3 +71,5 @@ export class Motion extends BaseModelWithAgendaItemAndListOfSpeakers { .map((submitter: Submitter) => submitter.user_id); } } + +export interface Motion extends MotionWithoutNestedModels {} diff --git a/client/src/app/shared/models/motions/state.ts b/client/src/app/shared/models/motions/state.ts index 12c5ad921..d8a6d283e 100644 --- a/client/src/app/shared/models/motions/state.ts +++ b/client/src/app/shared/models/motions/state.ts @@ -40,21 +40,4 @@ export class State extends BaseModel { public constructor(input?: any) { super(State.COLLECTIONSTRING, input); } - - public toString = (): string => { - return this.name; - }; - - /** - * Checks if a workflowstate has no 'next state' left, and is final - */ - public get isFinalState(): boolean { - if (!this.next_states_id || !this.next_states_id.length) { - return true; - } - if (this.next_states_id.length === 1 && this.next_states_id[0] === 0) { - return true; - } - return false; - } } diff --git a/client/src/app/shared/models/topics/topic.ts b/client/src/app/shared/models/topics/topic.ts index 695fd4a47..a902069b5 100644 --- a/client/src/app/shared/models/topics/topic.ts +++ b/client/src/app/shared/models/topics/topic.ts @@ -12,7 +12,7 @@ export class Topic extends BaseModelWithAgendaItemAndListOfSpeakers { public text: string; public attachments_id: number[]; - public constructor(input?: any) { + public constructor(input?: Partial) { super(Topic.COLLECTIONSTRING, input); } } diff --git a/client/src/app/shared/models/users/group.ts b/client/src/app/shared/models/users/group.ts index 0390220fe..14dbbddfc 100644 --- a/client/src/app/shared/models/users/group.ts +++ b/client/src/app/shared/models/users/group.ts @@ -11,11 +11,7 @@ export class Group extends BaseModel { public name: string; public permissions: string[]; - public constructor(input?: any) { + public constructor(input?: Partial) { super(Group.COLLECTIONSTRING, input); - if (!input) { - // permissions are required for new groups - this.permissions = []; - } } } diff --git a/client/src/app/shared/models/users/personal-note.ts b/client/src/app/shared/models/users/personal-note.ts index 48610c7ea..549b8ebfd 100644 --- a/client/src/app/shared/models/users/personal-note.ts +++ b/client/src/app/shared/models/users/personal-note.ts @@ -55,7 +55,7 @@ export class PersonalNote extends BaseModel implements PersonalNot public user_id: number; public notes: PersonalNotesFormat; - public constructor(input: any) { + public constructor(input: Partial) { super(PersonalNote.COLLECTIONSTRING, input); } } diff --git a/client/src/app/shared/models/users/user.ts b/client/src/app/shared/models/users/user.ts index 58ae9788d..8ae43b4fe 100644 --- a/client/src/app/shared/models/users/user.ts +++ b/client/src/app/shared/models/users/user.ts @@ -18,20 +18,20 @@ export class User extends BaseModel { public title: string; public first_name: string; public last_name: string; - public gender: string; + public gender?: string; public structure_level: string; public number: string; public about_me: string; public groups_id: number[]; public is_present: boolean; public is_committee: boolean; - public email: string; + public email?: string; public last_email_send?: string; // ISO datetime string - public comment: string; - public is_active: boolean; - public default_password: string; + public comment?: string; + public is_active?: boolean; + public default_password?: string; - public constructor(input?: any) { + public constructor(input?: Partial) { super(User.COLLECTIONSTRING, input); } } diff --git a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts index 75fa431b1..6c0882573 100644 --- a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts +++ b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts @@ -98,7 +98,7 @@ export class AgendaListComponent extends BaseListViewComponent impleme /** * Define extra filter properties */ - public filterProps = ['itemNumber', 'comment', 'getListTitle']; + public filterProps = ['item_number', 'comment', 'getListTitle']; /** * The usual constructor for components @@ -205,7 +205,7 @@ export class AgendaListComponent extends BaseListViewComponent impleme public async onAutoNumbering(): Promise { const title = this.translate.instant('Are you sure you want to number all agenda items?'); if (await this.promptService.open(title)) { - await this.repo.autoNumbering().then(null, this.raiseError); + await this.repo.autoNumbering().catch(this.raiseError); } } @@ -213,7 +213,7 @@ export class AgendaListComponent extends BaseListViewComponent impleme * Click handler for the done button in the dot-menu */ public async onDoneSingleButton(item: ViewItem): Promise { - await this.repo.update({ closed: !item.closed }, item).then(null, this.raiseError); + await this.repo.update({ closed: !item.closed }, item).catch(this.raiseError); } /** @@ -233,7 +233,7 @@ export class AgendaListComponent extends BaseListViewComponent impleme const title = this.translate.instant('Are you sure you want to remove this entry from the agenda?'); const content = item.contentObject.getTitle(); if (await this.promptService.open(title, content)) { - await this.repo.removeFromAgenda(item).then(null, this.raiseError); + await this.repo.removeFromAgenda(item).catch(this.raiseError); } } @@ -244,7 +244,7 @@ export class AgendaListComponent extends BaseListViewComponent impleme const title = this.translate.instant('Are you sure you want to delete this topic?'); const content = item.contentObject.getTitle(); if (await this.promptService.open(title, content)) { - await this.topicRepo.delete(item.contentObject).then(null, this.raiseError); + await this.topicRepo.delete(item.contentObject).catch(this.raiseError); } } @@ -295,7 +295,7 @@ export class AgendaListComponent extends BaseListViewComponent impleme public async setAgendaType(agendaType: number): Promise { try { for (const item of this.selectedRows) { - await this.repo.update({ type: agendaType }, item).then(null, this.raiseError); + await this.repo.update({ type: agendaType }, item).catch(this.raiseError); } } catch (e) { this.raiseError(e); diff --git a/client/src/app/site/agenda/components/item-info-dialog/item-info-dialog.component.ts b/client/src/app/site/agenda/components/item-info-dialog/item-info-dialog.component.ts index 6e15366ac..05138abcd 100644 --- a/client/src/app/site/agenda/components/item-info-dialog/item-info-dialog.component.ts +++ b/client/src/app/site/agenda/components/item-info-dialog/item-info-dialog.component.ts @@ -51,7 +51,7 @@ export class ItemInfoDialogComponent { // load current values this.agendaInfoForm.get('type').setValue(item.type); this.agendaInfoForm.get('durationText').setValue(this.durationService.durationToString(item.duration, 'h')); - this.agendaInfoForm.get('item_number').setValue(item.itemNumber); + this.agendaInfoForm.get('item_number').setValue(item.item_number); this.agendaInfoForm.get('comment').setValue(item.comment); } diff --git a/client/src/app/site/agenda/components/list-of-speakers/list-of-speakers.component.html b/client/src/app/site/agenda/components/list-of-speakers/list-of-speakers.component.html index fc5e61525..322ef7f59 100644 --- a/client/src/app/site/agenda/components/list-of-speakers/list-of-speakers.component.html +++ b/client/src/app/site/agenda/components/list-of-speakers/list-of-speakers.component.html @@ -60,7 +60,7 @@ mic - {{ activeSpeaker.getTitle() }} + {{ activeSpeaker.getListTitle() }} diff --git a/client/src/app/site/agenda/components/list-of-speakers/list-of-speakers.component.ts b/client/src/app/site/agenda/components/list-of-speakers/list-of-speakers.component.ts index 6258905cd..362f986cb 100644 --- a/client/src/app/site/agenda/components/list-of-speakers/list-of-speakers.component.ts +++ b/client/src/app/site/agenda/components/list-of-speakers/list-of-speakers.component.ts @@ -417,9 +417,7 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit */ public closeSpeakerList(): Promise { if (!this.viewListOfSpeakers.closed) { - return this.listOfSpeakersRepo - .update({ closed: true }, this.viewListOfSpeakers) - .then(null, this.raiseError); + return this.listOfSpeakersRepo.update({ closed: true }, this.viewListOfSpeakers).catch(this.raiseError); } } @@ -428,9 +426,7 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit */ public openSpeakerList(): Promise { if (this.viewListOfSpeakers.closed) { - return this.listOfSpeakersRepo - .update({ closed: false }, this.viewListOfSpeakers) - .then(null, this.raiseError); + return this.listOfSpeakersRepo.update({ closed: false }, this.viewListOfSpeakers).catch(this.raiseError); } } diff --git a/client/src/app/site/agenda/models/view-item.ts b/client/src/app/site/agenda/models/view-item.ts index f1ba15ead..408dbfa11 100644 --- a/client/src/app/site/agenda/models/view-item.ts +++ b/client/src/app/site/agenda/models/view-item.ts @@ -18,34 +18,6 @@ export class ViewItem extends BaseViewModelWithContentObject string | null; /** @@ -71,19 +43,5 @@ export class ViewItem extends BaseViewModelWithContentObject choice.key === this.type); return type ? type.csvName : ''; } - - /** - * @returns the weight the server assigns to that item. Mostly useful for sorting within - * it's own hierarchy level (items sharing a parent) - */ - public get weight(): number { - return this.item.weight; - } - - /** - * @returns the parent's id of that item (0 if no parent is set). - */ - public get parent_id(): number { - return this.item.parent_id; - } } +export interface ViewItem extends Item {} diff --git a/client/src/app/site/agenda/models/view-list-of-speakers.ts b/client/src/app/site/agenda/models/view-list-of-speakers.ts index 99b4ea921..330a6647a 100644 --- a/client/src/app/site/agenda/models/view-list-of-speakers.ts +++ b/client/src/app/site/agenda/models/view-list-of-speakers.ts @@ -1,4 +1,4 @@ -import { ListOfSpeakers } from 'app/shared/models/agenda/list-of-speakers'; +import { ListOfSpeakers, ListOfSpeakersWithoutNestedModels } from 'app/shared/models/agenda/list-of-speakers'; import { ContentObject } from 'app/shared/models/base/content-object'; import { BaseViewModelWithContentObject } from 'app/site/base/base-view-model-with-content-object'; import { BaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers'; @@ -19,20 +19,10 @@ export class ViewListOfSpeakers extends BaseViewModelWithContentObject speaker.state === SpeakerState.WAITING).length; } - public get closed(): boolean { - return this.listOfSpeakers.closed; - } - public get listOfSpeakersUrl(): string { return `/agenda/speakers/${this.id}`; } @@ -65,3 +51,7 @@ export class ViewListOfSpeakers extends BaseViewModelWithContentObject { public static COLLECTIONSTRING = Speaker.COLLECTIONSTRING; protected _collectionString = Speaker.COLLECTIONSTRING; - private _user?: ViewUser; - public get speaker(): Speaker { return this._model; } - public get user(): ViewUser | null { - return this._user; - } - - public get id(): number { - return this.speaker.id; - } - - public get user_id(): number { - return this.speaker.user_id; - } - - public get weight(): number { - return this.speaker.weight; - } - - public get marked(): boolean { - return this.speaker.marked; - } - - /** - * @returns an ISO datetime string or null - */ - public get begin_time(): string | null { - return this.speaker.begin_time; - } - - /** - * @returns an ISO datetime string or null - */ - public get end_time(): string | null { - return this.speaker.end_time; - } - /** * @returns * - waiting if there is no begin nor end time @@ -82,7 +46,10 @@ export class ViewSpeaker extends BaseViewModel { return this.user ? this.user.gender : ''; } - public getTitle = () => { - return this.name; + public getListTitle = () => { + return this.getTitle(); }; } +export interface ViewSpeaker extends Speaker { + user?: ViewUser; +} diff --git a/client/src/app/site/agenda/services/agenda-pdf.service.ts b/client/src/app/site/agenda/services/agenda-pdf.service.ts index d6f54c3e7..1832d0886 100644 --- a/client/src/app/site/agenda/services/agenda-pdf.service.ts +++ b/client/src/app/site/agenda/services/agenda-pdf.service.ts @@ -84,7 +84,7 @@ export class AgendaPdfService { }, { width: 60, - text: nodeItem.item.itemNumber + text: nodeItem.item.item_number }, { text: nodeItem.item.contentObject.getAgendaListTitleWithoutItemNumber() diff --git a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html index 3b905eb72..474041028 100644 --- a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html +++ b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html @@ -36,11 +36,11 @@
- - @@ -241,7 +241,6 @@ matInput placeholder="{{ 'Title' | translate }}" formControlName="title" - [value]="assignmentCopy.title || ''" /> {{ 'The title is required' | translate }} @@ -287,7 +286,6 @@ matInput placeholder="{{ 'Default comment on the ballot paper' | translate }}" formControlName="poll_description_default" - [value]="assignmentCopy.assignment.poll_description_default || ''" />
diff --git a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts index f584de7c9..704f41a5f 100644 --- a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts +++ b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts @@ -112,11 +112,6 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn */ private _assignment: ViewAssignment; - /** - * Copy instance of the assignment that the user might edit - */ - public assignmentCopy: ViewAssignment; - /** * Check if the operator is a candidate * @@ -288,7 +283,6 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn * @param assignment */ private patchForm(assignment: ViewAssignment): void { - this.assignmentCopy = assignment; const contentPatch: { [key: string]: any } = {}; Object.keys(this.assignmentForm.controls).forEach(control => { contentPatch[control] = assignment[control]; @@ -299,11 +293,11 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn /** * Save the current state of the assignment */ - public saveAssignment(): void { + public async saveAssignment(): Promise { if (this.newAssignment) { this.createAssignment(); } else { - this.updateAssignmentFromForm(); + await this.updateAssignmentFromForm(); } } @@ -312,21 +306,21 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn * TODO: directly open poll dialog? */ public async createPoll(): Promise { - await this.repo.addPoll(this.assignment).then(null, this.raiseError); + await this.repo.addPoll(this.assignment).catch(this.raiseError); } /** * Adds the operator to list of candidates */ public async addSelf(): Promise { - await this.repo.addSelf(this.assignment).then(null, this.raiseError); + await this.repo.addSelf(this.assignment).catch(this.raiseError); } /** * Removes the operator from list of candidates */ public async removeSelf(): Promise { - await this.repo.deleteSelf(this.assignment).then(null, this.raiseError); + await this.repo.deleteSelf(this.assignment).catch(this.raiseError); } /** @@ -337,7 +331,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn public async addUser(userId: number): Promise { const user = this.userRepo.getViewModel(userId); if (user) { - await this.repo.changeCandidate(user, this.assignment, true).then(null, this.raiseError); + await this.repo.changeCandidate(user, this.assignment, true).catch(this.raiseError); } } @@ -347,7 +341,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn * @param candidate A ViewAssignmentUser currently in the list of related users */ public async removeUser(candidate: ViewAssignmentRelatedUser): Promise { - await this.repo.changeCandidate(candidate.user, this.assignment, false).then(null, this.raiseError); + await this.repo.changeCandidate(candidate.user, this.assignment, false).catch(this.raiseError); } /** @@ -385,7 +379,6 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn this.editAssignment = true; // TODO set defaults? this.assignment = new ViewAssignment(new Assignment()); - this.assignmentCopy = new ViewAssignment(new Assignment()); } } @@ -414,7 +407,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn * @param value the phase to set */ public async onSetPhaseButton(value: number): Promise { - this.repo.update({ phase: value }, this.assignment).then(null, this.raiseError); + this.repo.update({ phase: value }, this.assignment).catch(this.raiseError); } public onDownloadPdf(): void { @@ -438,10 +431,13 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn } } - public updateAssignmentFromForm(): void { - this.repo - .patch({ ...this.assignmentForm.value }, this.assignmentCopy) - .then(() => (this.editAssignment = false), this.raiseError); + public async updateAssignmentFromForm(): Promise { + try { + await this.repo.patch({ ...this.assignmentForm.value }, this.assignment); + this.editAssignment = false; + } catch (e) { + this.raiseError(e); + } } /** @@ -482,7 +478,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn * (triggered on an autoupdate of either users or the assignment) */ private filterCandidates(): void { - if (!this.assignment || !this.assignment.candidates) { + if (!this.assignment || !this.assignment.candidates.length) { this.filteredCandidates.next(this.availableCandidates.getValue()); } else { this.filteredCandidates.next( @@ -499,14 +495,14 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn public onSortingChange(listInNewOrder: ViewAssignmentRelatedUser[]): void { this.repo .sortCandidates(listInNewOrder.map(relatedUser => relatedUser.id), this.assignment) - .then(null, this.raiseError); + .catch(this.raiseError); } public addToAgenda(): void { - this.itemRepo.addItemToAgenda(this.assignment).then(null, this.raiseError); + this.itemRepo.addItemToAgenda(this.assignment).catch(this.raiseError); } public removeFromAgenda(): void { - this.itemRepo.removeFromAgenda(this.assignment.agendaItem).then(null, this.raiseError); + this.itemRepo.removeFromAgenda(this.assignment.item).catch(this.raiseError); } } diff --git a/client/src/app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component.ts b/client/src/app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component.ts index 84ae0633a..13b281d1c 100644 --- a/client/src/app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component.ts +++ b/client/src/app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component.ts @@ -90,12 +90,10 @@ export class AssignmentPollDialogComponent { */ public submit(): void { const error = this.data.options.find(dataoption => { - for (const key of this.optionPollKeys) { + this.optionPollKeys.some(key => { const keyValue = dataoption.votes.find(o => o.value === key); - if (!keyValue || keyValue.weight === undefined) { - return true; - } - } + return !keyValue || keyValue.weight === undefined; + }); }); if (error) { this.matSnackBar.open( diff --git a/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.html b/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.html index 6ab6403d3..ebf258414 100644 --- a/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.html +++ b/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.html @@ -170,7 +170,7 @@

Candidates

{{ option.user.getFullName() }} - No user {{ option.user_id }} + No user {{ option.candidate_id }}
diff --git a/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts b/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts index bc505ebd6..dffd51c9e 100644 --- a/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts +++ b/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts @@ -161,7 +161,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit public async onDeletePoll(): Promise { const title = this.translate.instant('Are you sure you want to delete this ballot?'); if (await this.promptService.open(title)) { - await this.assignmentRepo.deletePoll(this.poll).then(null, this.raiseError); + await this.assignmentRepo.deletePoll(this.poll).catch(this.raiseError); } } @@ -197,13 +197,12 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit */ public enterVotes(): void { const dialogRef = this.dialog.open(AssignmentPollDialogComponent, { - // TODO deep copy of this.poll (JSON parse is ugly workaround) or sending just copy of the options data: this.poll.copy(), ...mediumDialogSettings }); dialogRef.afterClosed().subscribe(result => { if (result) { - this.assignmentRepo.updateVotes(result, this.poll).then(null, this.raiseError); + this.assignmentRepo.updateVotes(result, this.poll).catch(this.raiseError); } }); } @@ -236,7 +235,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit // TODO additional conditions: assignment not finished? const viewAssignmentRelatedUser = this.assignment.assignment_related_users.find( - user => user.user_id === option.user_id + user => user.user_id === option.candidate_id ); if (viewAssignmentRelatedUser) { this.assignmentRepo.markElected(viewAssignmentRelatedUser, this.assignment, !option.is_elected); @@ -249,7 +248,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit */ public async onEditDescriptionButton(): Promise { const desc: string = this.descriptionForm.get('description').value; - await this.assignmentRepo.updatePoll({ description: desc }, this.poll).then(null, this.raiseError); + await this.assignmentRepo.updatePoll({ description: desc }, this.poll).catch(this.raiseError); } /** diff --git a/client/src/app/site/assignments/models/view-assignment-poll-option.ts b/client/src/app/site/assignments/models/view-assignment-poll-option.ts index 1ff62e51e..76a090974 100644 --- a/client/src/app/site/assignments/models/view-assignment-poll-option.ts +++ b/client/src/app/site/assignments/models/view-assignment-poll-option.ts @@ -12,40 +12,14 @@ export class ViewAssignmentPollOption extends BaseViewModel votesOrder.indexOf(a.value) - votesOrder.indexOf(b.value)); } - - public get poll_id(): number { - return this.option.poll_id; - } - - public get weight(): number { - return this.option.weight; - } +} +export interface ViewAssignmentPollOption extends AssignmentPollOption { + user: ViewUser; } diff --git a/client/src/app/site/assignments/models/view-assignment-poll.ts b/client/src/app/site/assignments/models/view-assignment-poll.ts index ca55e8be8..23f805a74 100644 --- a/client/src/app/site/assignments/models/view-assignment-poll.ts +++ b/client/src/app/site/assignments/models/view-assignment-poll.ts @@ -1,87 +1,17 @@ -import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll'; +import { AssignmentPoll, AssignmentPollWithoutNestedModels } from 'app/shared/models/assignments/assignment-poll'; import { AssignmentPollOption } from 'app/shared/models/assignments/assignment-poll-option'; import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; -import { AssignmentPollMethod } from '../services/assignment-poll.service'; import { ViewAssignmentPollOption } from './view-assignment-poll-option'; export class ViewAssignmentPoll extends BaseProjectableViewModel { public static COLLECTIONSTRING = AssignmentPoll.COLLECTIONSTRING; protected _collectionString = AssignmentPoll.COLLECTIONSTRING; - private _options: ViewAssignmentPollOption[]; - public get poll(): AssignmentPoll { return this._model; } - public get options(): ViewAssignmentPollOption[] { - return this._options; - } - - public get id(): number { - return this.poll.id; - } - - public get pollmethod(): AssignmentPollMethod { - return this.poll.pollmethod; - } - - public get description(): string { - return this.poll.description; - } - - public get published(): boolean { - return this.poll.published; - } - - public get votesno(): number { - return this.poll.votesno; - } - public set votesno(amount: number) { - this.poll.votesno = amount; - } - - public get votesabstain(): number { - return this.poll.votesabstain; - } - public set votesabstain(amount: number) { - this.poll.votesabstain = amount; - } - - public get votesvalid(): number { - return this.poll.votesvalid; - } - public set votesvalid(amount: number) { - this.poll.votesvalid = amount; - } - - public get votesinvalid(): number { - return this.poll.votesinvalid; - } - public set votesinvalid(amount: number) { - this.poll.votesinvalid = amount; - } - - public get votescast(): number { - return this.poll.votescast; - } - public set votescast(amount: number) { - this.poll.votescast = amount; - } - - public get has_votes(): boolean { - return this.poll.has_votes; - } - - public get assignment_id(): number { - return this.poll.assignment_id; - } - - public getTitle = () => { - return 'Poll'; - }; - public getListTitle = () => { return this.getTitle(); }; @@ -90,6 +20,20 @@ export class ViewAssignmentPoll extends BaseProjectableViewModel return this.getTitle(); }; + public getSlide(): ProjectorElementBuildDeskriptor { + return { + getBasicProjectorElement: options => ({ + name: 'assignments/poll', + assignment_id: this.assignment_id, + poll_id: this.id, + getIdentifiers: () => ['name', 'assignment_id', 'poll_id'] + }), + slideOptions: [], + projectionDefaultName: 'assignments', + getDialogTitle: () => 'TODO' + }; + } + /** * Creates a copy with deep-copy on all changing numerical values, * but intact uncopied references to the users @@ -107,18 +51,8 @@ export class ViewAssignmentPoll extends BaseProjectableViewModel }); return poll; } - - public getSlide(): ProjectorElementBuildDeskriptor { - return { - getBasicProjectorElement: options => ({ - name: 'assignments/poll', - assignment_id: this.assignment_id, - poll_id: this.id, - getIdentifiers: () => ['name', 'assignment_id', 'poll_id'] - }), - slideOptions: [], - projectionDefaultName: 'assignments', - getDialogTitle: () => 'TODO' - }; - } +} + +export interface ViewAssignmentPoll extends AssignmentPollWithoutNestedModels { + options: ViewAssignmentPollOption[]; } diff --git a/client/src/app/site/assignments/models/view-assignment-related-user.ts b/client/src/app/site/assignments/models/view-assignment-related-user.ts index 16db71261..5ca0f1b1e 100644 --- a/client/src/app/site/assignments/models/view-assignment-related-user.ts +++ b/client/src/app/site/assignments/models/view-assignment-related-user.ts @@ -6,39 +6,15 @@ export class ViewAssignmentRelatedUser extends BaseViewModel string = this.getTitle; - - public getTitle: () => string = () => { - return this.user ? this.user.getFullName() : ''; + public getListTitle = () => { + return this.getTitle(); }; } + +export interface ViewAssignmentRelatedUser extends AssignmentRelatedUser { + user?: ViewUser; +} diff --git a/client/src/app/site/assignments/models/view-assignment.ts b/client/src/app/site/assignments/models/view-assignment.ts index 540e15552..9214b1cc5 100644 --- a/client/src/app/site/assignments/models/view-assignment.ts +++ b/client/src/app/site/assignments/models/view-assignment.ts @@ -1,5 +1,5 @@ import { SearchRepresentation } from 'app/core/ui-services/search.service'; -import { Assignment } from 'app/shared/models/assignments/assignment'; +import { Assignment, AssignmentWithoutNestedModels } from 'app/shared/models/assignments/assignment'; import { TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item'; import { BaseViewModelWithAgendaItemAndListOfSpeakers } from 'app/site/base/base-view-model-with-agenda-item-and-list-of-speakers'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; @@ -41,60 +41,18 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers public static COLLECTIONSTRING = Assignment.COLLECTIONSTRING; protected _collectionString = Assignment.COLLECTIONSTRING; - private _assignment_related_users?: ViewAssignmentRelatedUser[]; - private _polls?: ViewAssignmentPoll[]; - private _tags?: ViewTag[]; - private _attachments?: ViewMediafile[]; - public get assignment(): Assignment { return this._model; } - public get polls(): ViewAssignmentPoll[] { - return this._polls || []; - } - - public get title(): string { - return this.assignment.title; - } - - public get open_posts(): number { - return this.assignment.open_posts; - } - - public get description(): string { - return this.assignment.description; - } - - public get candidates(): ViewUser[] { - return this.assignment_related_users.map(aru => aru.user).filter(x => !!x); - } - - 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 + * TODO: Fix assignment creation: DO NOT create a ViewUser there... */ - public get phase(): number { - return this.assignment.phase; + public get candidates(): ViewUser[] { + if (!this.assignment_related_users) { + return []; + } + return this.assignment_related_users.map(aru => aru.user).filter(x => !!x); } public get phaseString(): string { @@ -123,7 +81,7 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers * @returns the amount of candidates in the assignment's candidate list */ public get candidateAmount(): number { - return this._assignment_related_users ? this._assignment_related_users.length : 0; + return this.assignment_related_users.length; } public formatForSearch(): SearchRepresentation { @@ -147,3 +105,11 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers }; } } +interface IAssignmentRelations { + assignment_related_users: ViewAssignmentRelatedUser[]; + polls?: ViewAssignmentPoll[]; + tags?: ViewTag[]; + attachments?: ViewMediafile[]; +} + +export interface ViewAssignment extends AssignmentWithoutNestedModels, IAssignmentRelations {} diff --git a/client/src/app/site/base/base-view-model-with-agenda-item-and-list-of-speakers.ts b/client/src/app/site/base/base-view-model-with-agenda-item-and-list-of-speakers.ts index 024296bf0..73579b6c0 100644 --- a/client/src/app/site/base/base-view-model-with-agenda-item-and-list-of-speakers.ts +++ b/client/src/app/site/base/base-view-model-with-agenda-item-and-list-of-speakers.ts @@ -1,13 +1,11 @@ -import { SearchRepresentation } from 'app/core/ui-services/search.service'; +import { applyMixins } from 'app/core/mixins'; import { BaseModelWithAgendaItemAndListOfSpeakers } from 'app/shared/models/base/base-model-with-agenda-item-and-list-of-speakers'; import { BaseProjectableViewModel } from './base-projectable-view-model'; -import { IBaseViewModelWithAgendaItem, isBaseViewModelWithAgendaItem } from './base-view-model-with-agenda-item'; +import { BaseViewModelWithAgendaItem, isBaseViewModelWithAgendaItem } from './base-view-model-with-agenda-item'; import { - IBaseViewModelWithListOfSpeakers, + BaseViewModelWithListOfSpeakers, isBaseViewModelWithListOfSpeakers } from './base-view-model-with-list-of-speakers'; -import { ViewItem } from '../agenda/models/view-item'; -import { ViewListOfSpeakers } from '../agenda/models/view-list-of-speakers'; export function isBaseViewModelWithAgendaItemAndListOfSpeakers( obj: any @@ -15,54 +13,15 @@ export function isBaseViewModelWithAgendaItemAndListOfSpeakers( return !!obj && isBaseViewModelWithAgendaItem(obj) && isBaseViewModelWithListOfSpeakers(obj); } -/** - * Base view class for view models with an agenda item and a list of speakers associated. - */ export abstract class BaseViewModelWithAgendaItemAndListOfSpeakers< M extends BaseModelWithAgendaItemAndListOfSpeakers = any -> extends BaseProjectableViewModel implements IBaseViewModelWithAgendaItem, IBaseViewModelWithListOfSpeakers { - protected _item?: ViewItem; - protected _list_of_speakers?: ViewListOfSpeakers; +> extends BaseProjectableViewModel {} - public get agendaItem(): ViewItem | null { - return this._item; - } +export interface BaseViewModelWithAgendaItemAndListOfSpeakers + extends BaseViewModelWithAgendaItem, + BaseViewModelWithListOfSpeakers {} - public get agenda_item_id(): number { - return this._model.agenda_item_id; - } - - public get agenda_item_number(): string | null { - return this.agendaItem && this.agendaItem.itemNumber ? this.agendaItem.itemNumber : null; - } - - public get listOfSpeakers(): ViewListOfSpeakers | null { - return this._list_of_speakers; - } - - public get list_of_speakers_id(): number { - return this._model.list_of_speakers_id; - } - - public getAgendaSlideTitle: () => string; - public getAgendaListTitle: () => string; - public getAgendaListTitleWithoutItemNumber: () => string; - public getAgendaSubtitle: () => string | null; - public getListOfSpeakersTitle: () => string; - public getListOfSpeakersSlideTitle: () => string; - - /** - * @returns the (optional) descriptive text to be exported in the CSV. - * May be overridden by inheriting classes - */ - public getCSVExportText(): string { - return ''; - } - - public abstract getDetailStateURL(): string; - - /** - * Should return a string representation of the object, so there can be searched for. - */ - public abstract formatForSearch(): SearchRepresentation; -} +applyMixins(BaseViewModelWithAgendaItemAndListOfSpeakers, [ + BaseViewModelWithAgendaItem, + BaseViewModelWithListOfSpeakers +]); diff --git a/client/src/app/site/base/base-view-model-with-agenda-item.ts b/client/src/app/site/base/base-view-model-with-agenda-item.ts index e9e438463..cbe027892 100644 --- a/client/src/app/site/base/base-view-model-with-agenda-item.ts +++ b/client/src/app/site/base/base-view-model-with-agenda-item.ts @@ -13,9 +13,11 @@ export function isBaseViewModelWithAgendaItem(obj: any): obj is BaseViewModelWit isSearchable(model) && model.getAgendaSlideTitle !== undefined && model.getAgendaListTitle !== undefined && + model.getAgendaSubtitle !== undefined && model.getCSVExportText !== undefined && - model.agendaItem !== undefined && - model.agenda_item_id !== undefined + model.item !== undefined && + model.getModel !== undefined && + model.getModel().agenda_item_id !== undefined ); } @@ -26,15 +28,11 @@ export interface TitleInformationWithAgendaItem extends TitleInformation { /** * Describes a base class for view models. */ -export interface IBaseViewModelWithAgendaItem +export interface BaseViewModelWithAgendaItem extends BaseProjectableViewModel, DetailNavigable, Searchable { - agendaItem: any | null; - - agenda_item_id: number; - - agenda_item_number: string | null; + item: any | null; /** * @returns the agenda title @@ -46,21 +44,10 @@ export interface IBaseViewModelWithAgendaItem string; - /** - * @return an optional subtitle for the agenda. - */ - getAgendaSubtitle: () => string | null; - /** * @return the agenda title with the verbose name of the content object */ getAgendaListTitleWithoutItemNumber: () => string; - - /** - * @returns the (optional) descriptive text to be exported in the CSV. - * May be overridden by inheriting classes - */ - getCSVExportText(): string; } /** @@ -68,41 +55,11 @@ export interface IBaseViewModelWithAgendaItem - extends BaseProjectableViewModel - implements IBaseViewModelWithAgendaItem { - protected _item?: any; - - public get agendaItem(): any | null { - return this._item; - } - - public get agenda_item_id(): number { - return this._model.agenda_item_id; - } - +export abstract class BaseViewModelWithAgendaItem< + M extends BaseModelWithAgendaItem = any +> extends BaseProjectableViewModel { public get agenda_item_number(): string | null { - return this.agendaItem && this.agendaItem.itemNumber ? this.agendaItem.itemNumber : null; - } - - /** - * @returns the agenda title for the item slides - */ - public getAgendaSlideTitle: () => string; - - /** - * @return the agenda title for the list view - */ - public getAgendaListTitle: () => string; - - /** - * @return the agenda title without any item number. - */ - public getAgendaListTitleWithoutItemNumber: () => string; - - public constructor(model: M, item?: any) { - super(model); - this._item = item || null; // Explicit set to null instead of undefined, if not given + return this.item && this.item.item_number ? this.item.item_number : null; } /** diff --git a/client/src/app/site/base/base-view-model-with-content-object.ts b/client/src/app/site/base/base-view-model-with-content-object.ts index a56677eeb..fa06d9f4e 100644 --- a/client/src/app/site/base/base-view-model-with-content-object.ts +++ b/client/src/app/site/base/base-view-model-with-content-object.ts @@ -13,13 +13,14 @@ export abstract class BaseViewModelWithContentObject< M extends BaseModelWithContentObject = any, C extends BaseViewModel = any > extends BaseViewModel { - protected _contentObject?: C; - public get contentObjectData(): ContentObject { return this.getModel().content_object; } - - public get contentObject(): C | null { - return this._contentObject; - } +} + +export interface BaseViewModelWithContentObject< + M extends BaseModelWithContentObject = any, + C extends BaseViewModel = any +> { + contentObject: C | null; } diff --git a/client/src/app/site/base/base-view-model-with-list-of-speakers.ts b/client/src/app/site/base/base-view-model-with-list-of-speakers.ts index 2d005f10b..906a5b59c 100644 --- a/client/src/app/site/base/base-view-model-with-list-of-speakers.ts +++ b/client/src/app/site/base/base-view-model-with-list-of-speakers.ts @@ -9,50 +9,19 @@ export function isBaseViewModelWithListOfSpeakers(obj: any): obj is BaseViewMode isDetailNavigable(model) && model.getListOfSpeakersTitle !== undefined && model.listOfSpeakers !== undefined && - model.list_of_speakers_id !== undefined + model.getModel !== undefined && + model.getModel().list_of_speakers_id !== undefined ); } -/** - * Describes a base view model with a list of speakers. - */ -export interface IBaseViewModelWithListOfSpeakers - extends BaseProjectableViewModel, - DetailNavigable { +export interface BaseViewModelWithListOfSpeakers extends DetailNavigable { listOfSpeakers: any | null; - - list_of_speakers_id: number; - getListOfSpeakersTitle: () => string; - getListOfSpeakersSlideTitle: () => string; } -/** - * Base view model class for models with a list of speakers. - * - * TODO: Resolve circular dependencies with `ViewListOfSpeakers` to avoid `any`. - */ -export abstract class BaseViewModelWithListOfSpeakers - extends BaseProjectableViewModel - implements IBaseViewModelWithListOfSpeakers { - protected _list_of_speakers?: any; - - public get listOfSpeakers(): any | null { - return this._list_of_speakers; - } - - public get list_of_speakers_id(): number { - return this._model.list_of_speakers_id; - } - - public getListOfSpeakersTitle: () => string; - public getListOfSpeakersSlideTitle: () => string; - - public constructor(model: M, listOfSpeakers?: any) { - super(model); - this._list_of_speakers = listOfSpeakers || null; // Explicit set to null instead of undefined, if not given - } - +export abstract class BaseViewModelWithListOfSpeakers< + M extends BaseModelWithListOfSpeakers = any +> extends BaseProjectableViewModel { public abstract getDetailStateURL(): string; } diff --git a/client/src/app/site/base/base-view-model.ts b/client/src/app/site/base/base-view-model.ts index 53ddf92de..3315127d9 100644 --- a/client/src/app/site/base/base-view-model.ts +++ b/client/src/app/site/base/base-view-model.ts @@ -11,47 +11,16 @@ export interface ViewModelConstructor { } /** - * Base class for view models. alls view models should have titles. + * Base class for view models. */ -export abstract class BaseViewModel implements Displayable, Identifiable, Collection { - public get id(): number { - return this._model.id; - } - - /** - * force children of BaseModel to have a collectionString. - * - * Has a getter but no setter. - */ - protected _collectionString: string; - - /** - * returns the collectionString. - * - * The server and the dataStore use it to identify the collection. - */ - public get collectionString(): string { - return this._collectionString; - } - +export abstract class BaseViewModel { /** * @returns the element id of the model */ public get elementId(): string { - return `${this.collectionString}:${this.id}`; + return this._model.elementId; } - public getTitle: () => string; - public getListTitle: () => string; - - /** - * Returns the verbose name. - * - * @param plural If the name should be plural - * @returns the verbose name of the model - */ - public getVerboseName: (plural?: boolean) => string; - /** * @param collectionString * @param model @@ -64,8 +33,25 @@ export abstract class BaseViewModel implements Displa public getModel(): M { return this._model; } - public toString(): string { return this.getTitle(); } + public toJSON(): M { + return this.getModel(); + } + public getUpdatedModel(update: Partial): M { + return this.getModel().getUpdatedVersion(update); + } +} +export interface BaseViewModel extends Displayable, Identifiable, Collection { + getTitle: () => string; + getListTitle: () => string; + + /** + * Returns the verbose name. + * + * @param plural If the name should be plural + * @returns the verbose name of the model + */ + getVerboseName: (plural?: boolean) => string; } diff --git a/client/src/app/site/common/components/super-search/super-search.component.html b/client/src/app/site/common/components/super-search/super-search.component.html index 50f491d4e..b5faa0187 100644 --- a/client/src/app/site/common/components/super-search/super-search.component.html +++ b/client/src/app/site/common/components/super-search/super-search.component.html @@ -98,7 +98,7 @@
-
+
diff --git a/client/src/app/site/common/components/super-search/super-search.component.ts b/client/src/app/site/common/components/super-search/super-search.component.ts index 666569cb0..cc2d92766 100644 --- a/client/src/app/site/common/components/super-search/super-search.component.ts +++ b/client/src/app/site/common/components/super-search/super-search.component.ts @@ -376,7 +376,7 @@ export class SuperSearchComponent implements OnInit { event.preventDefault(); event.stopPropagation(); } - if (!!this.selectedModel) { + if (this.selectedModel) { if (event.key === 'Enter') { this.viewResult(this.selectedModel); } diff --git a/client/src/app/site/mediafiles/models/view-mediafile.ts b/client/src/app/site/mediafiles/models/view-mediafile.ts index e4c873771..f86dd4a54 100644 --- a/client/src/app/site/mediafiles/models/view-mediafile.ts +++ b/client/src/app/site/mediafiles/models/view-mediafile.ts @@ -1,5 +1,5 @@ import { SearchRepresentation } from 'app/core/ui-services/search.service'; -import { Mediafile } from 'app/shared/models/mediafiles/mediafile'; +import { Mediafile, MediafileWithoutNestedModels } from 'app/shared/models/mediafiles/mediafile'; import { BaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { Searchable } from 'app/site/base/searchable'; @@ -18,72 +18,12 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers public static COLLECTIONSTRING = Mediafile.COLLECTIONSTRING; protected _collectionString = Mediafile.COLLECTIONSTRING; - private _parent?: ViewMediafile; - private _access_groups?: ViewGroup[]; - private _inherited_access_groups?: ViewGroup[]; - public get mediafile(): Mediafile { return this._model; } - public get parent(): ViewMediafile | null { - return this._parent; - } - - public get access_groups(): ViewGroup[] { - return this._access_groups || []; - } - - public get access_groups_id(): number[] { - return this.mediafile.access_groups_id; - } - - public get inherited_access_groups(): ViewGroup[] | null { - return this._inherited_access_groups; - } - - public get inherited_access_groups_id(): boolean | number[] { - return this.mediafile.inherited_access_groups_id; - } - - public get has_inherited_access_groups(): boolean { - return this.mediafile.has_inherited_access_groups; - } - - public get title(): string { - return this.filename; - } - public get filename(): string { - return this.mediafile.title; - } - - public get path(): string { - return this.mediafile.path; - } - - public get parent_id(): number { - return this.mediafile.parent_id; - } - - public get is_directory(): boolean { - return this.mediafile.is_directory; - } - - public get is_file(): boolean { - return !this.is_directory; - } - - public get size(): string | null { - return this.mediafile.filesize; - } - - public get prefix(): string { - return this.mediafile.media_url_prefix; - } - - public get url(): string { - return this.mediafile.url; + return this.title; } public get type(): string | null { @@ -105,7 +45,7 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers { key: 'Path', value: this.path }, { key: 'Type', value: type }, { key: 'Timestamp', value: this.timestamp }, - { key: 'Size', value: this.size ? this.size : '0' } + { key: 'Size', value: this.filesize ? this.filesize : '0' } ]; return { properties, @@ -114,6 +54,10 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers }; } + public get url(): string { + return this.mediafile.url; + } + public getDetailStateURL(): string { return this.is_directory ? ('/mediafiles/files/' + this.path).slice(0, -1) : this.url; } @@ -205,3 +149,9 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers } } } +interface IMediafileRelations { + parent?: ViewMediafile; + access_groups?: ViewGroup[]; + inherited_access_groups?: ViewGroup[]; +} +export interface ViewMediafile extends MediafileWithoutNestedModels, IMediafileRelations {} diff --git a/client/src/app/site/mediafiles/services/mediafiles-sort-list.service.ts b/client/src/app/site/mediafiles/services/mediafiles-sort-list.service.ts index c116352df..4df9258ee 100644 --- a/client/src/app/site/mediafiles/services/mediafiles-sort-list.service.ts +++ b/client/src/app/site/mediafiles/services/mediafiles-sort-list.service.ts @@ -27,7 +27,7 @@ export class MediafilesSortListService extends BaseSortListService implements CategoryTit public static COLLECTIONSTRING = Category.COLLECTIONSTRING; protected _collectionString = Category.COLLECTIONSTRING; - private _parent?: ViewCategory; - private _children?: ViewCategory[]; - private _motions?: ViewMotion[]; - public get category(): Category { return this._model; } - public get parent(): ViewCategory | null { - return this._parent; - } - public get oldestParent(): ViewCategory { if (!this.parent_id) { return this; @@ -40,34 +32,6 @@ export class ViewCategory extends BaseViewModel implements CategoryTit } } - public get children(): ViewCategory[] { - return this._children || []; - } - - public get motions(): ViewMotion[] { - return this._motions || []; - } - - public get name(): string { - return this.category.name; - } - - public get prefix(): string { - return this.category.prefix; - } - - public get weight(): number { - return this.category.weight; - } - - public get parent_id(): number { - return this.category.parent_id; - } - - public get level(): number { - return this.category.level; - } - public get prefixedName(): string { return this.prefix ? this.prefix + ' - ' + this.name : this.name; } @@ -132,3 +96,9 @@ export class ViewCategory extends BaseViewModel implements CategoryTit } } } +interface ICategoryRelations { + parent?: ViewCategory; + children?: ViewCategory[]; + motions?: ViewMotion[]; +} +export interface ViewCategory extends Category, ICategoryRelations {} diff --git a/client/src/app/site/motions/models/view-create-motion.ts b/client/src/app/site/motions/models/view-create-motion.ts index aff2118fb..c6a94b231 100644 --- a/client/src/app/site/motions/models/view-create-motion.ts +++ b/client/src/app/site/motions/models/view-create-motion.ts @@ -33,11 +33,4 @@ export class ViewCreateMotion extends ViewMotion { public getVerboseName = () => { throw new Error('This should not be used'); }; - - /** - * Duplicate this motion into a copy of itself - */ - public copy(): ViewCreateMotion { - return new ViewCreateMotion(this._model); - } } diff --git a/client/src/app/site/motions/models/view-motion-block.ts b/client/src/app/site/motions/models/view-motion-block.ts index 3e5e15571..0e0ba32ca 100644 --- a/client/src/app/site/motions/models/view-motion-block.ts +++ b/client/src/app/site/motions/models/view-motion-block.ts @@ -19,22 +19,9 @@ export class ViewMotionBlock extends BaseViewModelWithAgendaItemAndListOfSpeaker public static COLLECTIONSTRING = MotionBlock.COLLECTIONSTRING; protected _collectionString = MotionBlock.COLLECTIONSTRING; - private _motions?: ViewMotion[]; - public get motionBlock(): MotionBlock { return this._model; } - public get motions(): ViewMotion[] { - return this._motions || []; - } - - public get title(): string { - return this.motionBlock.title; - } - - public get internal(): boolean { - return this.motionBlock.internal; - } /** * Formats the category for search @@ -67,3 +54,8 @@ export class ViewMotionBlock extends BaseViewModelWithAgendaItemAndListOfSpeaker }; } } + +interface IMotionBLockRelations { + motions?: ViewMotion[]; +} +export interface ViewMotionBlock extends MotionBlock, IMotionBLockRelations {} diff --git a/client/src/app/site/motions/models/view-motion-comment-section.ts b/client/src/app/site/motions/models/view-motion-comment-section.ts index dfde766b5..7feae495f 100644 --- a/client/src/app/site/motions/models/view-motion-comment-section.ts +++ b/client/src/app/site/motions/models/view-motion-comment-section.ts @@ -18,69 +18,13 @@ export class ViewMotionCommentSection extends BaseViewModel group.id === update.id); - if (groupIndex < 0) { - this.read_groups.push(update); - } else { - this.read_groups[groupIndex] = update; - } - } else if (this.section.write_groups_id.includes(update.id)) { - const groupIndex = this.write_groups.findIndex(group => group.id === update.id); - if (groupIndex < 0) { - this.write_groups.push(update); - } else { - this.write_groups[groupIndex] = update; - } - } - } - } } + +interface IMotionCommentSectionRelations { + read_groups: ViewGroup[]; + write_groups: ViewGroup[]; +} +export interface ViewMotionCommentSection extends MotionCommentSection, IMotionCommentSectionRelations {} diff --git a/client/src/app/site/motions/models/view-motion.ts b/client/src/app/site/motions/models/view-motion.ts index f5a75fa5f..67adc0743 100644 --- a/client/src/app/site/motions/models/view-motion.ts +++ b/client/src/app/site/motions/models/view-motion.ts @@ -2,7 +2,7 @@ import { _ } from 'app/core/translate/translation-marker'; import { ConfigService } from 'app/core/ui-services/config.service'; import { DiffLinesInParagraph } from 'app/core/ui-services/diff.service'; import { SearchProperty, SearchRepresentation } from 'app/core/ui-services/search.service'; -import { Motion, MotionComment } from 'app/shared/models/motions/motion'; +import { Motion, MotionComment, MotionWithoutNestedModels } from 'app/shared/models/motions/motion'; import { PersonalNoteContent } from 'app/shared/models/users/personal-note'; import { TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item'; import { BaseViewModelWithAgendaItemAndListOfSpeakers } from 'app/site/base/base-view-model-with-agenda-item-and-list-of-speakers'; @@ -71,176 +71,26 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers submitter.user); - } - - public get supporters(): ViewUser[] { - return this._supporters || []; - } - - /** - * TODO: Where is this needed. Try to avoid this.. - */ - public set supporters(users: ViewUser[]) { - this._supporters = users; - this._model.supporters_id = users.map(user => user.id); - } - - public get motion_block(): ViewMotionBlock | null { - return this._motion_block; - } - - public get attachments(): ViewMediafile[] { - return this._attachments || []; - } - - public get tags(): ViewTag[] { - return this._tags || []; - } - - public get parent(): ViewMotion | null { - return this._parent; - } - - public get amendments(): ViewMotion[] { - return this._amendments || []; - } - - public get identifier(): string { - return this.motion.identifier; - } - - public get title(): string { - return this.motion.title; + return (this.submitters || []).map(submitter => submitter.user); } public get identifierOrTitle(): string { return this.identifier ? this.identifier : this.title; } - public get text(): string { - return this.motion.text; - } - - public get reason(): string { - return this.motion.reason; - } - - public get modified_final_version(): string { - return this.motion.modified_final_version; - } - - public set modified_final_version(value: string) { - if (this.motion) { - this.motion.modified_final_version = value; - } - } - - public get weight(): number { - return this.motion.weight; - } - - public get sort_parent_id(): number { - return this.motion.sort_parent_id; - } - - public get category_id(): number { - return this.motion.category_id; - } - - public get category_weight(): number { - return this.motion.category_weight; - } - - public get sorted_submitters_id(): number[] { - return this.motion.sorted_submitters_id; - } - - public get supporters_id(): number[] { - return this.motion.supporters_id; - } - - public get workflow(): ViewWorkflow { - return this._workflow; - } - - public get workflow_id(): number { - return this.motion.workflow_id; - } - - public get changeRecommendations(): ViewMotionChangeRecommendation[] { - return this._changeRecommendations; - } - - public get change_recommendations_id(): number[] { - return this.motion.change_recommendations_id; - } - - public get state_id(): number { - return this.motion.state_id; - } - - public get recommendation_id(): number { - return this.motion.recommendation_id; - } - - public get statute_paragraph_id(): number { - return this.motion.statute_paragraph_id; - } - 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 agenda_type(): number | null { + return this.item ? this.item.type : null; } - public get agenda_type(): number { - return this.agendaItem ? this.agendaItem.type : null; - } - - public get motion_block_id(): number { - return this.motion.motion_block_id; - } - - public get speakerAmount(): number { + public get speakerAmount(): number | null { return this.listOfSpeakers ? this.listOfSpeakers.waitingSpeakerAmount : null; } - public get parent_id(): number { - return this.motion.parent_id; - } - - public get amendment_paragraphs(): string[] { - return this.motion.amendment_paragraphs ? this.motion.amendment_paragraphs : []; - } - - public get tags_id(): number[] { - return this.motion.tags_id; - } - - public get attachments_id(): number[] { - return this.motion.attachments_id; - } - /** * @returns the creation date as Date object */ @@ -331,20 +181,6 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers { if (diffLine.diffLineTo === diffLine.diffLineFrom + 1) { @@ -521,7 +330,7 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers 0; + return this.amendment_paragraphs && this.amendment_paragraphs.length > 0; } public getSlide(configService: ConfigService): ProjectorElementBuildDeskriptor { @@ -554,11 +363,22 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers implements StateTitleInforma public static COLLECTIONSTRING = State.COLLECTIONSTRING; protected _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); - }); + return ( + !this.next_states_id || + !this.next_states_id.length || + (this.next_states_id.length === 1 && this.next_states_id[0] === 0) + ); } } + +interface IStateRelations { + next_states?: ViewState[]; + previous_states?: ViewState[]; + workflow?: ViewWorkflow; +} +export interface ViewState extends State, IStateRelations {} diff --git a/client/src/app/site/motions/models/view-statute-paragraph.ts b/client/src/app/site/motions/models/view-statute-paragraph.ts index 42fe87a96..bee447b7b 100644 --- a/client/src/app/site/motions/models/view-statute-paragraph.ts +++ b/client/src/app/site/motions/models/view-statute-paragraph.ts @@ -23,18 +23,6 @@ export class ViewStatuteParagraph extends BaseViewModel return this._model; } - public get title(): string { - return this.statuteParagraph.title; - } - - public get text(): string { - return this.statuteParagraph.text; - } - - public get weight(): number { - return this.statuteParagraph.weight; - } - public formatForSearch(): SearchRepresentation { return { properties: [{ key: 'Title', value: this.getTitle() }], searchValue: [this.getTitle()] }; } @@ -43,3 +31,4 @@ export class ViewStatuteParagraph extends BaseViewModel return '/motions/statute-paragraphs'; } } +export interface ViewStatuteParagraph extends StatuteParagraph {} diff --git a/client/src/app/site/motions/models/view-submitter.ts b/client/src/app/site/motions/models/view-submitter.ts index b2b121355..907881f66 100644 --- a/client/src/app/site/motions/models/view-submitter.ts +++ b/client/src/app/site/motions/models/view-submitter.ts @@ -6,37 +6,15 @@ export class ViewSubmitter extends BaseViewModel { public static COLLECTIONSTRING = Submitter.COLLECTIONSTRING; protected _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 getTitle = () => { - return this.user ? this.user.getTitle() : ''; - }; - public getListTitle = () => { return this.getTitle(); }; } +interface ISubmitterRelations { + user: ViewUser; +} +export interface ViewSubmitter extends Submitter, ISubmitterRelations {} diff --git a/client/src/app/site/motions/models/view-workflow.ts b/client/src/app/site/motions/models/view-workflow.ts index 930d4269d..5d4075d90 100644 --- a/client/src/app/site/motions/models/view-workflow.ts +++ b/client/src/app/site/motions/models/view-workflow.ts @@ -14,30 +14,12 @@ export class ViewWorkflow extends BaseViewModel implements WorkflowTit public static COLLECTIONSTRING = Workflow.COLLECTIONSTRING; protected _collectionString = Workflow.COLLECTIONSTRING; - private _states?: ViewState[]; - private _first_state?: ViewState; - public get workflow(): Workflow { return this._model; } - - public get name(): string { - return this.workflow.name; - } - - 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 first_state(): ViewState | null { - return this._first_state; - } } +interface IWorkflowRelations { + states?: ViewState[]; + first_state?: ViewState; +} +export interface ViewWorkflow extends Workflow, IWorkflowRelations {} diff --git a/client/src/app/site/motions/modules/amendment-list/amendment-list.component.ts b/client/src/app/site/motions/modules/amendment-list/amendment-list.component.ts index 16b390510..4aed14b20 100644 --- a/client/src/app/site/motions/modules/amendment-list/amendment-list.component.ts +++ b/client/src/app/site/motions/modules/amendment-list/amendment-list.component.ts @@ -110,7 +110,7 @@ export class AmendmentListComponent extends BaseListViewComponent im public ngOnInit(): void { // determine if a paramter exists. - if (!!this.route.snapshot.paramMap.get('id')) { + if (this.route.snapshot.paramMap.get('id')) { // set the parentMotion observable. This will "only" fire // if there is a subscription to the parent motion this.parentMotion = this.route.paramMap.pipe( @@ -133,7 +133,7 @@ export class AmendmentListComponent extends BaseListViewComponent im */ public getAmendmentSummary(amendment: ViewMotion): string { const diffLines = amendment.diffLines; - if (!!diffLines) { + if (diffLines) { return diffLines .map(diffLine => { return this.linenumberingService.stripLineNumbers(diffLine.text); diff --git a/client/src/app/site/motions/modules/category/components/category-detail/category-detail.component.ts b/client/src/app/site/motions/modules/category/components/category-detail/category-detail.component.ts index 40113d076..2e65218ef 100644 --- a/client/src/app/site/motions/modules/category/components/category-detail/category-detail.component.ts +++ b/client/src/app/site/motions/modules/category/components/category-detail/category-detail.component.ts @@ -220,7 +220,7 @@ export class CategoryDetailComponent extends BaseViewComponent implements OnInit const title = this.translate.instant('Are you sure you want to renumber all motions of this category?'); const content = this.selectedCategory.getTitle(); if (await this.promptService.open(title, content)) { - await this.repo.numberMotionsInCategory(this.selectedCategory).then(null, this.raiseError); + await this.repo.numberMotionsInCategory(this.selectedCategory).catch(this.raiseError); } } } diff --git a/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.html b/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.html index 0d4958415..7f193ff28 100644 --- a/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.html +++ b/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.html @@ -118,11 +118,11 @@
- - diff --git a/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.ts b/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.ts index 1d39cf1d3..f7fe97182 100644 --- a/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.ts +++ b/client/src/app/site/motions/modules/motion-block/components/motion-block-detail/motion-block-detail.component.ts @@ -254,10 +254,10 @@ export class MotionBlockDetailComponent extends BaseListViewComponent
- - @@ -614,7 +614,6 @@ autofocus placeholder="{{ 'Identifier' | translate }}" formControlName="identifier" - [value]="motionCopy.identifier || ''" />
@@ -626,7 +625,6 @@ matInput placeholder="{{ 'Title' | translate }}" formControlName="title" - [value]="motionCopy.title" required /> {{ 'The title is required' | translate }} @@ -844,7 +842,6 @@ matInput placeholder="{{ 'Origin' | translate }}" formControlName="origin" - [value]="motionCopy.origin" />
diff --git a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts index 90fea684d..344b66f56 100644 --- a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts +++ b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts @@ -203,11 +203,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, */ public amendmentsEnabled: boolean; - /** - * Copy of the motion that the user might edit - */ - public motionCopy: ViewMotion; - /** * All change recommendations to this motion */ @@ -645,12 +640,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, // new motion this.newMotion = true; this.editMotion = true; - // prevent 'undefined' to appear in the ui - const defaultMotion: Partial = { - title: '', - origin: '', - identifier: '' - }; + const defaultMotion: Partial = {}; if (this.route.snapshot.queryParams.parent) { this.amendmentEdit = true; const parentMotion = this.repo.getViewModel(this.route.snapshot.queryParams.parent); @@ -676,7 +666,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, } } this.motion = new ViewCreateMotion(new CreateMotion(defaultMotion)); - this.motionCopy = new ViewCreateMotion(new CreateMotion(defaultMotion)); } } @@ -836,7 +825,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, */ private updateMotionFromForm(): void { const newMotionValues = { ...this.contentForm.value }; - this.updateMotion(newMotionValues, this.motionCopy).then(() => { + this.updateMotion(newMotionValues, this.motion).then(() => { this.editMotion = false; this.amendmentEdit = false; }, this.raiseError); @@ -1155,8 +1144,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, public setEditMode(mode: boolean): void { this.editMotion = mode; if (mode) { - this.motionCopy = this.motion.copy(); - this.patchForm(this.motionCopy); + this.patchForm(this.motion); this.editNotificationSubscription = this.listenToEditNotification(); this.sendEditNotification(MotionEditNotificationType.TYPE_BEGIN_EDITING_MOTION); } @@ -1247,14 +1235,14 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, * Supports the motion (as requested user) */ public support(): void { - this.repo.support(this.motion).then(null, this.raiseError); + this.repo.support(this.motion).catch(this.raiseError); } /** * Unsupports the motion */ public unsupport(): void { - this.repo.unsupport(this.motion).then(null, this.raiseError); + this.repo.unsupport(this.motion).catch(this.raiseError); } /** @@ -1271,7 +1259,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, * @param id Motion state id */ public setState(id: number): void { - this.repo.setState(this.motion, id).then(null, this.raiseError); + this.repo.setState(this.motion, id).catch(this.raiseError); } /** @@ -1489,7 +1477,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, * @param unsubscriptionReason The reason for the unsubscription. */ private unsubscribeEditNotifications(unsubscriptionReason: MotionEditNotificationType): void { - if (!!this.editNotificationSubscription && !this.editNotificationSubscription.closed) { + if (this.editNotificationSubscription && !this.editNotificationSubscription.closed) { this.sendEditNotification(unsubscriptionReason); this.closeSnackBar(); this.editNotificationSubscription.unsubscribe(); @@ -1529,7 +1517,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, } else { this.motion.personalNote.star = !this.motion.personalNote.star; } - this.personalNoteService.savePersonalNote(this.motion, this.motion.personalNote).then(null, this.raiseError); + this.personalNoteService.savePersonalNote(this.motion, this.motion.personalNote).catch(this.raiseError); } /** @@ -1616,10 +1604,10 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, } public addToAgenda(): void { - this.itemRepo.addItemToAgenda(this.motion).then(null, this.raiseError); + this.itemRepo.addItemToAgenda(this.motion).catch(this.raiseError); } public removeFromAgenda(): void { - this.itemRepo.removeFromAgenda(this.motion.agendaItem).then(null, this.raiseError); + this.itemRepo.removeFromAgenda(this.motion.item).catch(this.raiseError); } } diff --git a/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.ts b/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.ts index c6817484a..d818a6fe7 100644 --- a/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.ts +++ b/client/src/app/site/motions/modules/motion-list/components/motion-list/motion-list.component.ts @@ -289,7 +289,7 @@ export class MotionListComponent extends BaseListViewComponent imple const localCategories = new Set(); for (const motion of motions) { - if (!motion.category_id) { + if (!motion.category) { motionsWithoutCategory++; } else { localCategories.add(motion.category.oldestParent); @@ -429,7 +429,7 @@ export class MotionListComponent extends BaseListViewComponent imple }; // TODO: "only update if different" was another repo-todo if (!Object.keys(partialUpdate).every(key => partialUpdate[key] === undefined)) { - await this.motionRepo.update(partialUpdate, motion).then(null, this.raiseError); + await this.motionRepo.update(partialUpdate, motion).catch(this.raiseError); } } }); diff --git a/client/src/app/site/motions/modules/shared-motion/motion-export-dialog/motion-export-dialog.component.ts b/client/src/app/site/motions/modules/shared-motion/motion-export-dialog/motion-export-dialog.component.ts index 6d868b3de..a5185ddd0 100644 --- a/client/src/app/site/motions/modules/shared-motion/motion-export-dialog/motion-export-dialog.component.ts +++ b/client/src/app/site/motions/modules/shared-motion/motion-export-dialog/motion-export-dialog.component.ts @@ -177,7 +177,7 @@ export class MotionExportDialogComponent implements OnInit { * @param nextState The next state the button will assume. */ private changeStateOfButton(button: MatButtonToggle, nextState: boolean): void { - if (!!button) { + if (button) { button.disabled = nextState; } } @@ -245,7 +245,7 @@ export class MotionExportDialogComponent implements OnInit { // restore selection or set default this.store.get('motion_export_selection').then(restored => { - if (!!restored) { + if (restored) { this.exportForm.patchValue(restored); } else { this.exportForm.patchValue(this.defaults); diff --git a/client/src/app/site/motions/services/amendment-filter-list.service.ts b/client/src/app/site/motions/services/amendment-filter-list.service.ts index 99c8949cf..a527861c5 100644 --- a/client/src/app/site/motions/services/amendment-filter-list.service.ts +++ b/client/src/app/site/motions/services/amendment-filter-list.service.ts @@ -90,7 +90,7 @@ export class AmendmentFilterListService extends MotionFilterListService { * Function to define a new storage key by parent id */ private updateStorageKey(): void { - if (!!this._parentMotionId) { + if (this._parentMotionId) { this.storageKey = `${this.keyPrefix}_parentId_${this._parentMotionId}`; } else { this.storageKey = this.keyPrefix; @@ -104,7 +104,7 @@ export class AmendmentFilterListService extends MotionFilterListService { */ protected preFilter(motions: ViewMotion[]): ViewMotion[] { return motions.filter(motion => { - if (!!this._parentMotionId) { + if (this._parentMotionId) { return motion.parent_id === this._parentMotionId; } else { return !!motion.parent_id; diff --git a/client/src/app/site/motions/services/motion-export.service.ts b/client/src/app/site/motions/services/motion-export.service.ts index e249459c7..126c75063 100644 --- a/client/src/app/site/motions/services/motion-export.service.ts +++ b/client/src/app/site/motions/services/motion-export.service.ts @@ -46,7 +46,7 @@ export class MotionExportService { if (!exportInfo) { return; } - if (!!exportInfo.format) { + if (exportInfo.format) { if (exportInfo.format === ExportFileFormat.PDF) { try { this.pdfExport.exportMotionCatalog(data, exportInfo); diff --git a/client/src/app/site/motions/services/motion-multiselect.service.ts b/client/src/app/site/motions/services/motion-multiselect.service.ts index af054cfa8..997f1be28 100644 --- a/client/src/app/site/motions/services/motion-multiselect.service.ts +++ b/client/src/app/site/motions/services/motion-multiselect.service.ts @@ -201,7 +201,7 @@ export class MotionMultiselectService { let requestData = null; if (selectedChoice.action === choices[0]) { requestData = motions.map(motion => { - let submitterIds = [...motion.sorted_submitters_id, ...(selectedChoice.items as number[])]; + let submitterIds = [...motion.sorted_submitter_ids, ...(selectedChoice.items as number[])]; submitterIds = submitterIds.filter((id, index, self) => self.indexOf(id) === index); // remove duplicates return { id: motion.id, @@ -211,7 +211,7 @@ export class MotionMultiselectService { } else if (selectedChoice.action === choices[1]) { requestData = motions.map(motion => { const submitterIdsToRemove = selectedChoice.items as number[]; - const submitterIds = motion.sorted_submitters_id.filter(id => !submitterIdsToRemove.includes(id)); + const submitterIds = motion.sorted_submitter_ids.filter(id => !submitterIdsToRemove.includes(id)); return { id: motion.id, submitters: submitterIds diff --git a/client/src/app/site/motions/services/motion-pdf.service.ts b/client/src/app/site/motions/services/motion-pdf.service.ts index e37a0106c..f84dfe5cd 100644 --- a/client/src/app/site/motions/services/motion-pdf.service.ts +++ b/client/src/app/site/motions/services/motion-pdf.service.ts @@ -312,7 +312,7 @@ export class MotionPdfService { // category if (motion.category && (!infoToExport || infoToExport.includes('category'))) { let categoryText = ''; - if (!!motion.category.parent) { + if (motion.category.parent) { categoryText = `${motion.category.parent.toString()}\n${this.translate.instant( 'Subcategory' )}: ${motion.category.toString()}`; diff --git a/client/src/app/site/motions/services/motion-xlsx-export.service.ts b/client/src/app/site/motions/services/motion-xlsx-export.service.ts index 32e5150bb..100096203 100644 --- a/client/src/app/site/motions/services/motion-xlsx-export.service.ts +++ b/client/src/app/site/motions/services/motion-xlsx-export.service.ts @@ -147,11 +147,11 @@ export class MotionXlsxExportService { data.push( ...properties.map(property => { const motionProp = motion[property]; - if (property === 'speakers') { + /*if (property === 'speakers') { return motion.listOfSpeakers && motion.listOfSpeakers.waitingSpeakerAmount > 0 ? motion.listOfSpeakers.waitingSpeakerAmount : ''; - } + }*/ if (!motionProp) { return ''; } diff --git a/client/src/app/site/projector/components/presentation-control/presentation-control.component.ts b/client/src/app/site/projector/components/presentation-control/presentation-control.component.ts index 53476140e..22ba0de2b 100644 --- a/client/src/app/site/projector/components/presentation-control/presentation-control.component.ts +++ b/client/src/app/site/projector/components/presentation-control/presentation-control.component.ts @@ -139,6 +139,6 @@ export class PresentationControlComponent extends BaseViewComponent { private updateElement(element: MediafileProjectorElement): void { const idElement = this.slideManager.getIdentifialbeProjectorElement(element); - this.projectorService.updateElement(this.projector.projector, idElement).then(null, this.raiseError); + this.projectorService.updateElement(this.projector.projector, idElement).catch(this.raiseError); } } diff --git a/client/src/app/site/projector/components/projector-detail/projector-detail.component.ts b/client/src/app/site/projector/components/projector-detail/projector-detail.component.ts index a7b82cfb3..b67595f6e 100644 --- a/client/src/app/site/projector/components/projector-detail/projector-detail.component.ts +++ b/client/src/app/site/projector/components/projector-detail/projector-detail.component.ts @@ -125,7 +125,7 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni * @param step (optional) The amount of steps to make. */ public scroll(direction: ScrollScaleDirection, step: number = 1): void { - this.repo.scroll(this.projector, direction, step).then(null, this.raiseError); + this.repo.scroll(this.projector, direction, step).catch(this.raiseError); } /** @@ -135,29 +135,29 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni * @param step (optional) The amount of steps to make. */ public scale(direction: ScrollScaleDirection, step: number = 1): void { - this.repo.scale(this.projector, direction, step).then(null, this.raiseError); + this.repo.scale(this.projector, direction, step).catch(this.raiseError); } public projectNextSlide(): void { - this.projectorService.projectNextSlide(this.projector.projector).then(null, this.raiseError); + this.projectorService.projectNextSlide(this.projector.projector).catch(this.raiseError); } public projectPreviousSlide(): void { - this.projectorService.projectPreviousSlide(this.projector.projector).then(null, this.raiseError); + this.projectorService.projectPreviousSlide(this.projector.projector).catch(this.raiseError); } public onSortingChange(event: CdkDragDrop): void { moveItemInArray(this.projector.elements_preview, event.previousIndex, event.currentIndex); - this.projectorService.savePreview(this.projector.projector).then(null, this.raiseError); + this.projectorService.savePreview(this.projector.projector).catch(this.raiseError); } public removePreviewElement(elementIndex: number): void { this.projector.elements_preview.splice(elementIndex, 1); - this.projectorService.savePreview(this.projector.projector).then(null, this.raiseError); + this.projectorService.savePreview(this.projector.projector).catch(this.raiseError); } public projectNow(elementIndex: number): void { - this.projectorService.projectPreviewSlide(this.projector.projector, elementIndex).then(null, this.raiseError); + this.projectorService.projectPreviewSlide(this.projector.projector, elementIndex).catch(this.raiseError); } public getSlideTitle(element: ProjectorElement): string { @@ -182,7 +182,7 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni public unprojectCurrent(element: ProjectorElement): void { const idElement = this.slideManager.getIdentifialbeProjectorElement(element); - this.projectorService.removeFrom(this.projector.projector, idElement).then(null, this.raiseError); + this.projectorService.removeFrom(this.projector.projector, idElement).catch(this.raiseError); } public isClosProjected(stable: boolean): boolean { diff --git a/client/src/app/site/projector/components/projector-list-entry/projector-list-entry.component.ts b/client/src/app/site/projector/components/projector-list-entry/projector-list-entry.component.ts index 2611e93c6..860644cc0 100644 --- a/client/src/app/site/projector/components/projector-list-entry/projector-list-entry.component.ts +++ b/client/src/app/site/projector/components/projector-list-entry/projector-list-entry.component.ts @@ -244,7 +244,7 @@ export class ProjectorListEntryComponent extends BaseViewComponent implements On public async onDeleteButton(): Promise { const title = this.translate.instant('Are you sure you want to delete this projector?'); if (await this.promptService.open(title, this.projector.name)) { - this.repo.delete(this.projector).then(null, this.raiseError); + this.repo.delete(this.projector).catch(this.raiseError); } } @@ -262,7 +262,7 @@ export class ProjectorListEntryComponent extends BaseViewComponent implements On width: width }; updateProjector.height = Math.round(width / aspectRatios[aspectRatioKey]); - this.repo.update(updateProjector, this.projector).then(null, this.raiseError); + this.repo.update(updateProjector, this.projector).catch(this.raiseError); } /** diff --git a/client/src/app/site/projector/components/projector-list/projector-list.component.html b/client/src/app/site/projector/components/projector-list/projector-list.component.html index 3c92c882e..1452791cc 100644 --- a/client/src/app/site/projector/components/projector-list/projector-list.component.html +++ b/client/src/app/site/projector/components/projector-list/projector-list.component.html @@ -5,7 +5,7 @@
- + Reference projector for current list of speakers:   - + New Projector
@@ -38,7 +38,7 @@ - diff --git a/client/src/app/site/projector/components/projector-list/projector-list.component.ts b/client/src/app/site/projector/components/projector-list/projector-list.component.ts index 39ad4a711..b97d25baa 100644 --- a/client/src/app/site/projector/components/projector-list/projector-list.component.ts +++ b/client/src/app/site/projector/components/projector-list/projector-list.component.ts @@ -35,7 +35,7 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit, /** * This member is set, if the user is creating a new projector. */ - public projectorToCreate: Projector | null; + public showCreateForm = false; /** * The create form. @@ -126,8 +126,8 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit, * Opens the create form. */ public onPlusButton(): void { - if (!this.projectorToCreate) { - this.projectorToCreate = new Projector(); + if (!this.showCreateForm) { + this.showCreateForm = true; this.createForm.setValue({ name: '' }); } } @@ -136,13 +136,13 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit, * Creates the comment section from the create form. */ public create(): void { - if (this.createForm.valid && this.projectorToCreate) { - this.projectorToCreate.patchValues(this.createForm.value as Projector); - this.projectorToCreate.patchValues({ + if (this.createForm.valid && this.showCreateForm) { + const projector: Partial = { + name: this.createForm.value.name, reference_projector_id: this.projectors[0].reference_projector_id - }); - this.repo.create(this.projectorToCreate).then(() => { - this.projectorToCreate = null; + }; + this.repo.create(projector).then(() => { + this.showCreateForm = false; this.cd.detectChanges(); }, this.raiseError); } @@ -158,7 +158,7 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit, this.create(); } if (event.key === 'Escape') { - this.projectorToCreate = null; + this.showCreateForm = null; } } @@ -169,6 +169,6 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit, const promises = this.projectors.map(projector => { return this.repo.update(update, projector); }); - Promise.all(promises).then(null, this.raiseError); + Promise.all(promises).catch(this.raiseError); } } diff --git a/client/src/app/site/projector/models/view-countdown.ts b/client/src/app/site/projector/models/view-countdown.ts index 61b08ae6e..1c8bd7472 100644 --- a/client/src/app/site/projector/models/view-countdown.ts +++ b/client/src/app/site/projector/models/view-countdown.ts @@ -16,26 +16,6 @@ export class ViewCountdown extends BaseProjectableViewModel implement return this._model; } - public get running(): boolean { - return this.countdown.running; - } - - public get default_time(): number { - return this.countdown.default_time; - } - - public get countdown_time(): number { - return this.countdown.countdown_time; - } - - public get description(): string { - return this.countdown.description || ''; - } - - public get title(): string { - return this.countdown.title; - } - public getSlide(): ProjectorElementBuildDeskriptor { return { getBasicProjectorElement: options => ({ @@ -66,3 +46,4 @@ export class ViewCountdown extends BaseProjectableViewModel implement }; } } +export interface ViewCountdown extends Countdown {} diff --git a/client/src/app/site/projector/models/view-projection-default.ts b/client/src/app/site/projector/models/view-projection-default.ts index 92a5b2b74..c98cae647 100644 --- a/client/src/app/site/projector/models/view-projection-default.ts +++ b/client/src/app/site/projector/models/view-projection-default.ts @@ -13,16 +13,5 @@ export class ViewProjectionDefault extends BaseViewModel public get projectionDefault(): ProjectionDefault { return this._model; } - - public get id(): number { - return this.projectionDefault.id; - } - - public get name(): string { - return this.projectionDefault.name; - } - - public get display_name(): string { - return this.projectionDefault.display_name; - } } +export interface ViewProjectionDefault extends ProjectionDefault {} diff --git a/client/src/app/site/projector/models/view-projector-message.ts b/client/src/app/site/projector/models/view-projector-message.ts index 18d50d765..8fff9d87b 100644 --- a/client/src/app/site/projector/models/view-projector-message.ts +++ b/client/src/app/site/projector/models/view-projector-message.ts @@ -14,10 +14,6 @@ export class ViewProjectorMessage extends BaseProjectableViewModel ({ @@ -41,3 +37,4 @@ export class ViewProjectorMessage extends BaseProjectableViewModel { public static COLLECTIONSTRING = Projector.COLLECTIONSTRING; protected _collectionString = Projector.COLLECTIONSTRING; - private _referenceProjector: ViewProjector; - public get projector(): Projector { return this._model; } - public get referenceProjector(): ViewProjector { - if (!this.reference_projector_id || this.reference_projector_id === this.id) { - return this; - } else { - return this._referenceProjector; - } - } - - public get name(): string { - return this.projector.name; - } - - public get projectiondefaults_id(): number[] { - return this.projector.projectiondefaults_id; - } - - public get elements(): ProjectorElements { - return this.projector.elements; - } - public get non_stable_elements(): ProjectorElements { return this.projector.elements.filter(element => !element.stable); } - - public get elements_preview(): ProjectorElements { - return this.projector.elements_preview; - } - - public get elements_history(): ProjectorElements[] { - return this.projector.elements_history; - } - - public get height(): number { - return this.projector.height; - } - - public get width(): number { - return this.projector.width; - } - - public get scale(): number { - return this.projector.scale; - } - - public get scroll(): number { - return this.projector.scroll; - } - - public get reference_projector_id(): number { - return this.projector.reference_projector_id; - } - - public get color(): string { - return this.projector.color; - } - - public get background_color(): string { - return this.projector.background_color; - } - - public get header_background_color(): string { - return this.projector.header_background_color; - } - - public get header_font_color(): string { - return this.projector.header_font_color; - } - - public get header_h1_color(): string { - return this.projector.header_h1_color; - } - - public get chyron_background_color(): string { - return this.projector.chyron_background_color; - } - - public get chyron_font_color(): string { - return this.projector.chyron_font_color; - } - - public get show_header_footer(): boolean { - return this.projector.show_header_footer; - } - - public get show_title(): boolean { - return this.projector.show_title; - } - - public get show_logo(): boolean { - return this.projector.show_logo; - } } +interface IProjectorRelations { + referenceProjector: ViewProjector; +} +export interface ViewProjector extends Projector, IProjectorRelations {} diff --git a/client/src/app/site/tags/models/view-tag.ts b/client/src/app/site/tags/models/view-tag.ts index a7b6d21a4..d10081a99 100644 --- a/client/src/app/site/tags/models/view-tag.ts +++ b/client/src/app/site/tags/models/view-tag.ts @@ -22,10 +22,6 @@ export class ViewTag extends BaseViewModel implements TagTitleInformation, return this._model; } - public get name(): string { - return this.tag.name; - } - public formatForSearch(): SearchRepresentation { return { properties: [{ key: 'Name', value: this.name }], searchValue: [this.name] }; } @@ -34,3 +30,4 @@ export class ViewTag extends BaseViewModel implements TagTitleInformation, return `/tags`; } } +export interface ViewTag extends Tag {} diff --git a/client/src/app/site/topics/components/topic-detail/topic-detail.component.ts b/client/src/app/site/topics/components/topic-detail/topic-detail.component.ts index 842a9bab9..0249e7c07 100644 --- a/client/src/app/site/topics/components/topic-detail/topic-detail.component.ts +++ b/client/src/app/site/topics/components/topic-detail/topic-detail.component.ts @@ -203,7 +203,7 @@ export class TopicDetailComponent extends BaseViewComponent { const title = this.translate.instant('Are you sure you want to delete this entry?'); const content = this.topic.title; if (await this.promptService.open(title, content)) { - await this.repo.delete(this.topic).then(null, this.raiseError); + await this.repo.delete(this.topic).catch(this.raiseError); this.router.navigate(['/agenda']); } } diff --git a/client/src/app/site/topics/models/view-topic.ts b/client/src/app/site/topics/models/view-topic.ts index 2f4eeb37b..0bbdb9407 100644 --- a/client/src/app/site/topics/models/view-topic.ts +++ b/client/src/app/site/topics/models/view-topic.ts @@ -14,31 +14,8 @@ export interface TopicTitleInformation extends TitleInformationWithAgendaItem { * Provides "safe" access to topic with all it's components * @ignore */ -export class ViewTopic extends BaseViewModelWithAgendaItemAndListOfSpeakers implements TopicTitleInformation { +export class ViewTopic extends BaseViewModelWithAgendaItemAndListOfSpeakers implements TopicTitleInformation { public static COLLECTIONSTRING = Topic.COLLECTIONSTRING; - protected _collectionString = Topic.COLLECTIONSTRING; - - private _attachments?: ViewMediafile[]; - - public get topic(): Topic { - return this._model; - } - - public get attachments(): ViewMediafile[] { - return this._attachments || []; - } - - public get attachments_id(): number[] { - return this.topic.attachments_id; - } - - public get title(): string { - return this.topic.title; - } - - public get text(): string { - return this.topic.text; - } /** * Formats the category for search @@ -81,3 +58,8 @@ export class ViewTopic extends BaseViewModelWithAgendaItemAndListOfSpeakers impl return this.attachments && this.attachments.length > 0; } } +interface ITopicRelations { + attachments: ViewMediafile[]; +} + +export interface ViewTopic extends Topic, ITopicRelations {} diff --git a/client/src/app/site/users/components/user-detail/user-detail.component.html b/client/src/app/site/users/components/user-detail/user-detail.component.html index 2d59fc89b..826ac0f2e 100644 --- a/client/src/app/site/users/components/user-detail/user-detail.component.html +++ b/client/src/app/site/users/components/user-detail/user-detail.component.html @@ -11,7 +11,8 @@

- New participant {{ user.full_name }} + New participant + {{ user.full_name }}

@@ -33,7 +34,7 @@ Change password - @@ -60,51 +61,42 @@ - +
- + - + - +
- + Please enter a valid email address @@ -120,7 +111,7 @@ - + - {{ gender | translate }} @@ -130,37 +121,34 @@
- + - - +
- - - {{ - group.getTitle() | translate - }} - - +
@@ -171,7 +159,6 @@ autocomplete="off" placeholder="{{ 'Initial password' | translate }}" formControlName="default_password" - [value]="user.default_password || ''" /> Generate