Merge pull request #5062 from FinnStutzenstein/perf

Use Proxies for ViewModels
This commit is contained in:
Sean 2019-10-16 09:22:00 +02:00 committed by GitHub
commit 611c0bce45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
113 changed files with 1152 additions and 2568 deletions

View File

@ -97,9 +97,9 @@ export class AutoupdateService {
elements = elements.concat(this.mapObjectsToBaseModels(collection, autoupdate.changed[collection])); 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); 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); await this.DS.flushToStorage(autoupdate.to_change_id);
this.DSUpdateManager.commit(updateSlot); this.DSUpdateManager.commit(updateSlot, autoupdate.to_change_id);
} else { } else {
// autoupdate fully in the future. we are missing something! // autoupdate fully in the future. we are missing something!
this.requestChanges(); this.requestChanges();
@ -172,7 +172,7 @@ export class AutoupdateService {
const oldChangeId = this.DS.maxChangeId; const oldChangeId = this.DS.maxChangeId;
const response = await this.websocketService.sendAndGetResponse<{}, AutoupdateFormat>('getElements', {}); 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[] = []; let allModels: BaseModel[] = [];
for (const collection of Object.keys(response.changed)) { for (const collection of Object.keys(response.changed)) {
if (this.modelMapper.isCollectionRegistered(collection)) { if (this.modelMapper.isCollectionRegistered(collection)) {
@ -183,7 +183,7 @@ export class AutoupdateService {
} }
await this.DS.set(allModels, response.to_change_id); 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}`); console.log(`Full update done from ${oldChangeId} to ${response.to_change_id}`);
} }

View File

@ -3,9 +3,9 @@ import { EventEmitter, Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model'; import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model';
import { BaseRepository } from '../repositories/base-repository';
import { CollectionStringMapperService } from './collection-string-mapper.service'; import { CollectionStringMapperService } from './collection-string-mapper.service';
import { Deferred } from '../promises/deferred'; import { Deferred } from '../promises/deferred';
import { RelationCacheService } from './relation-cache.service';
import { StorageService } from './storage.service'; import { StorageService } from './storage.service';
/** /**
@ -49,7 +49,7 @@ export class UpdateSlot {
/** /**
* @param DS Carries the DataStore: TODO (see below `DataStoreUpdateManagerService.getNewUpdateSlot`) * @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++; this._id = UpdateSlot.ID_COUTNER++;
} }
@ -169,7 +169,10 @@ export class DataStoreUpdateManagerService {
/** /**
* @param mapperService * @param mapperService
*/ */
public constructor(private mapperService: CollectionStringMapperService) {} public constructor(
private mapperService: CollectionStringMapperService,
private relationCacheService: RelationCacheService
) {}
/** /**
* Retrieve the current update slot. * 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 * @param DS The DataStore. This is a hack, becuase we cannot use the DataStore
* here, because these are cyclic dependencies... --> TODO * here, because these are cyclic dependencies... --> TODO
*/ */
public async getNewUpdateSlot(DS: DataStoreService, initialLoading: boolean = false): Promise<UpdateSlot> { public async getNewUpdateSlot(DS: DataStoreService): Promise<UpdateSlot> {
if (this.currentUpdateSlot) { if (this.currentUpdateSlot) {
const request = new Deferred(); const request = new Deferred();
this.updateSlotRequests.push(request); this.updateSlotRequests.push(request);
await request; await request;
} }
this.currentUpdateSlot = new UpdateSlot(DS, initialLoading); this.currentUpdateSlot = new UpdateSlot(DS);
return this.currentUpdateSlot; return this.currentUpdateSlot;
} }
@ -204,7 +207,7 @@ export class DataStoreUpdateManagerService {
* *
* @param slot The slot to commit * @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)) { if (!this.currentUpdateSlot || !this.currentUpdateSlot.equal(slot)) {
throw new Error('No or wrong update slot to be finished!'); throw new Error('No or wrong update slot to be finished!');
} }
@ -212,36 +215,26 @@ export class DataStoreUpdateManagerService {
// notify repositories in two phases // notify repositories in two phases
const repositories = this.mapperService.getAllRepositories(); 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 if (resetCache) {
const affectedRepos: { [collection: string]: BaseRepository<any, any, any> } = {}; this.relationCacheService.reset();
}
// Phase 1: deleting and creating of view models (in this order) // Phase 1: deleting and creating of view models (in this order)
repositories.forEach(repo => { repositories.forEach(repo => {
const deletedModelIds = slot.getDeletedModelIdsForCollection(repo.collectionString); const deletedModelIds = slot.getDeletedModelIdsForCollection(repo.collectionString);
repo.deleteModels(deletedModelIds); repo.deleteModels(deletedModelIds);
this.relationCacheService.registerDeletedModels(repo.collectionString, deletedModelIds);
const changedModelIds = slot.getChangedModelIdsForCollection(repo.collectionString); const changedModelIds = slot.getChangedModelIdsForCollection(repo.collectionString);
repo.changedModels(changedModelIds, slot.initialLoading); repo.changedModels(changedModelIds);
this.relationCacheService.registerChangedModels(repo.collectionString, changedModelIds, changeId);
if (deletedModelIds.length || changedModelIds.length) {
affectedRepos[repo.collectionString] = repo;
}
}); });
// Phase 2: updating dependencies (deleting ad changing in this order) // Phase 2: updating all repositories
repositories.forEach(repo => { repositories.forEach(repo => {
if (repo.updateDependenciesForDeletedModels(slot.getDeletedModels())) { repo.commitUpdate();
affectedRepos[repo.collectionString] = repo;
}
if (repo.updateDependenciesForChangedModels(slot.getChangedModels())) {
affectedRepos[repo.collectionString] = repo;
}
}); });
// 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(); slot.DS.triggerModifiedObservable();
// serve next slot request // serve next slot request
@ -350,7 +343,7 @@ export class DataStoreService {
// This promise will be resolved with cached datastore. // This promise will be resolved with cached datastore.
const store = await this.storageService.get<JsonStorage>(DataStoreService.cachePrefix + 'DS'); const store = await this.storageService.get<JsonStorage>(DataStoreService.cachePrefix + 'DS');
if (store) { if (store) {
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this, true); const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this);
// There is a store. Deserialize it // There is a store. Deserialize it
this.jsonStore = store; this.jsonStore = store;
@ -370,7 +363,7 @@ export class DataStoreService {
}); });
}); });
this.DSUpdateManager.commit(updateSlot); this.DSUpdateManager.commit(updateSlot, maxChangeId, true);
} else { } else {
await this.clear(); await this.clear();
} }

View File

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

View File

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

View File

@ -2,74 +2,35 @@ import { Injectable } from '@angular/core';
import { BaseModel } 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'; 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 { import {
BaseOrderedRelation,
isCustomRelationDefinition, isCustomRelationDefinition,
isGenericRelationDefinition, isGenericRelationDefinition,
isNestedRelationDefinition,
isNormalRelationDefinition, isNormalRelationDefinition,
isReverseRelationDefinition, isReverseRelationDefinition,
RelationDefinition, RelationDefinition
ReverseRelationDefinition
} from '../definitions/relations'; } from '../definitions/relations';
import { ViewModelStoreService } from './view-model-store.service'; import { ViewModelStoreService } from './view-model-store.service';
/** /**
* Manages relations between view models. This service is and should only used by the * 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({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class RelationManagerService { export class RelationManagerService {
public constructor(private viewModelStoreService: ViewModelStoreService) {} public constructor(
private viewModelStoreService: ViewModelStoreService,
private relationCacheService: RelationCacheService
) {}
/** public handleRelation<M extends BaseModel, V extends BaseViewModel>(
* Sorts the array of foreign view models in the given view models for the given relation.
*/
public sortByRelation<V extends BaseViewModel, VForegin extends BaseViewModel>(
relation: BaseOrderedRelation<VForegin>,
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<M extends BaseModel, V extends BaseViewModel>(
model: M,
modelCtor: ViewModelConstructor<V>,
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<M extends BaseModel, V extends BaseViewModel>(
model: M, model: M,
viewModel: V, viewModel: V,
relation: RelationDefinition, relation: RelationDefinition
initialLoading: boolean ): any {
): void {
if (isNormalRelationDefinition(relation)) { if (isNormalRelationDefinition(relation)) {
if ( if (
(relation.type === 'M2M' || relation.type === 'O2M') && (relation.type === 'M2M' || relation.type === 'O2M') &&
@ -80,22 +41,16 @@ export class RelationManagerService {
relation.foreignViewModel, relation.foreignViewModel,
model[relation.ownIdKey] model[relation.ownIdKey]
); );
viewModel['_' + relation.ownKey] = foreignViewModels; this.sortViewModels(foreignViewModels, relation.order);
this.sortByRelation(relation, viewModel); return foreignViewModels;
if (relation.afterSetRelation) {
relation.afterSetRelation(viewModel, foreignViewModels);
}
} else if (relation.type === 'M2O') { } else if (relation.type === 'M2O') {
const foreignViewModel = this.viewModelStoreService.get( const foreignViewModel = this.viewModelStoreService.get(
relation.foreignViewModel, relation.foreignViewModel,
model[relation.ownIdKey] model[relation.ownIdKey]
); );
viewModel['_' + relation.ownKey] = foreignViewModel; return foreignViewModel;
if (relation.afterSetRelation) {
relation.afterSetRelation(viewModel, foreignViewModel);
} }
} } else if (isReverseRelationDefinition(relation)) {
} else if (isReverseRelationDefinition(relation) && !initialLoading) {
if (relation.type === 'M2M') { if (relation.type === 'M2M') {
const foreignViewModels = this.viewModelStoreService.filter( const foreignViewModels = this.viewModelStoreService.filter(
relation.foreignViewModel, relation.foreignViewModel,
@ -104,16 +59,16 @@ export class RelationManagerService {
foreignViewModel[relation.foreignIdKey].constructor === Array && foreignViewModel[relation.foreignIdKey].constructor === Array &&
foreignViewModel[relation.foreignIdKey].includes(model.id) foreignViewModel[relation.foreignIdKey].includes(model.id)
); );
viewModel['_' + relation.ownKey] = foreignViewModels; this.sortViewModels(foreignViewModels, relation.order);
this.sortByRelation(relation, viewModel); return foreignViewModels;
} else if (relation.type === 'O2M') { } else if (relation.type === 'O2M') {
const foreignViewModels = this.viewModelStoreService.filter( const foreignViewModels = this.viewModelStoreService.filter(
relation.foreignViewModel, relation.foreignViewModel,
foreignViewModel => foreignViewModel =>
foreignViewModel[relation.foreignIdKey] && foreignViewModel[relation.foreignIdKey] === model.id foreignViewModel[relation.foreignIdKey] && foreignViewModel[relation.foreignIdKey] === model.id
); );
viewModel['_' + relation.ownKey] = foreignViewModels; this.sortViewModels(foreignViewModels, relation.order);
this.sortByRelation(relation, viewModel); return foreignViewModels;
} else if (relation.type === 'M2O') { } else if (relation.type === 'M2O') {
const foreignViewModel = this.viewModelStoreService.find( const foreignViewModel = this.viewModelStoreService.find(
relation.foreignViewModel, relation.foreignViewModel,
@ -121,236 +76,156 @@ export class RelationManagerService {
_foreignViewModel[relation.foreignIdKey] && _foreignViewModel[relation.foreignIdKey] &&
_foreignViewModel[relation.foreignIdKey] === model.id _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)) { } else if (isGenericRelationDefinition(relation)) {
const contentObject = this.viewModelStoreService.get<BaseViewModel>( const contentObject = this.viewModelStoreService.get<BaseViewModel>(
model[relation.ownContentObjectDataKey].collection, model[relation.ownContentObjectDataKey].collection,
model[relation.ownContentObjectDataKey].id model[relation.ownContentObjectDataKey].id
); );
if (contentObject && relation.isVForeign(contentObject)) { if (contentObject && relation.isVForeign(contentObject)) {
viewModel['_' + relation.ownKey] = contentObject; return contentObject;
} }
} else if (isCustomRelationDefinition(relation)) { } else if (isCustomRelationDefinition(relation)) {
relation.setRelations(model, viewModel); return relation.get(model, viewModel);
} }
} }
public handleCachedRelation<V extends BaseViewModel>(
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];
}
if (!isCustomRelationDefinition(relation)) {
if (cached) {
cached = this.relationCacheService.checkCacheValidity(changeIds);
}
if (!cached) {
result = this.handleRelation(model, viewModel, relation) as BaseViewModel | BaseViewModel[];
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))
);
} else {
newChangeIds[result.elementId] = this.relationCacheService.query(result.elementId);
}
target[cachePropertyChangeIds] = newChangeIds;
} else {
delete target[cacheProperty];
}
}
} else {
// 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;
}
/** /**
* Updates an own view model with an deleted model implicit given by the deletedId and * Sorts the array of foreign view models in the given view models for the given relation.
* the collection via the relation.
*
* @return true, if something was updated.
*/ */
public updateSingleDependencyForDeletedModel( public sortViewModels(viewModels: BaseViewModel[], order?: string): void {
ownViewModel: BaseViewModel, viewModels.sort((a: BaseViewModel, b: BaseViewModel) => {
relation: ReverseRelationDefinition, if (!order || a[order] === b[order]) {
deletedId: number return a.id - b.id;
): 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 = <any>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;
}
}
// 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;
}
}
return false;
}
/**
* 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 = <any>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] === <any>changedId) {
// Check, if this is the matching foreign view model.
const foreignViewModel = this.viewModelStoreService.get(collection, changedId);
ownViewModel['_' + relation.ownKey] = <any>foreignViewModel;
if (relation.afterDependencyChange) {
relation.afterDependencyChange(ownViewModel, foreignViewModel);
}
return true;
}
}
} else if (isReverseRelationDefinition(relation)) {
const foreignViewModel = <any>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 { } else {
const ownViewModelArray = <any>ownViewModel['_' + relation.ownKey]; return a[order] - b[order];
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;
}
}
}
}
// 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;
} else {
const ownViewModelArray = <any>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;
}
}
}
}
// 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;
} }
}); });
}
public createViewModel<M extends BaseModel, V extends BaseViewModel>(
model: M,
viewModelCtor: ViewModelConstructor<V>,
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(<any>property, target, _model, viewModel, relation);
}
return result;
}
}); });
return updated;
} else if (isCustomRelationDefinition(relation)) {
const foreignViewModel = <any>this.viewModelStoreService.get(collection, changedId);
return relation.updateDependency(ownViewModel, foreignViewModel);
} else if (isGenericRelationDefinition(relation)) {
const foreignModel = <any>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; // set nested models
} (nestedModelDescriptors[model.collectionString] || []).forEach(
(modelDescriptor: ModelDescriptor<BaseModel, BaseViewModel>) => {
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);
private setForeingViewModelInOwnViewModelArray( viewModel[modelDescriptor.ownKey] = nestedViewModels;
foreignViewModel: BaseViewModel,
ownViewModel: BaseViewModel,
ownKey: string
): void {
let ownViewModelArray = <any>ownViewModel['_' + ownKey];
if (!ownViewModelArray) {
ownViewModel['_' + ownKey] = [];
ownViewModelArray = <any>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;
} }
);
return viewModel;
} }
} }

View File

@ -53,7 +53,7 @@ export class TimeTravelService {
* @param history the desired point in the history of OpenSlides * @param history the desired point in the history of OpenSlides
*/ */
public async loadHistoryPoint(history: History): Promise<void> { public async loadHistoryPoint(history: History): Promise<void> {
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS, true); const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS);
await this.stopTime(history); await this.stopTime(history);
const historyData: HistoryData = await this.getHistoryData(history); const historyData: HistoryData = await this.getHistoryData(history);
@ -67,7 +67,7 @@ export class TimeTravelService {
}); });
await this.DS.set(allModels, 0); await this.DS.set(allModels, 0);
this.DSUpdateManager.commit(updateSlot); this.DSUpdateManager.commit(updateSlot, 1, true);
} }
/** /**

View File

@ -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'; import { BaseViewModel, ViewModelConstructor } from 'app/site/base/base-view-model';
// All "standard" relations. // All "standard" relations.
export type RelationDefinition<VForeign extends BaseViewModel = BaseViewModel> = export type RelationDefinition<VForeign extends BaseViewModel = BaseViewModel> =
| NormalRelationDefinition<VForeign> | NormalRelationDefinition<VForeign>
| ReverseRelationDefinition<VForeign> | ReverseRelationDefinition<VForeign>
| NestedRelationDefinition<VForeign> | CustomRelationDefinition
| CustomRelationDefinition<VForeign>
| GenericRelationDefinition<VForeign>; | GenericRelationDefinition<VForeign>;
interface BaseRelationDefinition<VForeign extends BaseViewModel> { interface BaseRelationDefinition<VForeign extends BaseViewModel> {
@ -39,8 +38,6 @@ interface BaseNormalRelationDefinition<VForeign extends BaseViewModel> extends B
* the model and view model. E.g. `category_id` in a motion. * the model and view model. E.g. `category_id` in a motion.
*/ */
ownIdKey: string; ownIdKey: string;
afterDependencyChange?: (ownViewModel: BaseViewModel, foreignViewModel: BaseViewModel) => void;
} }
/** /**
@ -58,19 +55,16 @@ interface NormalM2MRelationDefinition<VForeign extends BaseViewModel>
extends BaseNormalRelationDefinition<VForeign>, extends BaseNormalRelationDefinition<VForeign>,
BaseOrderedRelation<VForeign> { BaseOrderedRelation<VForeign> {
type: 'M2M'; type: 'M2M';
afterSetRelation?: (ownViewModel: BaseViewModel, foreignViewModels: BaseViewModel[]) => void;
} }
interface NormalO2MRelationDefinition<VForeign extends BaseViewModel> interface NormalO2MRelationDefinition<VForeign extends BaseViewModel>
extends BaseNormalRelationDefinition<VForeign>, extends BaseNormalRelationDefinition<VForeign>,
BaseOrderedRelation<VForeign> { BaseOrderedRelation<VForeign> {
type: 'O2M'; type: 'O2M';
afterSetRelation?: (ownViewModel: BaseViewModel, foreignViewModels: BaseViewModel[]) => void;
} }
interface NormalM2ORelationDefinition<VForeign extends BaseViewModel> extends BaseNormalRelationDefinition<VForeign> { interface NormalM2ORelationDefinition<VForeign extends BaseViewModel> extends BaseNormalRelationDefinition<VForeign> {
type: 'M2O'; type: 'M2O';
afterSetRelation?: (ownViewModel: BaseViewModel, foreignViewModel: BaseViewModel | null) => void;
} }
export type NormalRelationDefinition<VForeign extends BaseViewModel = BaseViewModel> = export type NormalRelationDefinition<VForeign extends BaseViewModel = BaseViewModel> =
@ -131,29 +125,6 @@ export function isReverseRelationDefinition(obj: RelationDefinition): obj is Rev
return (relation.type === 'M2O' || relation.type === 'O2M' || relation.type === 'M2M') && !!relation.foreignIdKey; 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<VForeign extends BaseViewModel> extends BaseOrderedRelation<VForeign> {
type: 'nested';
ownKey: string;
/**
* The nested relations.
*/
relationDefinition?: RelationDefinition[];
/**
* The matching model for the foreignViewModel.
*/
foreignModel: ModelConstructor<BaseModel>;
}
export function isNestedRelationDefinition(obj: RelationDefinition): obj is NestedRelationDefinition<BaseViewModel> {
return obj.type === 'nested';
}
interface GenericRelationDefinition<VForeign extends BaseViewModel = BaseViewModel> { interface GenericRelationDefinition<VForeign extends BaseViewModel = BaseViewModel> {
type: 'generic'; type: 'generic';
@ -180,21 +151,19 @@ export function isGenericRelationDefinition(obj: RelationDefinition): obj is Gen
/** /**
* A custom relation with callbacks with things todo. * A custom relation with callbacks with things todo.
*/ */
interface CustomRelationDefinition<VForeign extends BaseViewModel> { interface CustomRelationDefinition {
type: 'custom'; type: 'custom';
foreignViewModel: ViewModelConstructor<VForeign>;
/** /**
* 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;
/** get: (model: BaseModel, viewModel: BaseViewModel) => any;
* Called, when the dependency was updated.
*/ getCacheObjectToCheck: (viewModel: BaseViewModel) => BaseViewModel | null;
updateDependency: (ownViewModel: BaseViewModel, foreignViewModel: VForeign) => boolean;
} }
export function isCustomRelationDefinition(obj: RelationDefinition): obj is CustomRelationDefinition<BaseViewModel> { export function isCustomRelationDefinition(obj: RelationDefinition): obj is CustomRelationDefinition {
return obj.type === 'custom'; return obj.type === 'custom';
} }

View File

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

View File

@ -507,7 +507,7 @@ export class PdfDocumentService {
* Cancel the pdf generation * Cancel the pdf generation
*/ */
private cancelPdfCreation(): void { private cancelPdfCreation(): void {
if (!!this.pdfWorker) { if (this.pdfWorker) {
this.pdfWorker.terminate(); this.pdfWorker.terminate();
this.pdfWorker = null; this.pdfWorker = null;
} }

View File

@ -35,7 +35,7 @@ function applyLayout(content: any): void {
if (Array.isArray(section)) { if (Array.isArray(section)) {
applyLayout(section); applyLayout(section);
} else { } else {
if (!!section.layout) { if (section.layout) {
let layout: object; let layout: object;
switch (section.layout) { switch (section.layout) {
case 'switchColorTableLayout': { case 'switchColorTableLayout': {
@ -48,7 +48,7 @@ function applyLayout(content: any): void {
} }
} }
if (!!layout) { if (layout) {
section.layout = layout; section.layout = layout;
} }
} }
@ -94,7 +94,7 @@ addEventListener('message', ({ data }) => {
applyLayout(data.doc.content); applyLayout(data.doc.content);
if (!!data.doc.tmpfooter) { if (data.doc.tmpfooter) {
addPageNumbers(data); addPageNumbers(data);
} }

View File

@ -15,7 +15,6 @@ import { ItemTitleInformation, ViewItem } from 'app/site/agenda/models/view-item
import { ViewAssignment } from 'app/site/assignments/models/view-assignment'; import { ViewAssignment } from 'app/site/assignments/models/view-assignment';
import { import {
BaseViewModelWithAgendaItem, BaseViewModelWithAgendaItem,
IBaseViewModelWithAgendaItem,
isBaseViewModelWithAgendaItem isBaseViewModelWithAgendaItem
} from 'app/site/base/base-view-model-with-agenda-item'; } from 'app/site/base/base-view-model-with-agenda-item';
import { ViewMotion } from 'app/site/motions/models/view-motion'; 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. * @returns {ViewItem} The modified item extended with the `getSubtitle()`-function.
*/ */
protected createViewModelWithTitles(model: Item, initialLoading: boolean): ViewItem { protected createViewModelWithTitles(model: Item): ViewItem {
const viewModel = super.createViewModelWithTitles(model, initialLoading); const viewModel = super.createViewModelWithTitles(model);
viewModel.getSubtitle = () => this.getSubtitle(viewModel); viewModel.getSubtitle = () => this.getSubtitle(viewModel);
return viewModel; return viewModel;
} }
@ -153,17 +152,13 @@ export class ItemRepositoryService extends BaseHasContentObjectRepository<
* @param viewModel the view model that the update is based on * @param viewModel the view model that the update is based on
*/ */
public async update(update: Partial<Item>, viewModel: ViewItem): Promise<void> { public async update(update: Partial<Item>, viewModel: ViewItem): Promise<void> {
const sendUpdate = new this.baseModelCtor(); const sendUpdate = viewModel.getUpdatedModel(update);
sendUpdate.patchValues(viewModel.getModel());
sendUpdate.patchValues(update);
const clone = JSON.parse(JSON.stringify(sendUpdate)); const clone = JSON.parse(JSON.stringify(sendUpdate));
clone.item_number = clone._itemNumber; clone.item_number = clone._itemNumber;
const restPath = `/rest/${sendUpdate.collectionString}/${sendUpdate.id}/`; return await this.dataSend.updateModel(clone);
return await this.httpService.put(restPath, clone);
} }
public async addItemToAgenda(contentObject: IBaseViewModelWithAgendaItem<any>): Promise<Identifiable> { public async addItemToAgenda(contentObject: BaseViewModelWithAgendaItem<any>): Promise<Identifiable> {
return await this.httpService.post('/rest/agenda/item/', { return await this.httpService.post('/rest/agenda/item/', {
collection: contentObject.collectionString, collection: contentObject.collectionString,
id: contentObject.id id: contentObject.id

View File

@ -24,6 +24,7 @@ import { ViewTopic } from 'app/site/topics/models/view-topic';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
import { BaseHasContentObjectRepository } from '../base-has-content-object-repository'; import { BaseHasContentObjectRepository } from '../base-has-content-object-repository';
import { BaseIsListOfSpeakersContentObjectRepository } from '../base-is-list-of-speakers-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 { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { DataStoreService } from '../../core-services/data-store.service'; import { DataStoreService } from '../../core-services/data-store.service';
import { ItemRepositoryService } from './item-repository.service'; import { ItemRepositoryService } from './item-repository.service';
@ -36,23 +37,30 @@ const ListOfSpeakersRelations: RelationDefinition[] = [
VForeignVerbose: 'BaseViewModelWithListOfSpeakers', VForeignVerbose: 'BaseViewModelWithListOfSpeakers',
ownContentObjectDataKey: 'contentObjectData', ownContentObjectDataKey: 'contentObjectData',
ownKey: 'contentObject' ownKey: 'contentObject'
}, }
];
const ListOfSpeakersNestedModelDescriptors: NestedModelDescriptors = {
'agenda/list-of-speakers': [
{ {
type: 'nested',
ownKey: 'speakers', ownKey: 'speakers',
foreignViewModel: ViewSpeaker, foreignViewModel: ViewSpeaker,
foreignModel: Speaker, foreignModel: Speaker,
order: 'weight', order: 'weight',
relationDefinition: [ relationDefinitionsByKey: {
{ user: {
type: 'M2O', type: 'M2O',
ownIdKey: 'user_id', ownIdKey: 'user_id',
ownKey: 'user', ownKey: 'user',
foreignViewModel: ViewUser foreignViewModel: ViewUser
} }
] },
titles: {
getTitle: (viewSpeaker: ViewSpeaker) => viewSpeaker.name
} }
]; }
]
};
/** /**
* Repository service for lists of speakers * Repository service for lists of speakers
@ -96,7 +104,8 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit
translate, translate,
relationManager, relationManager,
ListOfSpeakers, ListOfSpeakers,
ListOfSpeakersRelations ListOfSpeakersRelations,
ListOfSpeakersNestedModelDescriptors
); );
} }
@ -116,7 +125,7 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit
// TODO: This can be resolved with #4738 // TODO: This can be resolved with #4738
const item = this.itemRepo.findByContentObject(titleInformation.contentObjectData); const item = this.itemRepo.findByContentObject(titleInformation.contentObjectData);
if (item) { if (item) {
(<any>titleInformation.title_information).agenda_item_number = item.itemNumber; (<any>titleInformation.title_information).agenda_item_number = item.item_number;
} }
return repo.getListOfSpeakersTitle(titleInformation.title_information); return repo.getListOfSpeakersTitle(titleInformation.title_information);

View File

@ -19,6 +19,7 @@ import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { ViewTag } from 'app/site/tags/models/view-tag'; import { ViewTag } from 'app/site/tags/models/view-tag';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../base-is-agenda-item-and-list-of-speakers-content-object-repository'; import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../base-is-agenda-item-and-list-of-speakers-content-object-repository';
import { NestedModelDescriptors } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service'; import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { DataStoreService } from '../../core-services/data-store.service'; import { DataStoreService } from '../../core-services/data-store.service';
@ -34,46 +35,53 @@ const AssignmentRelations: RelationDefinition[] = [
ownIdKey: 'attachments_id', ownIdKey: 'attachments_id',
ownKey: 'attachments', ownKey: 'attachments',
foreignViewModel: ViewMediafile foreignViewModel: ViewMediafile
}, }
];
const AssignmentNestedModelDescriptors: NestedModelDescriptors = {
'assignments/assignment': [
{ {
type: 'nested',
ownKey: 'assignment_related_users', ownKey: 'assignment_related_users',
foreignViewModel: ViewAssignmentRelatedUser, foreignViewModel: ViewAssignmentRelatedUser,
foreignModel: AssignmentRelatedUser, foreignModel: AssignmentRelatedUser,
order: 'weight', order: 'weight',
relationDefinition: [ relationDefinitionsByKey: {
{ user: {
type: 'M2O', type: 'M2O',
ownIdKey: 'user_id', ownIdKey: 'user_id',
ownKey: 'user', ownKey: 'user',
foreignViewModel: ViewUser foreignViewModel: ViewUser
} }
] },
titles: {
getTitle: (viewAssignmentRelatedUser: ViewAssignmentRelatedUser) =>
viewAssignmentRelatedUser.user ? viewAssignmentRelatedUser.user.getFullName() : ''
}
}, },
{ {
type: 'nested',
ownKey: 'polls', ownKey: 'polls',
foreignViewModel: ViewAssignmentPoll, foreignViewModel: ViewAssignmentPoll,
foreignModel: AssignmentPoll, foreignModel: AssignmentPoll,
relationDefinition: [ relationDefinitionsByKey: {}
}
],
'assignments/assignment-poll': [
{ {
type: 'nested',
ownKey: 'options', ownKey: 'options',
foreignViewModel: ViewAssignmentPollOption, foreignViewModel: ViewAssignmentPollOption,
foreignModel: AssignmentPollOption, foreignModel: AssignmentPollOption,
order: 'weight', order: 'weight',
relationDefinition: [ relationDefinitionsByKey: {
{ user: {
type: 'M2O', type: 'M2O',
ownIdKey: 'user_id', ownIdKey: 'candidate_id',
ownKey: 'user', ownKey: 'user',
foreignViewModel: ViewUser foreignViewModel: ViewUser
} }
] }
} }
] ]
} };
];
/** /**
* Repository Service for Assignments. * Repository Service for Assignments.
@ -122,7 +130,8 @@ export class AssignmentRepositoryService extends BaseIsAgendaItemAndListOfSpeake
translate, translate,
relationManager, relationManager,
Assignment, Assignment,
AssignmentRelations AssignmentRelations,
AssignmentNestedModelDescriptors
); );
} }

View File

@ -40,9 +40,9 @@ export abstract class BaseHasContentObjectRepository<
/** /**
* @override * @override
*/ */
public changedModels(ids: number[], initialLoading: boolean): void { public changedModels(ids: number[]): void {
ids.forEach(id => { 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; this.viewModelStore[id] = v;
const contentObject = v.contentObjectData; const contentObject = v.contentObjectData;

View File

@ -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 { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model'; import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
import { import {
IBaseViewModelWithAgendaItem, BaseViewModelWithAgendaItem,
TitleInformationWithAgendaItem TitleInformationWithAgendaItem
} from 'app/site/base/base-view-model-with-agenda-item'; } 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 { import {
IBaseIsAgendaItemContentObjectRepository, IBaseIsAgendaItemContentObjectRepository,
isBaseIsAgendaItemContentObjectRepository isBaseIsAgendaItemContentObjectRepository
@ -30,14 +30,14 @@ export function isBaseIsAgendaItemAndListOfSpeakersContentObjectRepository(
* multi-inheritance by implementing both inherit classes again... * multi-inheritance by implementing both inherit classes again...
*/ */
export abstract class BaseIsAgendaItemAndListOfSpeakersContentObjectRepository< export abstract class BaseIsAgendaItemAndListOfSpeakersContentObjectRepository<
V extends BaseProjectableViewModel & IBaseViewModelWithAgendaItem & IBaseViewModelWithListOfSpeakers & T, V extends BaseProjectableViewModel & BaseViewModelWithAgendaItem & BaseViewModelWithListOfSpeakers & T,
M extends BaseModel, M extends BaseModel,
T extends TitleInformationWithAgendaItem T extends TitleInformationWithAgendaItem
> extends BaseRepository<V, M, T> > extends BaseRepository<V, M, T>
implements implements
IBaseIsAgendaItemContentObjectRepository<V, M, T>, IBaseIsAgendaItemContentObjectRepository<V, M, T>,
IBaseIsListOfSpeakersContentObjectRepository<V, M, T> { IBaseIsListOfSpeakersContentObjectRepository<V, M, T> {
protected groupRelationsByCollections(): void { protected extendRelations(): void {
this.relationDefinitions.push({ this.relationDefinitions.push({
type: 'M2O', type: 'M2O',
ownIdKey: 'agenda_item_id', ownIdKey: 'agenda_item_id',
@ -47,10 +47,9 @@ export abstract class BaseIsAgendaItemAndListOfSpeakersContentObjectRepository<
this.relationDefinitions.push({ this.relationDefinitions.push({
type: 'M2O', type: 'M2O',
ownIdKey: 'list_of_speakers_id', ownIdKey: 'list_of_speakers_id',
ownKey: 'list_of_speakers', ownKey: 'listOfSpeakers',
foreignViewModel: ViewListOfSpeakers foreignViewModel: ViewListOfSpeakers
}); });
super.groupRelationsByCollections();
} }
public getAgendaListTitle(titleInformation: T): string { public getAgendaListTitle(titleInformation: T): string {
@ -87,8 +86,8 @@ export abstract class BaseIsAgendaItemAndListOfSpeakersContentObjectRepository<
return this.getAgendaSlideTitle(titleInformation); return this.getAgendaSlideTitle(titleInformation);
}; };
protected createViewModelWithTitles(model: M, initialLoading: boolean): V { protected createViewModelWithTitles(model: M): V {
const viewModel = super.createViewModelWithTitles(model, initialLoading); const viewModel = super.createViewModelWithTitles(model);
viewModel.getAgendaListTitle = () => this.getAgendaListTitle(viewModel); viewModel.getAgendaListTitle = () => this.getAgendaListTitle(viewModel);
viewModel.getAgendaListTitleWithoutItemNumber = () => this.getAgendaListTitleWithoutItemNumber(viewModel); viewModel.getAgendaListTitleWithoutItemNumber = () => this.getAgendaListTitleWithoutItemNumber(viewModel);
viewModel.getAgendaSlideTitle = () => this.getAgendaSlideTitle(viewModel); viewModel.getAgendaSlideTitle = () => this.getAgendaSlideTitle(viewModel);

View File

@ -64,14 +64,13 @@ export abstract class BaseIsAgendaItemContentObjectRepository<
); );
} }
protected groupRelationsByCollections(): void { protected extendRelations(): void {
this.relationDefinitions.push({ this.relationDefinitions.push({
type: 'M2O', type: 'M2O',
ownIdKey: 'agenda_item_id', ownIdKey: 'agenda_item_id',
ownKey: 'item', ownKey: 'item',
foreignViewModel: ViewItem foreignViewModel: ViewItem
}); });
super.groupRelationsByCollections();
} }
/** /**
@ -115,8 +114,8 @@ export abstract class BaseIsAgendaItemContentObjectRepository<
/** /**
* Adds the agenda titles to the viewmodel. * Adds the agenda titles to the viewmodel.
*/ */
protected createViewModelWithTitles(model: M, initialLoading: boolean): V { protected createViewModelWithTitles(model: M): V {
const viewModel = super.createViewModelWithTitles(model, initialLoading); const viewModel = super.createViewModelWithTitles(model);
viewModel.getAgendaListTitle = () => this.getAgendaListTitle(viewModel); viewModel.getAgendaListTitle = () => this.getAgendaListTitle(viewModel);
viewModel.getAgendaListTitleWithoutItemNumber = () => this.getAgendaListTitleWithoutItemNumber(viewModel); viewModel.getAgendaListTitleWithoutItemNumber = () => this.getAgendaListTitleWithoutItemNumber(viewModel);
viewModel.getAgendaSlideTitle = () => this.getAgendaSlideTitle(viewModel); viewModel.getAgendaSlideTitle = () => this.getAgendaSlideTitle(viewModel);

View File

@ -61,14 +61,13 @@ export abstract class BaseIsListOfSpeakersContentObjectRepository<
); );
} }
protected groupRelationsByCollections(): void { protected extendRelations(): void {
this.relationDefinitions.push({ this.relationDefinitions.push({
type: 'M2O', type: 'M2O',
ownIdKey: 'list_of_speakers_id', ownIdKey: 'list_of_speakers_id',
ownKey: 'list_of_speakers', ownKey: 'list_of_speakers',
foreignViewModel: ViewListOfSpeakers foreignViewModel: ViewListOfSpeakers
}); });
super.groupRelationsByCollections();
} }
public getListOfSpeakersTitle(titleInformation: T): string { public getListOfSpeakersTitle(titleInformation: T): string {
@ -82,8 +81,8 @@ export abstract class BaseIsListOfSpeakersContentObjectRepository<
/** /**
* Adds the list of speakers titles to the view model * Adds the list of speakers titles to the view model
*/ */
protected createViewModelWithTitles(model: M, initialLoading: boolean): V { protected createViewModelWithTitles(model: M): V {
const viewModel = super.createViewModelWithTitles(model, initialLoading); const viewModel = super.createViewModelWithTitles(model);
viewModel.getListOfSpeakersTitle = () => this.getListOfSpeakersTitle(viewModel); viewModel.getListOfSpeakersTitle = () => this.getListOfSpeakersTitle(viewModel);
viewModel.getListOfSpeakersSlideTitle = () => this.getListOfSpeakersSlideTitle(viewModel); viewModel.getListOfSpeakersSlideTitle = () => this.getListOfSpeakersSlideTitle(viewModel);
return viewModel; return viewModel;

View File

@ -7,18 +7,28 @@ import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model
import { BaseViewModel, TitleInformation, ViewModelConstructor } from '../../site/base/base-view-model'; import { BaseViewModel, TitleInformation, ViewModelConstructor } from '../../site/base/base-view-model';
import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service'; import { CollectionStringMapperService } from '../core-services/collection-string-mapper.service';
import { DataSendService } from '../core-services/data-send.service'; import { DataSendService } from '../core-services/data-send.service';
import { CollectionIds, DataStoreService } from '../core-services/data-store.service'; import { DataStoreService } from '../core-services/data-store.service';
import { Identifiable } from '../../shared/models/base/identifiable'; import { Identifiable } from '../../shared/models/base/identifiable';
import { OnAfterAppsLoaded } from '../definitions/on-after-apps-loaded'; import { OnAfterAppsLoaded } from '../definitions/on-after-apps-loaded';
import { RelationManagerService } from '../core-services/relation-manager.service'; import { RelationManagerService } from '../core-services/relation-manager.service';
import { import { RelationDefinition, ReverseRelationDefinition } from '../definitions/relations';
isNormalRelationDefinition,
isReverseRelationDefinition,
RelationDefinition,
ReverseRelationDefinition
} from '../definitions/relations';
import { ViewModelStoreService } from '../core-services/view-model-store.service'; import { ViewModelStoreService } from '../core-services/view-model-store.service';
export interface ModelDescriptor<M extends BaseModel, V extends BaseViewModel> {
relationDefinitionsByKey: { [key: string]: RelationDefinition };
ownKey: string;
foreignViewModel: ViewModelConstructor<V>;
foreignModel: ModelConstructor<M>;
order?: string;
titles?: {
[key: string]: (viewModel: V) => string;
};
}
export interface NestedModelDescriptors {
[collection: string]: ModelDescriptor<BaseModel, BaseViewModel>[];
}
export abstract class BaseRepository<V extends BaseViewModel & T, M extends BaseModel, T extends TitleInformation> export abstract class BaseRepository<V extends BaseViewModel & T, M extends BaseModel, T extends TitleInformation>
implements OnAfterAppsLoaded, Collection { implements OnAfterAppsLoaded, Collection {
/** /**
@ -89,6 +99,8 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
protected reverseRelationsByCollection: { [collection: string]: ReverseRelationDefinition<BaseViewModel>[] } = {}; protected reverseRelationsByCollection: { [collection: string]: ReverseRelationDefinition<BaseViewModel>[] } = {};
protected relationsByKey: { [key: string]: RelationDefinition<BaseViewModel> } = {};
/** /**
* The view model ctor of the encapsulated view model. * The view model ctor of the encapsulated view model.
*/ */
@ -111,12 +123,16 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
protected translate: TranslateService, protected translate: TranslateService,
protected relationManager: RelationManagerService, protected relationManager: RelationManagerService,
protected baseModelCtor: ModelConstructor<M>, protected baseModelCtor: ModelConstructor<M>,
protected relationDefinitions: RelationDefinition<BaseViewModel>[] = [] protected relationDefinitions: RelationDefinition<BaseViewModel>[] = [],
protected nestedModelDescriptors: NestedModelDescriptors = {}
) { ) {
this._collectionString = baseModelCtor.COLLECTIONSTRING; this._collectionString = baseModelCtor.COLLECTIONSTRING;
this.groupRelationsByCollections(); this.extendRelations();
this.buildReverseRelationsGrouping();
this.relationDefinitions.forEach(relation => {
this.relationsByKey[relation.ownKey] = relation;
});
// All data is piped through an auditTime of 1ms. This is to prevent massive // All data is piped through an auditTime of 1ms. This is to prevent massive
// updates, if e.g. an autoupdate with a lot motions come in. The result is just one // updates, if e.g. an autoupdate with a lot motions come in. The result is just one
@ -130,55 +146,7 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
this.languageCollator = new Intl.Collator(this.translate.currentLang); this.languageCollator = new Intl.Collator(this.translate.currentLang);
} }
/** protected extendRelations(): void {}
* Reorders the relations to provide faster access.
*/
protected groupRelationsByCollections(): void {
this.relationDefinitions.forEach(relation => {
this._groupRelationsByCollections(relation, relation);
});
}
/**
* Recursive function for reorderung the relations.
*/
protected _groupRelationsByCollections(relation: RelationDefinition, baseRelation: RelationDefinition): void {
if (relation.type === 'nested') {
(relation.relationDefinition || []).forEach(nestedRelation => {
this._groupRelationsByCollections(nestedRelation, baseRelation);
});
} else if (
relation.type === '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<BaseViewModel>[];
if (reverseRelations.length) {
this.reverseRelationsByCollection[collection] = reverseRelations;
}
});
}
public onAfterAppsLoaded(): void { public onAfterAppsLoaded(): void {
this.baseViewModelCtor = this.collectionStringMapperService.getViewModelConstructor(this.collectionString); this.baseViewModelCtor = this.collectionStringMapperService.getViewModelConstructor(this.collectionString);
@ -214,12 +182,9 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
* *
* @param ids All model ids. * @param ids All model ids.
*/ */
public changedModels(ids: number[], initialLoading: boolean): void { public changedModels(ids: number[]): void {
ids.forEach(id => { ids.forEach(id => {
this.viewModelStore[id] = this.createViewModelWithTitles( this.viewModelStore[id] = this.createViewModelWithTitles(this.DS.get(this.collectionString, id));
this.DS.get(this.collectionString, id),
initialLoading
);
this.updateViewModelObservable(id); this.updateViewModelObservable(id);
}); });
} }
@ -228,115 +193,20 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
* After creating a view model, all functions for models form the repo * After creating a view model, all functions for models form the repo
* are assigned to the new view model. * are assigned to the new view model.
*/ */
protected createViewModelWithTitles(model: M, initialLoading: boolean): V { protected createViewModelWithTitles(model: M): V {
const viewModel = this.relationManager.createViewModel( const viewModel = this.relationManager.createViewModel(
model, model,
this.baseViewModelCtor, this.baseViewModelCtor,
this.relationDefinitions, this.relationsByKey,
initialLoading this.nestedModelDescriptors
); );
viewModel.getTitle = () => this.getTitle(viewModel); viewModel.getTitle = () => this.getTitle(viewModel);
viewModel.getListTitle = () => this.getListTitle(viewModel); viewModel.getListTitle = () => this.getListTitle(viewModel);
viewModel.getVerboseName = this.getVerboseName; viewModel.getVerboseName = this.getVerboseName;
return viewModel; 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 * Saves the (full) update to an existing model. So called "update"-function
* Provides a default procedure, but can be overwritten if required * Provides a default procedure, but can be overwritten if required
@ -345,10 +215,8 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
* @param viewModel the view model that the update is based on * @param viewModel the view model that the update is based on
*/ */
public async update(update: Partial<M>, viewModel: V): Promise<void> { public async update(update: Partial<M>, viewModel: V): Promise<void> {
const sendUpdate = new this.baseModelCtor(); const data = viewModel.getUpdatedModel(update);
sendUpdate.patchValues(viewModel.getModel()); return await this.dataSend.updateModel(data);
sendUpdate.patchValues(update);
return await this.dataSend.updateModel(sendUpdate);
} }
/** /**
@ -359,9 +227,8 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
* @param viewModel the motion to update * @param viewModel the motion to update
*/ */
public async patch(update: Partial<M>, viewModel: V): Promise<void> { public async patch(update: Partial<M>, viewModel: V): Promise<void> {
const patch = new this.baseModelCtor(); const patch = new this.baseModelCtor(update);
patch.id = viewModel.id; patch.id = viewModel.id;
patch.patchValues(update);
return await this.dataSend.partialUpdateModel(patch); return await this.dataSend.partialUpdateModel(patch);
} }
@ -384,8 +251,7 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
public async create(model: M): Promise<Identifiable> { public async create(model: M): Promise<Identifiable> {
// this ensures we get a valid base model, even if the view was just // this ensures we get a valid base model, even if the view was just
// sending an object with "as MyModelClass" // sending an object with "as MyModelClass"
const sendModel = new this.baseModelCtor(); const sendModel = new this.baseModelCtor(model);
sendModel.patchValues(model);
// Strips empty fields from the sending mode data (except false) // Strips empty fields from the sending mode data (except false)
// required for i.e. users, since group list is mandatory // required for i.e. users, since group list is mandatory

View File

@ -154,8 +154,8 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config,
throw new Error('Config variables cannot be created'); throw new Error('Config variables cannot be created');
} }
public changedModels(ids: number[], initialLoading: boolean): void { public changedModels(ids: number[]): void {
super.changedModels(ids, initialLoading); super.changedModels(ids);
ids.forEach(id => { ids.forEach(id => {
this.updateConfigStructure(false, this.viewModelStore[id]); this.updateConfigStructure(false, this.viewModelStore[id]);
@ -246,10 +246,7 @@ export class ConfigRepositoryService extends BaseRepository<ViewConfig, Config,
* Saves a config value. * Saves a config value.
*/ */
public async update(config: Partial<Config>, viewConfig: ViewConfig): Promise<void> { public async update(config: Partial<Config>, viewConfig: ViewConfig): Promise<void> {
const updatedConfig = new Config(); const updatedConfig = viewConfig.getUpdatedModel(config);
updatedConfig.patchValues(viewConfig.config);
updatedConfig.patchValues(config);
// TODO: Use datasendService, if it can switch correctly between put, post and patch
await this.http.put(`/rest/${updatedConfig.collectionString}/${updatedConfig.key}/`, updatedConfig); await this.http.put(`/rest/${updatedConfig.collectionString}/${updatedConfig.key}/`, updatedConfig);
} }

View File

@ -112,41 +112,29 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
/** /**
* Sets a change recommendation to accepted. * Sets a change recommendation to accepted.
* *
* @param {ViewMotionChangeRecommendation} change * @param {ViewMotionChangeRecommendation} changeRecommendation
*/ */
public async setAccepted(change: ViewMotionChangeRecommendation): Promise<void> { public async setAccepted(changeRecommendation: ViewMotionChangeRecommendation): Promise<void> {
const changeReco = change.changeRecommendation; await this.patch({ rejected: false }, changeRecommendation);
changeReco.patchValues({
rejected: false
});
await this.dataSend.partialUpdateModel(changeReco);
} }
/** /**
* Sets a change recommendation to rejected. * Sets a change recommendation to rejected.
* *
* @param {ViewMotionChangeRecommendation} change * @param {ViewMotionChangeRecommendation} changeRecommendation
*/ */
public async setRejected(change: ViewMotionChangeRecommendation): Promise<void> { public async setRejected(changeRecommendation: ViewMotionChangeRecommendation): Promise<void> {
const changeReco = change.changeRecommendation; await this.patch({ rejected: true }, changeRecommendation);
changeReco.patchValues({
rejected: true
});
await this.dataSend.partialUpdateModel(changeReco);
} }
/** /**
* Sets if a change recommendation is internal (for the administrators) or not. * Sets if a change recommendation is internal (for the administrators) or not.
* *
* @param {ViewMotionChangeRecommendation} change * @param {ViewMotionChangeRecommendation} changeRecommendation
* @param {boolean} internal * @param {boolean} internal
*/ */
public async setInternal(change: ViewMotionChangeRecommendation, internal: boolean): Promise<void> { public async setInternal(changeRecommendation: ViewMotionChangeRecommendation, internal: boolean): Promise<void> {
const changeReco = change.changeRecommendation; await this.patch({ internal: internal }, changeRecommendation);
changeReco.patchValues({
internal: internal
});
await this.dataSend.partialUpdateModel(changeReco);
} }
public getTitleWithChanges = (originalTitle: string, change: ViewUnifiedChange, crMode: ChangeRecoMode): string => { public getTitleWithChanges = (originalTitle: string, change: ViewUnifiedChange, crMode: ChangeRecoMode): string => {

View File

@ -32,6 +32,7 @@ import { ViewTag } from 'app/site/tags/models/view-tag';
import { ViewPersonalNote } from 'app/site/users/models/view-personal-note'; import { ViewPersonalNote } from 'app/site/users/models/view-personal-note';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../base-is-agenda-item-and-list-of-speakers-content-object-repository'; import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../base-is-agenda-item-and-list-of-speakers-content-object-repository';
import { NestedModelDescriptors } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service'; import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { DataSendService } from '../../core-services/data-send.service'; import { DataSendService } from '../../core-services/data-send.service';
import { LinenumberingService, LineNumberRange } from '../../ui-services/linenumbering.service'; import { LinenumberingService, LineNumberRange } from '../../ui-services/linenumbering.service';
@ -94,21 +95,6 @@ const MotionRelations: RelationDefinition[] = [
ownKey: 'motion_block', ownKey: 'motion_block',
foreignViewModel: ViewMotionBlock foreignViewModel: ViewMotionBlock
}, },
{
type: 'nested',
ownKey: 'submitters',
foreignViewModel: ViewSubmitter,
foreignModel: Submitter,
order: 'weight',
relationDefinition: [
{
type: 'M2O',
ownIdKey: 'user_id',
ownKey: 'user',
foreignViewModel: ViewUser
}
]
},
{ {
type: 'M2M', type: 'M2M',
ownIdKey: 'supporters_id', ownIdKey: 'supporters_id',
@ -138,10 +124,39 @@ const MotionRelations: RelationDefinition[] = [
foreignIdKey: 'parent_id', foreignIdKey: 'parent_id',
ownKey: 'amendments', ownKey: 'amendments',
foreignViewModel: ViewMotion foreignViewModel: ViewMotion
},
// TMP:
{
type: 'M2O',
ownIdKey: 'parent_id',
ownKey: 'parent',
foreignViewModel: ViewMotion
} }
// Personal notes are dynamically added in the repo. // 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) * Repository Services for motions (and potentially categories)
* *
@ -198,7 +213,17 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
private readonly diff: DiffService, private readonly diff: DiffService,
private operator: OperatorService private operator: OperatorService
) { ) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, relationManager, Motion, MotionRelations); super(
DS,
dataSend,
mapperService,
viewModelStoreService,
translate,
relationManager,
Motion,
MotionRelations,
MotionNestedModelDescriptors
);
config.get<SortProperty>('motions_motions_sorting').subscribe(conf => { config.get<SortProperty>('motions_motions_sorting').subscribe(conf => {
this.sortProperty = conf; this.sortProperty = conf;
this.setConfigSortFn(); this.setConfigSortFn();
@ -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) => { public getTitle = (titleInformation: MotionTitleInformation) => {
if (titleInformation.identifier) { if (titleInformation.identifier) {
return titleInformation.identifier + ': ' + titleInformation.title; return titleInformation.identifier + ': ' + titleInformation.title;
@ -321,8 +298,8 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
return this.translate.instant(plural ? 'Motions' : 'Motion'); return this.translate.instant(plural ? 'Motions' : 'Motion');
}; };
protected createViewModelWithTitles(model: Motion, initialLoading: boolean): ViewMotion { protected createViewModelWithTitles(model: Motion): ViewMotion {
const viewModel = super.createViewModelWithTitles(model, initialLoading); const viewModel = super.createViewModelWithTitles(model);
viewModel.getIdentifierOrTitle = () => this.getIdentifierOrTitle(viewModel); viewModel.getIdentifierOrTitle = () => this.getIdentifierOrTitle(viewModel);
viewModel.getProjectorTitle = () => this.getAgendaSlideTitle(viewModel); viewModel.getProjectorTitle = () => this.getAgendaSlideTitle(viewModel);
@ -330,6 +307,37 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
return viewModel; 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 * Get the personal note content for one motion by their id
* *
@ -341,9 +349,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
return; return;
} }
const personalNote = this.viewModelStoreService.find(ViewPersonalNote, pn => { const personalNote = this.getPersonalNote();
return pn.userId === this.operator.user.id;
});
if (!personalNote) { if (!personalNote) {
return; return;
} }
@ -735,7 +741,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
const motion = amendment.parent; const motion = amendment.parent;
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength); const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
return amendment.amendment_paragraphs return (amendment.amendment_paragraphs || [])
.map( .map(
(newText: string, paraNo: number): DiffLinesInParagraph => { (newText: string, paraNo: number): DiffLinesInParagraph => {
if (newText !== null) { if (newText !== null) {
@ -779,7 +785,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
const motion = amendment.parent; const motion = amendment.parent;
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength); const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
return amendment.amendment_paragraphs return (amendment.amendment_paragraphs || [])
.map( .map(
(newText: string, paraNo: number): ViewMotionAmendedParagraph => { (newText: string, paraNo: number): ViewMotionAmendedParagraph => {
if (newText === null) { if (newText === null) {

View File

@ -25,6 +25,12 @@ const StateRelations: RelationDefinition[] = [
ownIdKey: 'next_states_id', ownIdKey: 'next_states_id',
ownKey: 'next_states', ownKey: 'next_states',
foreignViewModel: ViewState foreignViewModel: ViewState
},
{
type: 'M2M',
foreignIdKey: 'next_states_id',
ownKey: 'previous_states',
foreignViewModel: ViewState
} }
]; ];

View File

@ -80,8 +80,7 @@ export class ProjectorRepositoryService extends BaseRepository<ViewProjector, Pr
* Creates a new projector. Adds the clock as default, stable element * Creates a new projector. Adds the clock as default, stable element
*/ */
public async create(projectorData: Partial<Projector>): Promise<Identifiable> { public async create(projectorData: Partial<Projector>): Promise<Identifiable> {
const projector = new Projector(); const projector = new Projector(projectorData);
projector.patchValues(projectorData);
projector.elements = [{ name: 'core/clock', stable: true }]; projector.elements = [{ name: 'core/clock', stable: true }];
return await this.dataSend.createModel(projector); return await this.dataSend.createModel(projector);
} }

View File

@ -147,8 +147,8 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
/** /**
* Adds teh short and full name to the view user. * Adds teh short and full name to the view user.
*/ */
protected createViewModelWithTitles(model: User, initialLoading: boolean): ViewUser { protected createViewModelWithTitles(model: User): ViewUser {
const viewModel = super.createViewModelWithTitles(model, initialLoading); const viewModel = super.createViewModelWithTitles(model);
viewModel.getFullName = () => this.getFullName(viewModel); viewModel.getFullName = () => this.getFullName(viewModel);
viewModel.getShortName = () => this.getShortName(viewModel); viewModel.getShortName = () => this.getShortName(viewModel);
return viewModel; return viewModel;
@ -163,23 +163,19 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User, UserTi
* @param viewUser * @param viewUser
*/ */
public async update(update: Partial<User>, viewUser: ViewUser): Promise<void> { public async update(update: Partial<User>, viewUser: ViewUser): Promise<void> {
const updateUser = new User();
updateUser.patchValues(viewUser.user);
updateUser.patchValues(update);
// if the user deletes the username, reset // if the user deletes the username, reset
// prevents the server of generating '<firstname> <lastname> +1' as username // prevents the server of generating '<firstname> <lastname> +1' as username
if (updateUser.username === '') { if (update.username === '') {
updateUser.username = viewUser.username; update.username = viewUser.username;
} }
// if the update user does not have a gender-field, send gender as empty string. // if the update user does not have a gender-field, send gender as empty string.
// This allow to delete a previously selected gender // This allow to delete a previously selected gender
if (!updateUser.gender) { if (!update.gender) {
updateUser.gender = ''; update.gender = '';
} }
return await this.dataSend.updateModel(updateUser); return super.update(update, viewUser);
} }
/** /**

View File

@ -309,7 +309,7 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
}; };
}); });
if (!!noneOptionLabel) { if (noneOptionLabel) {
filterProperties.push('-'); filterProperties.push('-');
filterProperties.push({ filterProperties.push({
condition: null, condition: null,

View File

@ -95,7 +95,7 @@ export abstract class BaseSortService<T extends Identifiable & Displayable> {
case 'number': case 'number':
return firstProperty > secondProperty ? 1 : -1; return firstProperty > secondProperty ? 1 : -1;
case 'string': case 'string':
if (!!firstProperty && !secondProperty) { if (firstProperty && !secondProperty) {
return -1; return -1;
} else if (!firstProperty && !!secondProperty) { } else if (!firstProperty && !!secondProperty) {
return 1; return 1;

View File

@ -156,7 +156,7 @@ export class ExtensionFieldComponent implements OnInit, OnDestroy {
this.searchValueSubscription = this.extensionFieldForm this.searchValueSubscription = this.extensionFieldForm
.get('list') .get('list')
.valueChanges.subscribe((value: number) => { .valueChanges.subscribe((value: number) => {
if (!!value) { if (value) {
if (this.listSubmitOnChange) { if (this.listSubmitOnChange) {
this.listChange.emit(value); this.listChange.emit(value);
} }

View File

@ -531,7 +531,7 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
propertyAsString = '' + item[prop]; propertyAsString = '' + item[prop];
} }
if (!!propertyAsString) { if (propertyAsString) {
const foundProp = const foundProp =
propertyAsString propertyAsString
.trim() .trim()
@ -635,9 +635,11 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
* does not work, tying the tables to the same hight. * does not work, tying the tables to the same hight.
*/ */
public async scrollToPreviousPosition(): Promise<void> { public async scrollToPreviousPosition(): Promise<void> {
if (this.ngrid) {
const scrollIndex = await this.getScrollIndex(this.listStorageKey); const scrollIndex = await this.getScrollIndex(this.listStorageKey);
this.ngrid.viewport.scrollToIndex(scrollIndex); this.ngrid.viewport.scrollToIndex(scrollIndex);
} }
}
/** /**
* This function changes the height of the row for virtual-scrolling in the relating `.scss`-file. * This function changes the height of the row for virtual-scrolling in the relating `.scss`-file.

View File

@ -47,7 +47,7 @@ export class RoundedInputComponent implements OnInit, OnDestroy {
*/ */
@Input() @Input()
public set model(value: string) { public set model(value: string) {
if (!!value) { if (value) {
this.modelForm.setValue(value); this.modelForm.setValue(value);
} }
} }

View File

@ -633,7 +633,7 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> implemen
break; break;
case Direction.RIGHT: case Direction.RIGHT:
if (!!possibleParent) { if (possibleParent) {
const nextLevel = this.nextNode.level + direction.steps; const nextLevel = this.nextNode.level + direction.steps;
if (nextLevel <= possibleParent.level) { if (nextLevel <= possibleParent.level) {
this.placeholderLevel = nextLevel; this.placeholderLevel = nextLevel;
@ -646,7 +646,7 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> implemen
break; break;
case Direction.NOWAY: case Direction.NOWAY:
if (!!possibleParent) { if (!possibleParent) {
if (this.nextNode.level <= possibleParent.level + 1) { if (this.nextNode.level <= possibleParent.level + 1) {
this.placeholderLevel = this.nextNode.level; this.placeholderLevel = this.nextNode.level;
} else { } else {
@ -678,7 +678,7 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> implemen
case Direction.UPWARDS: case Direction.UPWARDS:
for (let i = 0; i < steps; ++i) { for (let i = 0; i < steps; ++i) {
const parent = this.getExpandedParentNode(this.osTreeData[currentPosition - 1]); const parent = this.getExpandedParentNode(this.osTreeData[currentPosition - 1]);
if (!!parent) { if (parent) {
currentPosition = parent.position; currentPosition = parent.position;
} else { } else {
break; break;
@ -693,7 +693,7 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> implemen
currentPosition -= 1; currentPosition -= 1;
for (let i = 0; i < steps; ++i) { for (let i = 0; i < steps; ++i) {
const parent = this.getExpandedParentNode(this.osTreeData[currentPosition]); const parent = this.getExpandedParentNode(this.osTreeData[currentPosition]);
if (!!parent) { if (parent) {
currentPosition = parent.position - 1; currentPosition = parent.position - 1;
} else { } else {
break; break;

View File

@ -35,7 +35,7 @@ export class SpeakerButtonComponent implements OnDestroy {
this.cleanLosSub(); this.cleanLosSub();
if (!!listOfSpeakers) { if (listOfSpeakers) {
this.losSub = this.listOfSpeakersRepo this.losSub = this.listOfSpeakersRepo
.getViewModelObservable(listOfSpeakers.id) .getViewModelObservable(listOfSpeakers.id)
.pipe(distinctUntilChanged()) .pipe(distinctUntilChanged())

View File

@ -2,6 +2,12 @@ import { BaseModelWithContentObject } from '../base/base-model-with-content-obje
import { ContentObject } from '../base/content-object'; import { ContentObject } from '../base/content-object';
import { Speaker } from './speaker'; import { Speaker } from './speaker';
export interface ListOfSpeakersWithoutNestedModels extends BaseModelWithContentObject<ListOfSpeakers> {
id: number;
title_information: object;
closed: boolean;
}
/** /**
* Representations of agenda Item * Representations of agenda Item
* @ignore * @ignore
@ -19,3 +25,5 @@ export class ListOfSpeakers extends BaseModelWithContentObject<ListOfSpeakers> {
super(ListOfSpeakers.COLLECTIONSTRING, input); super(ListOfSpeakers.COLLECTIONSTRING, input);
} }
} }
export interface ListOfSpeakers extends ListOfSpeakersWithoutNestedModels {}

View File

@ -17,10 +17,6 @@ export class AssignmentPollOption extends BaseModel<AssignmentPollOption> {
public id: number; // The AssignmentPollOption id public id: number; // The AssignmentPollOption id
public candidate_id: number; // the user id of the candidate 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 is_elected: boolean;
public votes: AssignmentOptionVote[]; public votes: AssignmentOptionVote[];
public poll_id: number; public poll_id: number;

View File

@ -2,6 +2,20 @@ import { AssignmentPollMethod } from 'app/site/assignments/services/assignment-p
import { AssignmentPollOption } from './assignment-poll-option'; import { AssignmentPollOption } from './assignment-poll-option';
import { BaseModel } from '../base/base-model'; import { BaseModel } from '../base/base-model';
export interface AssignmentPollWithoutNestedModels extends BaseModel<AssignmentPoll> {
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 * Content of the 'polls' property of assignments
* @ignore * @ignore
@ -11,21 +25,8 @@ export class AssignmentPoll extends BaseModel<AssignmentPoll> {
private static DECIMAL_FIELDS = ['votesvalid', 'votesinvalid', 'votescast', 'votesno', 'votesabstain']; private static DECIMAL_FIELDS = ['votesvalid', 'votesinvalid', 'votescast', 'votesno', 'votesabstain'];
public id: number; public id: number;
public pollmethod: AssignmentPollMethod;
public description: string;
public published: boolean;
public options: AssignmentPollOption[]; 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) { public constructor(input?: any) {
// cast stringify numbers // cast stringify numbers
if (input) { if (input) {
@ -38,3 +39,4 @@ export class AssignmentPoll extends BaseModel<AssignmentPoll> {
super(AssignmentPoll.COLLECTIONSTRING, input); super(AssignmentPoll.COLLECTIONSTRING, input);
} }
} }
export interface AssignmentPoll extends AssignmentPollWithoutNestedModels {}

View File

@ -2,6 +2,17 @@ import { AssignmentPoll } from './assignment-poll';
import { AssignmentRelatedUser } from './assignment-related-user'; import { AssignmentRelatedUser } from './assignment-related-user';
import { BaseModelWithAgendaItemAndListOfSpeakers } from '../base/base-model-with-agenda-item-and-list-of-speakers'; import { BaseModelWithAgendaItemAndListOfSpeakers } from '../base/base-model-with-agenda-item-and-list-of-speakers';
export interface AssignmentWithoutNestedModels extends BaseModelWithAgendaItemAndListOfSpeakers<Assignment> {
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. * Representation of an assignment.
* @ignore * @ignore
@ -10,15 +21,8 @@ export class Assignment extends BaseModelWithAgendaItemAndListOfSpeakers<Assignm
public static COLLECTIONSTRING = 'assignments/assignment'; public static COLLECTIONSTRING = 'assignments/assignment';
public id: number; public id: number;
public title: string;
public description: string;
public open_posts: number;
public phase: number; // see Openslides constants
public assignment_related_users: AssignmentRelatedUser[]; public assignment_related_users: AssignmentRelatedUser[];
public poll_description_default: number;
public polls: AssignmentPoll[]; public polls: AssignmentPoll[];
public tags_id: number[];
public attachments_id: number[];
public constructor(input?: any) { public constructor(input?: any) {
super(Assignment.COLLECTIONSTRING, input); super(Assignment.COLLECTIONSTRING, input);
@ -31,13 +35,5 @@ export class Assignment extends BaseModelWithAgendaItemAndListOfSpeakers<Assignm
}) })
.map((candidate: AssignmentRelatedUser) => candidate.user_id); .map((candidate: AssignmentRelatedUser) => 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 {}

View File

@ -4,7 +4,7 @@ import { ContentObject } from './content-object';
/** /**
* A base model which has a content object, like items of list of speakers. * A base model which has a content object, like items of list of speakers.
*/ */
export abstract class BaseModelWithContentObject<T = object> extends BaseModel<T> { export abstract class BaseModelWithContentObject<T = any> extends BaseModel<T> {
public abstract content_object: ContentObject; public abstract content_object: ContentObject;
public get contentObjectData(): ContentObject { public get contentObjectData(): ContentObject {

View File

@ -1,5 +1,5 @@
import { Collection } from './collection'; import { Collection } from './collection';
import { Deserializable } from './deserializable'; import { Deserializer } from './deserializer';
import { Identifiable } from './identifiable'; import { Identifiable } from './identifiable';
export interface ModelConstructor<T extends BaseModel<T>> { export interface ModelConstructor<T extends BaseModel<T>> {
@ -11,69 +11,21 @@ export interface ModelConstructor<T extends BaseModel<T>> {
* Abstract parent class to set rules and functions for all models. * 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<Motion>` * When inherit from this class, give the subclass as the type. E.g. `class Motion extends BaseModel<Motion>`
*/ */
export abstract class BaseModel<T = object> implements Deserializable, Identifiable, Collection { export abstract class BaseModel<T = any> extends Deserializer implements Identifiable, Collection {
/** public get elementId(): string {
* force children of BaseModel to have a collectionString. return `${this.collectionString}:${this.id}`;
*
* 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;
} }
/** /**
* force children of BaseModel to have an id * force children of BaseModel to have an id
*/ */
public abstract id: number; public abstract id: number;
/** protected constructor(public readonly collectionString: string, input?: Partial<T>) {
* constructor that calls super from parent class super(input);
*
* @param collectionString
* @param verboseName
* @param input
*/
protected constructor(collectionString: string, input?: any) {
this._collectionString = collectionString;
if (input) {
this.changeNullValuesToUndef(input);
this.deserialize(input);
}
} }
/** public getUpdatedVersion(update: Partial<T>): T {
* Prevent to send literally "null" if should be send const copy: T = (<any>Object.assign({}, this)) as T;
* @param input object to deserialize return Object.assign(copy, update);
*/
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<T>): 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);
} }
} }

View File

@ -11,28 +11,16 @@ export abstract class Deserializer implements Deserializable {
*/ */
protected constructor(input?: any) { protected constructor(input?: any) {
if (input) { if (input) {
this.changeNullValuesToUndef(input);
this.deserialize(input); this.deserialize(input);
} }
} }
/** /**
* should be used to assign JSON values to the object itself. * Most simple and most commonly used deserialize function.
* @param input * Inherited to children, can be overwritten for special use cases
* @param input JSON data for deserialization.
*/ */
public deserialize(input: any): void { public deserialize(input: any): void {
Object.assign(this, input); 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;
}
});
}
} }

View File

@ -9,6 +9,21 @@ interface FileMetadata {
encrypted?: boolean; encrypted?: boolean;
} }
export interface MediafileWithoutNestedModels extends BaseModelWithListOfSpeakers<Mediafile> {
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" * Representation of MediaFile. Has the nested property "File"
* @ignore * @ignore
@ -16,25 +31,14 @@ interface FileMetadata {
export class Mediafile extends BaseModelWithListOfSpeakers<Mediafile> { export class Mediafile extends BaseModelWithListOfSpeakers<Mediafile> {
public static COLLECTIONSTRING = 'mediafiles/mediafile'; public static COLLECTIONSTRING = 'mediafiles/mediafile';
public id: number; public id: number;
public title: string;
public mediafile?: FileMetadata; 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 { public get has_inherited_access_groups(): boolean {
return typeof this.inherited_access_groups_id !== 'boolean'; return typeof this.inherited_access_groups_id !== 'boolean';
} }
public constructor(input?: any) { public constructor(input?: any) {
super(Mediafile.COLLECTIONSTRING); super(Mediafile.COLLECTIONSTRING, input);
// Do not change null to undefined...
this.deserialize(input);
} }
/** /**
@ -46,3 +50,4 @@ export class Mediafile extends BaseModelWithListOfSpeakers<Mediafile> {
return `${this.media_url_prefix}${this.path}`; return `${this.media_url_prefix}${this.path}`;
} }
} }
export interface Mediafile extends MediafileWithoutNestedModels {}

View File

@ -9,6 +9,40 @@ export interface MotionComment {
read_groups_id: number[]; read_groups_id: number[];
} }
export interface MotionWithoutNestedModels extends BaseModelWithAgendaItemAndListOfSpeakers<Motion> {
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. * Representation of Motion.
* *
@ -20,35 +54,7 @@ export class Motion extends BaseModelWithAgendaItemAndListOfSpeakers<Motion> {
public static COLLECTIONSTRING = 'motions/motion'; public static COLLECTIONSTRING = 'motions/motion';
public id: number; 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 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) { public constructor(input?: any) {
super(Motion.COLLECTIONSTRING, input); super(Motion.COLLECTIONSTRING, input);
@ -57,7 +63,7 @@ export class Motion extends BaseModelWithAgendaItemAndListOfSpeakers<Motion> {
/** /**
* returns the motion submitters user ids * returns the motion submitters user ids
*/ */
public get sorted_submitters_id(): number[] { public get sorted_submitter_ids(): number[] {
return this.submitters return this.submitters
.sort((a: Submitter, b: Submitter) => { .sort((a: Submitter, b: Submitter) => {
return a.weight - b.weight; return a.weight - b.weight;
@ -65,3 +71,5 @@ export class Motion extends BaseModelWithAgendaItemAndListOfSpeakers<Motion> {
.map((submitter: Submitter) => submitter.user_id); .map((submitter: Submitter) => submitter.user_id);
} }
} }
export interface Motion extends MotionWithoutNestedModels {}

View File

@ -40,21 +40,4 @@ export class State extends BaseModel<State> {
public constructor(input?: any) { public constructor(input?: any) {
super(State.COLLECTIONSTRING, input); 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;
}
} }

View File

@ -12,7 +12,7 @@ export class Topic extends BaseModelWithAgendaItemAndListOfSpeakers<Topic> {
public text: string; public text: string;
public attachments_id: number[]; public attachments_id: number[];
public constructor(input?: any) { public constructor(input?: Partial<Topic>) {
super(Topic.COLLECTIONSTRING, input); super(Topic.COLLECTIONSTRING, input);
} }
} }

View File

@ -11,11 +11,7 @@ export class Group extends BaseModel<Group> {
public name: string; public name: string;
public permissions: string[]; public permissions: string[];
public constructor(input?: any) { public constructor(input?: Partial<Group>) {
super(Group.COLLECTIONSTRING, input); super(Group.COLLECTIONSTRING, input);
if (!input) {
// permissions are required for new groups
this.permissions = [];
}
} }
} }

View File

@ -55,7 +55,7 @@ export class PersonalNote extends BaseModel<PersonalNote> implements PersonalNot
public user_id: number; public user_id: number;
public notes: PersonalNotesFormat; public notes: PersonalNotesFormat;
public constructor(input: any) { public constructor(input: Partial<PersonalNote>) {
super(PersonalNote.COLLECTIONSTRING, input); super(PersonalNote.COLLECTIONSTRING, input);
} }
} }

View File

@ -18,20 +18,20 @@ export class User extends BaseModel<User> {
public title: string; public title: string;
public first_name: string; public first_name: string;
public last_name: string; public last_name: string;
public gender: string; public gender?: string;
public structure_level: string; public structure_level: string;
public number: string; public number: string;
public about_me: string; public about_me: string;
public groups_id: number[]; public groups_id: number[];
public is_present: boolean; public is_present: boolean;
public is_committee: boolean; public is_committee: boolean;
public email: string; public email?: string;
public last_email_send?: string; // ISO datetime string public last_email_send?: string; // ISO datetime string
public comment: string; public comment?: string;
public is_active: boolean; public is_active?: boolean;
public default_password: string; public default_password?: string;
public constructor(input?: any) { public constructor(input?: Partial<User>) {
super(User.COLLECTIONSTRING, input); super(User.COLLECTIONSTRING, input);
} }
} }

View File

@ -98,7 +98,7 @@ export class AgendaListComponent extends BaseListViewComponent<ViewItem> impleme
/** /**
* Define extra filter properties * Define extra filter properties
*/ */
public filterProps = ['itemNumber', 'comment', 'getListTitle']; public filterProps = ['item_number', 'comment', 'getListTitle'];
/** /**
* The usual constructor for components * The usual constructor for components
@ -205,7 +205,7 @@ export class AgendaListComponent extends BaseListViewComponent<ViewItem> impleme
public async onAutoNumbering(): Promise<void> { public async onAutoNumbering(): Promise<void> {
const title = this.translate.instant('Are you sure you want to number all agenda items?'); const title = this.translate.instant('Are you sure you want to number all agenda items?');
if (await this.promptService.open(title)) { 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<ViewItem> impleme
* Click handler for the done button in the dot-menu * Click handler for the done button in the dot-menu
*/ */
public async onDoneSingleButton(item: ViewItem): Promise<void> { public async onDoneSingleButton(item: ViewItem): Promise<void> {
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<ViewItem> impleme
const title = this.translate.instant('Are you sure you want to remove this entry from the agenda?'); const title = this.translate.instant('Are you sure you want to remove this entry from the agenda?');
const content = item.contentObject.getTitle(); const content = item.contentObject.getTitle();
if (await this.promptService.open(title, content)) { 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<ViewItem> impleme
const title = this.translate.instant('Are you sure you want to delete this topic?'); const title = this.translate.instant('Are you sure you want to delete this topic?');
const content = item.contentObject.getTitle(); const content = item.contentObject.getTitle();
if (await this.promptService.open(title, content)) { 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<ViewItem> impleme
public async setAgendaType(agendaType: number): Promise<void> { public async setAgendaType(agendaType: number): Promise<void> {
try { try {
for (const item of this.selectedRows) { 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) { } catch (e) {
this.raiseError(e); this.raiseError(e);

View File

@ -51,7 +51,7 @@ export class ItemInfoDialogComponent {
// load current values // load current values
this.agendaInfoForm.get('type').setValue(item.type); this.agendaInfoForm.get('type').setValue(item.type);
this.agendaInfoForm.get('durationText').setValue(this.durationService.durationToString(item.duration, 'h')); 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); this.agendaInfoForm.get('comment').setValue(item.comment);
} }

View File

@ -60,7 +60,7 @@
<mat-icon>mic</mat-icon> <mat-icon>mic</mat-icon>
</span> </span>
<span class="name">{{ activeSpeaker.getTitle() }}</span> <span class="name">{{ activeSpeaker.getListTitle() }}</span>
<span class="suffix"> <span class="suffix">
<!-- Stop speaker button --> <!-- Stop speaker button -->

View File

@ -417,9 +417,7 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
*/ */
public closeSpeakerList(): Promise<void> { public closeSpeakerList(): Promise<void> {
if (!this.viewListOfSpeakers.closed) { if (!this.viewListOfSpeakers.closed) {
return this.listOfSpeakersRepo return this.listOfSpeakersRepo.update({ closed: true }, this.viewListOfSpeakers).catch(this.raiseError);
.update({ closed: true }, this.viewListOfSpeakers)
.then(null, this.raiseError);
} }
} }
@ -428,9 +426,7 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
*/ */
public openSpeakerList(): Promise<void> { public openSpeakerList(): Promise<void> {
if (this.viewListOfSpeakers.closed) { if (this.viewListOfSpeakers.closed) {
return this.listOfSpeakersRepo return this.listOfSpeakersRepo.update({ closed: false }, this.viewListOfSpeakers).catch(this.raiseError);
.update({ closed: false }, this.viewListOfSpeakers)
.then(null, this.raiseError);
} }
} }

View File

@ -18,34 +18,6 @@ export class ViewItem extends BaseViewModelWithContentObject<Item, BaseViewModel
return this._model; return this._model;
} }
public get itemNumber(): string {
return this.item.item_number;
}
public get title_information(): object {
return this.item.title_information;
}
public get duration(): number {
return this.item.duration;
}
public get type(): number {
return this.item.type;
}
public get closed(): boolean {
return this.item.closed;
}
public get comment(): string {
return this.item.comment;
}
public get level(): number {
return this.item.level;
}
public getSubtitle: () => string | null; public getSubtitle: () => string | null;
/** /**
@ -71,19 +43,5 @@ export class ViewItem extends BaseViewModelWithContentObject<Item, BaseViewModel
const type = ItemVisibilityChoices.find(choice => choice.key === this.type); const type = ItemVisibilityChoices.find(choice => choice.key === this.type);
return type ? type.csvName : ''; 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 {}

View File

@ -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 { ContentObject } from 'app/shared/models/base/content-object';
import { BaseViewModelWithContentObject } from 'app/site/base/base-view-model-with-content-object'; import { BaseViewModelWithContentObject } from 'app/site/base/base-view-model-with-content-object';
import { BaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers'; import { BaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers';
@ -19,20 +19,10 @@ export class ViewListOfSpeakers extends BaseViewModelWithContentObject<ListOfSpe
public static COLLECTIONSTRING = ListOfSpeakers.COLLECTIONSTRING; public static COLLECTIONSTRING = ListOfSpeakers.COLLECTIONSTRING;
protected _collectionString = ListOfSpeakers.COLLECTIONSTRING; protected _collectionString = ListOfSpeakers.COLLECTIONSTRING;
private _speakers?: ViewSpeaker[];
public get listOfSpeakers(): ListOfSpeakers { public get listOfSpeakers(): ListOfSpeakers {
return this._model; return this._model;
} }
public get speakers(): ViewSpeaker[] {
return this._speakers || [];
}
public get title_information(): object {
return this.listOfSpeakers.title_information;
}
/** /**
* Gets the amount of waiting speakers * Gets the amount of waiting speakers
*/ */
@ -40,10 +30,6 @@ export class ViewListOfSpeakers extends BaseViewModelWithContentObject<ListOfSpe
return this.speakers.filter(speaker => speaker.state === SpeakerState.WAITING).length; return this.speakers.filter(speaker => speaker.state === SpeakerState.WAITING).length;
} }
public get closed(): boolean {
return this.listOfSpeakers.closed;
}
public get listOfSpeakersUrl(): string { public get listOfSpeakersUrl(): string {
return `/agenda/speakers/${this.id}`; return `/agenda/speakers/${this.id}`;
} }
@ -65,3 +51,7 @@ export class ViewListOfSpeakers extends BaseViewModelWithContentObject<ListOfSpe
}; };
} }
} }
interface IListOfSpeakersRelations {
speakers: ViewSpeaker[];
}
export interface ViewListOfSpeakers extends ListOfSpeakersWithoutNestedModels, IListOfSpeakersRelations {}

View File

@ -18,46 +18,10 @@ export class ViewSpeaker extends BaseViewModel<Speaker> {
public static COLLECTIONSTRING = Speaker.COLLECTIONSTRING; public static COLLECTIONSTRING = Speaker.COLLECTIONSTRING;
protected _collectionString = Speaker.COLLECTIONSTRING; protected _collectionString = Speaker.COLLECTIONSTRING;
private _user?: ViewUser;
public get speaker(): Speaker { public get speaker(): Speaker {
return this._model; 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 * @returns
* - waiting if there is no begin nor end time * - waiting if there is no begin nor end time
@ -82,7 +46,10 @@ export class ViewSpeaker extends BaseViewModel<Speaker> {
return this.user ? this.user.gender : ''; return this.user ? this.user.gender : '';
} }
public getTitle = () => { public getListTitle = () => {
return this.name; return this.getTitle();
}; };
} }
export interface ViewSpeaker extends Speaker {
user?: ViewUser;
}

View File

@ -84,7 +84,7 @@ export class AgendaPdfService {
}, },
{ {
width: 60, width: 60,
text: nodeItem.item.itemNumber text: nodeItem.item.item_number
}, },
{ {
text: nodeItem.item.contentObject.getAgendaListTitleWithoutItemNumber() text: nodeItem.item.contentObject.getAgendaListTitleWithoutItemNumber()

View File

@ -36,11 +36,11 @@
<!-- Add/remove to/from agenda --> <!-- Add/remove to/from agenda -->
<div *osPerms="'agenda.can_manage'"> <div *osPerms="'agenda.can_manage'">
<button mat-menu-item (click)="addToAgenda()" *ngIf="assignment && !assignment.agendaItem"> <button mat-menu-item (click)="addToAgenda()" *ngIf="assignment && !assignment.item">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
<span translate>Add to agenda</span> <span translate>Add to agenda</span>
</button> </button>
<button mat-menu-item (click)="removeFromAgenda()" *ngIf="assignment && assignment.agendaItem"> <button mat-menu-item (click)="removeFromAgenda()" *ngIf="assignment && assignment.item">
<mat-icon>remove</mat-icon> <mat-icon>remove</mat-icon>
<span translate>Remove from agenda</span> <span translate>Remove from agenda</span>
</button> </button>
@ -241,7 +241,6 @@
matInput matInput
placeholder="{{ 'Title' | translate }}" placeholder="{{ 'Title' | translate }}"
formControlName="title" formControlName="title"
[value]="assignmentCopy.title || ''"
/> />
<mat-error>{{ 'The title is required' | translate }}</mat-error> <mat-error>{{ 'The title is required' | translate }}</mat-error>
</mat-form-field> </mat-form-field>
@ -287,7 +286,6 @@
matInput matInput
placeholder="{{ 'Default comment on the ballot paper' | translate }}" placeholder="{{ 'Default comment on the ballot paper' | translate }}"
formControlName="poll_description_default" formControlName="poll_description_default"
[value]="assignmentCopy.assignment.poll_description_default || ''"
/> />
</mat-form-field> </mat-form-field>
</div> </div>

View File

@ -112,11 +112,6 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
*/ */
private _assignment: ViewAssignment; private _assignment: ViewAssignment;
/**
* Copy instance of the assignment that the user might edit
*/
public assignmentCopy: ViewAssignment;
/** /**
* Check if the operator is a candidate * Check if the operator is a candidate
* *
@ -288,7 +283,6 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
* @param assignment * @param assignment
*/ */
private patchForm(assignment: ViewAssignment): void { private patchForm(assignment: ViewAssignment): void {
this.assignmentCopy = assignment;
const contentPatch: { [key: string]: any } = {}; const contentPatch: { [key: string]: any } = {};
Object.keys(this.assignmentForm.controls).forEach(control => { Object.keys(this.assignmentForm.controls).forEach(control => {
contentPatch[control] = assignment[control]; contentPatch[control] = assignment[control];
@ -299,11 +293,11 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
/** /**
* Save the current state of the assignment * Save the current state of the assignment
*/ */
public saveAssignment(): void { public async saveAssignment(): Promise<void> {
if (this.newAssignment) { if (this.newAssignment) {
this.createAssignment(); this.createAssignment();
} else { } else {
this.updateAssignmentFromForm(); await this.updateAssignmentFromForm();
} }
} }
@ -312,21 +306,21 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
* TODO: directly open poll dialog? * TODO: directly open poll dialog?
*/ */
public async createPoll(): Promise<void> { public async createPoll(): Promise<void> {
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 * Adds the operator to list of candidates
*/ */
public async addSelf(): Promise<void> { public async addSelf(): Promise<void> {
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 * Removes the operator from list of candidates
*/ */
public async removeSelf(): Promise<void> { public async removeSelf(): Promise<void> {
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<void> { public async addUser(userId: number): Promise<void> {
const user = this.userRepo.getViewModel(userId); const user = this.userRepo.getViewModel(userId);
if (user) { 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 * @param candidate A ViewAssignmentUser currently in the list of related users
*/ */
public async removeUser(candidate: ViewAssignmentRelatedUser): Promise<void> { public async removeUser(candidate: ViewAssignmentRelatedUser): Promise<void> {
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; this.editAssignment = true;
// TODO set defaults? // TODO set defaults?
this.assignment = new ViewAssignment(new Assignment()); 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 * @param value the phase to set
*/ */
public async onSetPhaseButton(value: number): Promise<void> { public async onSetPhaseButton(value: number): Promise<void> {
this.repo.update({ phase: value }, this.assignment).then(null, this.raiseError); this.repo.update({ phase: value }, this.assignment).catch(this.raiseError);
} }
public onDownloadPdf(): void { public onDownloadPdf(): void {
@ -438,10 +431,13 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
} }
} }
public updateAssignmentFromForm(): void { public async updateAssignmentFromForm(): Promise<void> {
this.repo try {
.patch({ ...this.assignmentForm.value }, this.assignmentCopy) await this.repo.patch({ ...this.assignmentForm.value }, this.assignment);
.then(() => (this.editAssignment = false), this.raiseError); 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) * (triggered on an autoupdate of either users or the assignment)
*/ */
private filterCandidates(): void { private filterCandidates(): void {
if (!this.assignment || !this.assignment.candidates) { if (!this.assignment || !this.assignment.candidates.length) {
this.filteredCandidates.next(this.availableCandidates.getValue()); this.filteredCandidates.next(this.availableCandidates.getValue());
} else { } else {
this.filteredCandidates.next( this.filteredCandidates.next(
@ -499,14 +495,14 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
public onSortingChange(listInNewOrder: ViewAssignmentRelatedUser[]): void { public onSortingChange(listInNewOrder: ViewAssignmentRelatedUser[]): void {
this.repo this.repo
.sortCandidates(listInNewOrder.map(relatedUser => relatedUser.id), this.assignment) .sortCandidates(listInNewOrder.map(relatedUser => relatedUser.id), this.assignment)
.then(null, this.raiseError); .catch(this.raiseError);
} }
public addToAgenda(): void { public addToAgenda(): void {
this.itemRepo.addItemToAgenda(this.assignment).then(null, this.raiseError); this.itemRepo.addItemToAgenda(this.assignment).catch(this.raiseError);
} }
public removeFromAgenda(): void { public removeFromAgenda(): void {
this.itemRepo.removeFromAgenda(this.assignment.agendaItem).then(null, this.raiseError); this.itemRepo.removeFromAgenda(this.assignment.item).catch(this.raiseError);
} }
} }

View File

@ -90,12 +90,10 @@ export class AssignmentPollDialogComponent {
*/ */
public submit(): void { public submit(): void {
const error = this.data.options.find(dataoption => { 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); const keyValue = dataoption.votes.find(o => o.value === key);
if (!keyValue || keyValue.weight === undefined) { return !keyValue || keyValue.weight === undefined;
return true; });
}
}
}); });
if (error) { if (error) {
this.matSnackBar.open( this.matSnackBar.open(

View File

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

View File

@ -161,7 +161,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
public async onDeletePoll(): Promise<void> { public async onDeletePoll(): Promise<void> {
const title = this.translate.instant('Are you sure you want to delete this ballot?'); const title = this.translate.instant('Are you sure you want to delete this ballot?');
if (await this.promptService.open(title)) { 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 { public enterVotes(): void {
const dialogRef = this.dialog.open(AssignmentPollDialogComponent, { 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(), data: this.poll.copy(),
...mediumDialogSettings ...mediumDialogSettings
}); });
dialogRef.afterClosed().subscribe(result => { dialogRef.afterClosed().subscribe(result => {
if (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? // TODO additional conditions: assignment not finished?
const viewAssignmentRelatedUser = this.assignment.assignment_related_users.find( const viewAssignmentRelatedUser = this.assignment.assignment_related_users.find(
user => user.user_id === option.user_id user => user.user_id === option.candidate_id
); );
if (viewAssignmentRelatedUser) { if (viewAssignmentRelatedUser) {
this.assignmentRepo.markElected(viewAssignmentRelatedUser, this.assignment, !option.is_elected); this.assignmentRepo.markElected(viewAssignmentRelatedUser, this.assignment, !option.is_elected);
@ -249,7 +248,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
*/ */
public async onEditDescriptionButton(): Promise<void> { public async onEditDescriptionButton(): Promise<void> {
const desc: string = this.descriptionForm.get('description').value; 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);
} }
/** /**

View File

@ -12,40 +12,14 @@ export class ViewAssignmentPollOption extends BaseViewModel<AssignmentPollOption
public static COLLECTIONSTRING = AssignmentPollOption.COLLECTIONSTRING; public static COLLECTIONSTRING = AssignmentPollOption.COLLECTIONSTRING;
protected _collectionString = AssignmentPollOption.COLLECTIONSTRING; protected _collectionString = AssignmentPollOption.COLLECTIONSTRING;
private _user?: ViewUser; // This is the "candidate". We'll stay consistent wich user here...
public get option(): AssignmentPollOption { public get option(): AssignmentPollOption {
return this._model; return this._model;
} }
/**
* Note: "User" instead of "candidate" to be consistent.
*/
public get user(): ViewUser | null {
return this._user;
}
public get id(): number {
return this.option.id;
}
public get user_id(): number {
return this.option.user_id;
}
public get is_elected(): boolean {
return this.option.is_elected;
}
public get votes(): AssignmentOptionVote[] { public get votes(): AssignmentOptionVote[] {
return this.option.votes.sort((a, b) => votesOrder.indexOf(a.value) - votesOrder.indexOf(b.value)); return this.option.votes.sort((a, b) => 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;
} }

View File

@ -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 { AssignmentPollOption } from 'app/shared/models/assignments/assignment-poll-option';
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model'; import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { AssignmentPollMethod } from '../services/assignment-poll.service';
import { ViewAssignmentPollOption } from './view-assignment-poll-option'; import { ViewAssignmentPollOption } from './view-assignment-poll-option';
export class ViewAssignmentPoll extends BaseProjectableViewModel<AssignmentPoll> { export class ViewAssignmentPoll extends BaseProjectableViewModel<AssignmentPoll> {
public static COLLECTIONSTRING = AssignmentPoll.COLLECTIONSTRING; public static COLLECTIONSTRING = AssignmentPoll.COLLECTIONSTRING;
protected _collectionString = AssignmentPoll.COLLECTIONSTRING; protected _collectionString = AssignmentPoll.COLLECTIONSTRING;
private _options: ViewAssignmentPollOption[];
public get poll(): AssignmentPoll { public get poll(): AssignmentPoll {
return this._model; 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 = () => { public getListTitle = () => {
return this.getTitle(); return this.getTitle();
}; };
@ -90,6 +20,20 @@ export class ViewAssignmentPoll extends BaseProjectableViewModel<AssignmentPoll>
return this.getTitle(); 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, * Creates a copy with deep-copy on all changing numerical values,
* but intact uncopied references to the users * but intact uncopied references to the users
@ -107,18 +51,8 @@ export class ViewAssignmentPoll extends BaseProjectableViewModel<AssignmentPoll>
}); });
return poll; return poll;
} }
}
public getSlide(): ProjectorElementBuildDeskriptor { export interface ViewAssignmentPoll extends AssignmentPollWithoutNestedModels {
return { options: ViewAssignmentPollOption[];
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'
};
}
} }

View File

@ -6,39 +6,15 @@ export class ViewAssignmentRelatedUser extends BaseViewModel<AssignmentRelatedUs
public static COLLECTIONSTRING = AssignmentRelatedUser.COLLECTIONSTRING; public static COLLECTIONSTRING = AssignmentRelatedUser.COLLECTIONSTRING;
protected _collectionString = AssignmentRelatedUser.COLLECTIONSTRING; protected _collectionString = AssignmentRelatedUser.COLLECTIONSTRING;
private _user?: ViewUser;
public get assignmentRelatedUser(): AssignmentRelatedUser { public get assignmentRelatedUser(): AssignmentRelatedUser {
return this._model; return this._model;
} }
public get user(): ViewUser { public getListTitle = () => {
return this._user; return this.getTitle();
}
public get id(): number {
return this.assignmentRelatedUser.id;
}
public get user_id(): number {
return this.assignmentRelatedUser.user_id;
}
public get assignment_id(): number {
return this.assignmentRelatedUser.assignment_id;
}
public get elected(): boolean {
return this.assignmentRelatedUser.elected;
}
public get weight(): number {
return this.assignmentRelatedUser.weight;
}
public getListTitle: () => string = this.getTitle;
public getTitle: () => string = () => {
return this.user ? this.user.getFullName() : '';
}; };
} }
export interface ViewAssignmentRelatedUser extends AssignmentRelatedUser {
user?: ViewUser;
}

View File

@ -1,5 +1,5 @@
import { SearchRepresentation } from 'app/core/ui-services/search.service'; 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 { 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 { BaseViewModelWithAgendaItemAndListOfSpeakers } from 'app/site/base/base-view-model-with-agenda-item-and-list-of-speakers';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
@ -41,60 +41,18 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers
public static COLLECTIONSTRING = Assignment.COLLECTIONSTRING; public static COLLECTIONSTRING = Assignment.COLLECTIONSTRING;
protected _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 { public get assignment(): Assignment {
return this._model; 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 { public get candidates(): ViewUser[] {
return this.assignment.phase; if (!this.assignment_related_users) {
return [];
}
return this.assignment_related_users.map(aru => aru.user).filter(x => !!x);
} }
public get phaseString(): string { public get phaseString(): string {
@ -123,7 +81,7 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers
* @returns the amount of candidates in the assignment's candidate list * @returns the amount of candidates in the assignment's candidate list
*/ */
public get candidateAmount(): number { public get candidateAmount(): number {
return this._assignment_related_users ? this._assignment_related_users.length : 0; return this.assignment_related_users.length;
} }
public formatForSearch(): SearchRepresentation { 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 {}

View File

@ -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 { BaseModelWithAgendaItemAndListOfSpeakers } from 'app/shared/models/base/base-model-with-agenda-item-and-list-of-speakers';
import { BaseProjectableViewModel } from './base-projectable-view-model'; 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 { import {
IBaseViewModelWithListOfSpeakers, BaseViewModelWithListOfSpeakers,
isBaseViewModelWithListOfSpeakers isBaseViewModelWithListOfSpeakers
} from './base-view-model-with-list-of-speakers'; } 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( export function isBaseViewModelWithAgendaItemAndListOfSpeakers(
obj: any obj: any
@ -15,54 +13,15 @@ export function isBaseViewModelWithAgendaItemAndListOfSpeakers(
return !!obj && isBaseViewModelWithAgendaItem(obj) && isBaseViewModelWithListOfSpeakers(obj); 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< export abstract class BaseViewModelWithAgendaItemAndListOfSpeakers<
M extends BaseModelWithAgendaItemAndListOfSpeakers = any M extends BaseModelWithAgendaItemAndListOfSpeakers = any
> extends BaseProjectableViewModel implements IBaseViewModelWithAgendaItem<M>, IBaseViewModelWithListOfSpeakers<M> { > extends BaseProjectableViewModel<M> {}
protected _item?: ViewItem;
protected _list_of_speakers?: ViewListOfSpeakers;
public get agendaItem(): ViewItem | null { export interface BaseViewModelWithAgendaItemAndListOfSpeakers<M extends BaseModelWithAgendaItemAndListOfSpeakers = any>
return this._item; extends BaseViewModelWithAgendaItem<M>,
} BaseViewModelWithListOfSpeakers<M> {}
public get agenda_item_id(): number { applyMixins(BaseViewModelWithAgendaItemAndListOfSpeakers, [
return this._model.agenda_item_id; BaseViewModelWithAgendaItem,
} BaseViewModelWithListOfSpeakers
]);
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;
}

View File

@ -13,9 +13,11 @@ export function isBaseViewModelWithAgendaItem(obj: any): obj is BaseViewModelWit
isSearchable(model) && isSearchable(model) &&
model.getAgendaSlideTitle !== undefined && model.getAgendaSlideTitle !== undefined &&
model.getAgendaListTitle !== undefined && model.getAgendaListTitle !== undefined &&
model.getAgendaSubtitle !== undefined &&
model.getCSVExportText !== undefined && model.getCSVExportText !== undefined &&
model.agendaItem !== undefined && model.item !== undefined &&
model.agenda_item_id !== 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. * Describes a base class for view models.
*/ */
export interface IBaseViewModelWithAgendaItem<M extends BaseModelWithAgendaItem = any> export interface BaseViewModelWithAgendaItem<M extends BaseModelWithAgendaItem = any>
extends BaseProjectableViewModel<M>, extends BaseProjectableViewModel<M>,
DetailNavigable, DetailNavigable,
Searchable { Searchable {
agendaItem: any | null; item: any | null;
agenda_item_id: number;
agenda_item_number: string | null;
/** /**
* @returns the agenda title * @returns the agenda title
@ -46,21 +44,10 @@ export interface IBaseViewModelWithAgendaItem<M extends BaseModelWithAgendaItem
*/ */
getAgendaListTitle: () => string; getAgendaListTitle: () => string;
/**
* @return an optional subtitle for the agenda.
*/
getAgendaSubtitle: () => string | null;
/** /**
* @return the agenda title with the verbose name of the content object * @return the agenda title with the verbose name of the content object
*/ */
getAgendaListTitleWithoutItemNumber: () => string; 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<M extends BaseModelWithAgendaItem
* *
* TODO: Resolve circular dependencies with `ViewItem` to avoid `any`. * TODO: Resolve circular dependencies with `ViewItem` to avoid `any`.
*/ */
export abstract class BaseViewModelWithAgendaItem<M extends BaseModelWithAgendaItem = any> export abstract class BaseViewModelWithAgendaItem<
extends BaseProjectableViewModel<M> M extends BaseModelWithAgendaItem = any
implements IBaseViewModelWithAgendaItem<M> { > extends BaseProjectableViewModel<M> {
protected _item?: any;
public get agendaItem(): any | null {
return this._item;
}
public get agenda_item_id(): number {
return this._model.agenda_item_id;
}
public get agenda_item_number(): string | null { public get agenda_item_number(): string | null {
return this.agendaItem && this.agendaItem.itemNumber ? this.agendaItem.itemNumber : null; return this.item && this.item.item_number ? this.item.item_number : 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
} }
/** /**

View File

@ -13,13 +13,14 @@ export abstract class BaseViewModelWithContentObject<
M extends BaseModelWithContentObject = any, M extends BaseModelWithContentObject = any,
C extends BaseViewModel = any C extends BaseViewModel = any
> extends BaseViewModel<M> { > extends BaseViewModel<M> {
protected _contentObject?: C;
public get contentObjectData(): ContentObject { public get contentObjectData(): ContentObject {
return this.getModel().content_object; return this.getModel().content_object;
} }
}
public get contentObject(): C | null { export interface BaseViewModelWithContentObject<
return this._contentObject; M extends BaseModelWithContentObject = any,
} C extends BaseViewModel = any
> {
contentObject: C | null;
} }

View File

@ -9,50 +9,19 @@ export function isBaseViewModelWithListOfSpeakers(obj: any): obj is BaseViewMode
isDetailNavigable(model) && isDetailNavigable(model) &&
model.getListOfSpeakersTitle !== undefined && model.getListOfSpeakersTitle !== undefined &&
model.listOfSpeakers !== undefined && model.listOfSpeakers !== undefined &&
model.list_of_speakers_id !== undefined model.getModel !== undefined &&
model.getModel().list_of_speakers_id !== undefined
); );
} }
/** export interface BaseViewModelWithListOfSpeakers<M extends BaseModelWithListOfSpeakers = any> extends DetailNavigable {
* Describes a base view model with a list of speakers.
*/
export interface IBaseViewModelWithListOfSpeakers<M extends BaseModelWithListOfSpeakers = any>
extends BaseProjectableViewModel<M>,
DetailNavigable {
listOfSpeakers: any | null; listOfSpeakers: any | null;
list_of_speakers_id: number;
getListOfSpeakersTitle: () => string; getListOfSpeakersTitle: () => string;
getListOfSpeakersSlideTitle: () => string; getListOfSpeakersSlideTitle: () => string;
} }
/** export abstract class BaseViewModelWithListOfSpeakers<
* Base view model class for models with a list of speakers. M extends BaseModelWithListOfSpeakers = any
* > extends BaseProjectableViewModel<M> {
* TODO: Resolve circular dependencies with `ViewListOfSpeakers` to avoid `any`.
*/
export abstract class BaseViewModelWithListOfSpeakers<M extends BaseModelWithListOfSpeakers = any>
extends BaseProjectableViewModel<M>
implements IBaseViewModelWithListOfSpeakers<M> {
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
}
public abstract getDetailStateURL(): string; public abstract getDetailStateURL(): string;
} }

View File

@ -11,47 +11,16 @@ export interface ViewModelConstructor<T extends BaseViewModel> {
} }
/** /**
* Base class for view models. alls view models should have titles. * Base class for view models.
*/ */
export abstract class BaseViewModel<M extends BaseModel = any> implements Displayable, Identifiable, Collection { export abstract class BaseViewModel<M extends BaseModel = any> {
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;
}
/** /**
* @returns the element id of the model * @returns the element id of the model
*/ */
public get elementId(): string { 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 collectionString
* @param model * @param model
@ -64,8 +33,25 @@ export abstract class BaseViewModel<M extends BaseModel = any> implements Displa
public getModel(): M { public getModel(): M {
return this._model; return this._model;
} }
public toString(): string { public toString(): string {
return this.getTitle(); return this.getTitle();
} }
public toJSON(): M {
return this.getModel();
}
public getUpdatedModel(update: Partial<M>): M {
return this.getModel().getUpdatedVersion(update);
}
}
export interface BaseViewModel<M extends BaseModel = any> 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;
} }

View File

@ -98,7 +98,7 @@
</div> </div>
<mat-divider [vertical]="true"></mat-divider> <mat-divider [vertical]="true"></mat-divider>
<div *ngIf="showPreview" class="result-preview flex-1"> <div *ngIf="showPreview" class="result-preview flex-1">
<div *ngIf="!!selectedModel"> <div *ngIf="selectedModel">
<os-preview [viewModel]="selectedModel"> </os-preview> <os-preview [viewModel]="selectedModel"> </os-preview>
</div> </div>
</div> </div>

View File

@ -376,7 +376,7 @@ export class SuperSearchComponent implements OnInit {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }
if (!!this.selectedModel) { if (this.selectedModel) {
if (event.key === 'Enter') { if (event.key === 'Enter') {
this.viewResult(this.selectedModel); this.viewResult(this.selectedModel);
} }

View File

@ -1,5 +1,5 @@
import { SearchRepresentation } from 'app/core/ui-services/search.service'; 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 { BaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { Searchable } from 'app/site/base/searchable'; import { Searchable } from 'app/site/base/searchable';
@ -18,72 +18,12 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
public static COLLECTIONSTRING = Mediafile.COLLECTIONSTRING; public static COLLECTIONSTRING = Mediafile.COLLECTIONSTRING;
protected _collectionString = Mediafile.COLLECTIONSTRING; protected _collectionString = Mediafile.COLLECTIONSTRING;
private _parent?: ViewMediafile;
private _access_groups?: ViewGroup[];
private _inherited_access_groups?: ViewGroup[];
public get mediafile(): Mediafile { public get mediafile(): Mediafile {
return this._model; 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 { public get filename(): string {
return this.mediafile.title; return this.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;
} }
public get type(): string | null { public get type(): string | null {
@ -105,7 +45,7 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
{ key: 'Path', value: this.path }, { key: 'Path', value: this.path },
{ key: 'Type', value: type }, { key: 'Type', value: type },
{ key: 'Timestamp', value: this.timestamp }, { key: 'Timestamp', value: this.timestamp },
{ key: 'Size', value: this.size ? this.size : '0' } { key: 'Size', value: this.filesize ? this.filesize : '0' }
]; ];
return { return {
properties, properties,
@ -114,6 +54,10 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
}; };
} }
public get url(): string {
return this.mediafile.url;
}
public getDetailStateURL(): string { public getDetailStateURL(): string {
return this.is_directory ? ('/mediafiles/files/' + this.path).slice(0, -1) : this.url; return this.is_directory ? ('/mediafiles/files/' + this.path).slice(0, -1) : this.url;
} }
@ -205,3 +149,9 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
} }
} }
} }
interface IMediafileRelations {
parent?: ViewMediafile;
access_groups?: ViewGroup[];
inherited_access_groups?: ViewGroup[];
}
export interface ViewMediafile extends MediafileWithoutNestedModels, IMediafileRelations {}

View File

@ -27,7 +27,7 @@ export class MediafilesSortListService extends BaseSortListService<ViewMediafile
label: this.translate.instant('Type') label: this.translate.instant('Type')
}, },
{ {
property: 'size', property: 'filesize',
label: this.translate.instant('Size') label: this.translate.instant('Size')
} }
]; ];

View File

@ -20,18 +20,10 @@ export class ViewCategory extends BaseViewModel<Category> implements CategoryTit
public static COLLECTIONSTRING = Category.COLLECTIONSTRING; public static COLLECTIONSTRING = Category.COLLECTIONSTRING;
protected _collectionString = Category.COLLECTIONSTRING; protected _collectionString = Category.COLLECTIONSTRING;
private _parent?: ViewCategory;
private _children?: ViewCategory[];
private _motions?: ViewMotion[];
public get category(): Category { public get category(): Category {
return this._model; return this._model;
} }
public get parent(): ViewCategory | null {
return this._parent;
}
public get oldestParent(): ViewCategory { public get oldestParent(): ViewCategory {
if (!this.parent_id) { if (!this.parent_id) {
return this; return this;
@ -40,34 +32,6 @@ export class ViewCategory extends BaseViewModel<Category> 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 { public get prefixedName(): string {
return this.prefix ? this.prefix + ' - ' + this.name : this.name; return this.prefix ? this.prefix + ' - ' + this.name : this.name;
} }
@ -132,3 +96,9 @@ export class ViewCategory extends BaseViewModel<Category> implements CategoryTit
} }
} }
} }
interface ICategoryRelations {
parent?: ViewCategory;
children?: ViewCategory[];
motions?: ViewMotion[];
}
export interface ViewCategory extends Category, ICategoryRelations {}

View File

@ -33,11 +33,4 @@ export class ViewCreateMotion extends ViewMotion {
public getVerboseName = () => { public getVerboseName = () => {
throw new Error('This should not be used'); throw new Error('This should not be used');
}; };
/**
* Duplicate this motion into a copy of itself
*/
public copy(): ViewCreateMotion {
return new ViewCreateMotion(this._model);
}
} }

View File

@ -19,22 +19,9 @@ export class ViewMotionBlock extends BaseViewModelWithAgendaItemAndListOfSpeaker
public static COLLECTIONSTRING = MotionBlock.COLLECTIONSTRING; public static COLLECTIONSTRING = MotionBlock.COLLECTIONSTRING;
protected _collectionString = MotionBlock.COLLECTIONSTRING; protected _collectionString = MotionBlock.COLLECTIONSTRING;
private _motions?: ViewMotion[];
public get motionBlock(): MotionBlock { public get motionBlock(): MotionBlock {
return this._model; 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 * Formats the category for search
@ -67,3 +54,8 @@ export class ViewMotionBlock extends BaseViewModelWithAgendaItemAndListOfSpeaker
}; };
} }
} }
interface IMotionBLockRelations {
motions?: ViewMotion[];
}
export interface ViewMotionBlock extends MotionBlock, IMotionBLockRelations {}

View File

@ -18,69 +18,13 @@ export class ViewMotionCommentSection extends BaseViewModel<MotionCommentSection
public static COLLECTIONSTRING = MotionCommentSection.COLLECTIONSTRING; public static COLLECTIONSTRING = MotionCommentSection.COLLECTIONSTRING;
protected _collectionString = MotionCommentSection.COLLECTIONSTRING; protected _collectionString = MotionCommentSection.COLLECTIONSTRING;
private _read_groups: ViewGroup[];
private _write_groups: ViewGroup[];
public get section(): MotionCommentSection { public get section(): MotionCommentSection {
return this._model; return this._model;
} }
public get id(): number {
return this.section.id;
} }
public get name(): string { interface IMotionCommentSectionRelations {
return this.section.name; read_groups: ViewGroup[];
} write_groups: ViewGroup[];
public get read_groups_id(): number[] {
return this.section.read_groups_id;
}
public get write_groups_id(): number[] {
return this.section.write_groups_id;
}
public get read_groups(): ViewGroup[] {
return this._read_groups;
}
public get write_groups(): ViewGroup[] {
return this._write_groups;
}
public get weight(): number {
return this.section.weight;
}
/**
* TODO: Where is this needed? Try to avoid this.
*/
public set name(name: string) {
this._model.name = name;
}
/**
* Updates the local objects if required
* @param section
*/
public updateDependencies(update: BaseViewModel): void {
if (update instanceof ViewGroup) {
if (this.section.read_groups_id.includes(update.id)) {
const groupIndex = this.read_groups.findIndex(group => 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;
}
}
}
}
} }
export interface ViewMotionCommentSection extends MotionCommentSection, IMotionCommentSectionRelations {}

View File

@ -2,7 +2,7 @@ import { _ } from 'app/core/translate/translation-marker';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { DiffLinesInParagraph } from 'app/core/ui-services/diff.service'; import { DiffLinesInParagraph } from 'app/core/ui-services/diff.service';
import { SearchProperty, SearchRepresentation } from 'app/core/ui-services/search.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 { PersonalNoteContent } from 'app/shared/models/users/personal-note';
import { TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item'; import { TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item';
import { BaseViewModelWithAgendaItemAndListOfSpeakers } from 'app/site/base/base-view-model-with-agenda-item-and-list-of-speakers'; import { BaseViewModelWithAgendaItemAndListOfSpeakers } from 'app/site/base/base-view-model-with-agenda-item-and-list-of-speakers';
@ -71,176 +71,26 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
return this._model; return this._model;
} }
public get category(): ViewCategory | null {
return this._category;
}
public get state(): ViewState | null {
return this._state;
}
public get recommendation(): ViewState | null {
return this._recommendation;
}
public get submitters(): ViewSubmitter[] {
return this._submitters || [];
}
public get submittersAsUsers(): ViewUser[] { public get submittersAsUsers(): ViewUser[] {
return this.submitters.map(submitter => submitter.user); return (this.submitters || []).map(submitter => 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;
} }
public get identifierOrTitle(): string { public get identifierOrTitle(): string {
return this.identifier ? this.identifier : this.title; 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[] { public get possibleRecommendations(): ViewState[] {
return this.workflow ? this.workflow.states.filter(state => state.recommendation_label !== undefined) : null; return this.workflow ? this.workflow.states.filter(state => state.recommendation_label !== undefined) : null;
} }
public get origin(): string { public get agenda_type(): number | null {
return this.motion.origin; return this.item ? this.item.type : null;
} }
public get agenda_type(): number { public get speakerAmount(): number | null {
return this.agendaItem ? this.agendaItem.type : null;
}
public get motion_block_id(): number {
return this.motion.motion_block_id;
}
public get speakerAmount(): number {
return this.listOfSpeakers ? this.listOfSpeakers.waitingSpeakerAmount : 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 * @returns the creation date as Date object
*/ */
@ -331,20 +181,6 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
return this.state ? this.state.css_class : ''; return this.state ? this.state.css_class : '';
} }
/**
* getter to access diff lines
*/
public get diffLines(): DiffLinesInParagraph[] {
if (!this.parent_id) {
throw new Error('No parent No diff');
}
return this._diffLines;
}
public set diffLines(value: DiffLinesInParagraph[]) {
this._diffLines = value;
}
/** /**
* Determine if a motion has a parent at all * Determine if a motion has a parent at all
*/ */
@ -359,20 +195,6 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
return !!this.amendments && !!this.amendments.length; return !!this.amendments && !!this.amendments.length;
} }
/**
* Determine if the motion has parents, is a parent of neither
*/
public get amendmentType(): number {
if (this.hasAmendments) {
return AmendmentType.Parent;
} else if (this.hasParent) {
return AmendmentType.Amendment;
} else {
// not any amendment
return 0;
}
}
/** /**
* Get the number of the first diff line, in case a motion is an amendment * Get the number of the first diff line, in case a motion is an amendment
*/ */
@ -386,19 +208,6 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
public static COLLECTIONSTRING = Motion.COLLECTIONSTRING; public static COLLECTIONSTRING = Motion.COLLECTIONSTRING;
protected _collectionString = Motion.COLLECTIONSTRING; protected _collectionString = Motion.COLLECTIONSTRING;
protected _category?: ViewCategory;
protected _submitters?: ViewSubmitter[];
protected _supporters?: ViewUser[];
protected _workflow?: ViewWorkflow;
protected _state?: ViewState;
protected _recommendation?: ViewState;
protected _motion_block?: ViewMotionBlock;
protected _attachments?: ViewMediafile[];
protected _tags?: ViewTag[];
protected _parent?: ViewMotion;
protected _amendments?: ViewMotion[];
protected _changeRecommendations?: ViewMotionChangeRecommendation[];
protected _diffLines?: DiffLinesInParagraph[];
public personalNote?: PersonalNoteContent; public personalNote?: PersonalNoteContent;
// This is set by the repository // This is set by the repository
@ -412,7 +221,7 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
* @return The lines of the amendment * @return The lines of the amendment
*/ */
public getChangeLines(): string { public getChangeLines(): string {
if (!!this.diffLines) { if (this.diffLines) {
return this.diffLines return this.diffLines
.map(diffLine => { .map(diffLine => {
if (diffLine.diffLineTo === diffLine.diffLineFrom + 1) { if (diffLine.diffLineTo === diffLine.diffLineFrom + 1) {
@ -521,7 +330,7 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
* specified by amendment_paragraphs-array * specified by amendment_paragraphs-array
*/ */
public isParagraphBasedAmendment(): boolean { public isParagraphBasedAmendment(): boolean {
return this.amendment_paragraphs.length > 0; return this.amendment_paragraphs && this.amendment_paragraphs.length > 0;
} }
public getSlide(configService: ConfigService): ProjectorElementBuildDeskriptor { public getSlide(configService: ConfigService): ProjectorElementBuildDeskriptor {
@ -554,11 +363,22 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
getDialogTitle: this.getAgendaSlideTitle getDialogTitle: this.getAgendaSlideTitle
}; };
} }
}
/** interface TIMotionRelations {
* Duplicate this motion into a copy of itself category?: ViewCategory;
*/ submitters: ViewSubmitter[];
public copy(): ViewMotion { supporters?: ViewUser[];
return new ViewMotion(this._model); workflow?: ViewWorkflow;
} state?: ViewState;
recommendation?: ViewState;
motion_block?: ViewMotionBlock;
attachments?: ViewMediafile[];
tags?: ViewTag[];
parent?: ViewMotion;
amendments?: ViewMotion[];
changeRecommendations?: ViewMotionChangeRecommendation[];
diffLines?: DiffLinesInParagraph[];
} }
export interface ViewMotion extends MotionWithoutNestedModels, TIMotionRelations {}

View File

@ -1,4 +1,4 @@
import { MergeAmendment, State } from 'app/shared/models/motions/state'; import { State } from 'app/shared/models/motions/state';
import { BaseViewModel } from '../../base/base-view-model'; import { BaseViewModel } from '../../base/base-view-model';
import { ViewWorkflow } from './view-workflow'; import { ViewWorkflow } from './view-workflow';
@ -14,83 +14,22 @@ export class ViewState extends BaseViewModel<State> implements StateTitleInforma
public static COLLECTIONSTRING = State.COLLECTIONSTRING; public static COLLECTIONSTRING = State.COLLECTIONSTRING;
protected _collectionString = State.COLLECTIONSTRING; protected _collectionString = State.COLLECTIONSTRING;
private _next_states?: ViewState[];
public _workflow?: ViewWorkflow;
public get state(): State { public get state(): State {
return this._model; 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 { public get isFinalState(): boolean {
return !this.next_states_id || this.next_states_id.length === 0; return (
!this.next_states_id ||
!this.next_states_id.length ||
(this.next_states_id.length === 1 && this.next_states_id[0] === 0)
);
}
} }
public get previous_states(): ViewState[] { interface IStateRelations {
if (!this.workflow) { next_states?: ViewState[];
return []; previous_states?: ViewState[];
} workflow?: ViewWorkflow;
return this.workflow.states.filter(state => {
return state.next_states_id.includes(this.id);
});
}
} }
export interface ViewState extends State, IStateRelations {}

View File

@ -23,18 +23,6 @@ export class ViewStatuteParagraph extends BaseViewModel<StatuteParagraph>
return this._model; 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 { public formatForSearch(): SearchRepresentation {
return { properties: [{ key: 'Title', value: this.getTitle() }], searchValue: [this.getTitle()] }; return { properties: [{ key: 'Title', value: this.getTitle() }], searchValue: [this.getTitle()] };
} }
@ -43,3 +31,4 @@ export class ViewStatuteParagraph extends BaseViewModel<StatuteParagraph>
return '/motions/statute-paragraphs'; return '/motions/statute-paragraphs';
} }
} }
export interface ViewStatuteParagraph extends StatuteParagraph {}

View File

@ -6,37 +6,15 @@ export class ViewSubmitter extends BaseViewModel<Submitter> {
public static COLLECTIONSTRING = Submitter.COLLECTIONSTRING; public static COLLECTIONSTRING = Submitter.COLLECTIONSTRING;
protected _collectionString = Submitter.COLLECTIONSTRING; protected _collectionString = Submitter.COLLECTIONSTRING;
private _user?: ViewUser;
public get submitter(): Submitter { public get submitter(): Submitter {
return this._model; 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 = () => { public getListTitle = () => {
return this.getTitle(); return this.getTitle();
}; };
} }
interface ISubmitterRelations {
user: ViewUser;
}
export interface ViewSubmitter extends Submitter, ISubmitterRelations {}

View File

@ -14,30 +14,12 @@ export class ViewWorkflow extends BaseViewModel<Workflow> implements WorkflowTit
public static COLLECTIONSTRING = Workflow.COLLECTIONSTRING; public static COLLECTIONSTRING = Workflow.COLLECTIONSTRING;
protected _collectionString = Workflow.COLLECTIONSTRING; protected _collectionString = Workflow.COLLECTIONSTRING;
private _states?: ViewState[];
private _first_state?: ViewState;
public get workflow(): Workflow { public get workflow(): Workflow {
return this._model; 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 {}

View File

@ -110,7 +110,7 @@ export class AmendmentListComponent extends BaseListViewComponent<ViewMotion> im
public ngOnInit(): void { public ngOnInit(): void {
// determine if a paramter exists. // 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 // set the parentMotion observable. This will "only" fire
// if there is a subscription to the parent motion // if there is a subscription to the parent motion
this.parentMotion = this.route.paramMap.pipe( this.parentMotion = this.route.paramMap.pipe(
@ -133,7 +133,7 @@ export class AmendmentListComponent extends BaseListViewComponent<ViewMotion> im
*/ */
public getAmendmentSummary(amendment: ViewMotion): string { public getAmendmentSummary(amendment: ViewMotion): string {
const diffLines = amendment.diffLines; const diffLines = amendment.diffLines;
if (!!diffLines) { if (diffLines) {
return diffLines return diffLines
.map(diffLine => { .map(diffLine => {
return this.linenumberingService.stripLineNumbers(diffLine.text); return this.linenumberingService.stripLineNumbers(diffLine.text);

View File

@ -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 title = this.translate.instant('Are you sure you want to renumber all motions of this category?');
const content = this.selectedCategory.getTitle(); const content = this.selectedCategory.getTitle();
if (await this.promptService.open(title, content)) { 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);
} }
} }
} }

View File

@ -118,11 +118,11 @@
<os-projector-button *ngIf="block" [object]="block" [menuItem]="true"></os-projector-button> <os-projector-button *ngIf="block" [object]="block" [menuItem]="true"></os-projector-button>
<div *osPerms="'agenda.can_manage'"> <div *osPerms="'agenda.can_manage'">
<button mat-menu-item (click)="addToAgenda()" *ngIf="block && !block.agendaItem"> <button mat-menu-item (click)="addToAgenda()" *ngIf="block && !block.item">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
<span translate>Add to agenda</span> <span translate>Add to agenda</span>
</button> </button>
<button mat-menu-item (click)="removeFromAgenda()" *ngIf="block && block.agendaItem"> <button mat-menu-item (click)="removeFromAgenda()" *ngIf="block && block.item">
<mat-icon>remove</mat-icon> <mat-icon>remove</mat-icon>
<span translate>Remove from agenda</span> <span translate>Remove from agenda</span>
</button> </button>

View File

@ -254,10 +254,10 @@ export class MotionBlockDetailComponent extends BaseListViewComponent<ViewMotion
} }
public addToAgenda(): void { public addToAgenda(): void {
this.itemRepo.addItemToAgenda(this.block).then(null, this.raiseError); this.itemRepo.addItemToAgenda(this.block).catch(this.raiseError);
} }
public removeFromAgenda(): void { public removeFromAgenda(): void {
this.itemRepo.removeFromAgenda(this.block.agendaItem).then(null, this.raiseError); this.itemRepo.removeFromAgenda(this.block.item).catch(this.raiseError);
} }
} }

View File

@ -53,6 +53,6 @@ export class MotionCommentSectionSortComponent extends BaseViewComponent impleme
* @param commentsInOrder * @param commentsInOrder
*/ */
public onSortingChange(commentsInOrder: ViewMotionCommentSection[]): void { public onSortingChange(commentsInOrder: ViewMotionCommentSection[]): void {
this.repo.sortCommentSections(commentsInOrder).then(null, this.raiseError); this.repo.sortCommentSections(commentsInOrder).catch(this.raiseError);
} }
} }

View File

@ -282,7 +282,7 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
* @param {boolean} internal * @param {boolean} internal
*/ */
public setInternal(change: ViewMotionChangeRecommendation, internal: boolean): void { public setInternal(change: ViewMotionChangeRecommendation, internal: boolean): void {
this.recoRepo.setInternal(change, internal).then(null, this.raiseError); this.recoRepo.setInternal(change, internal).catch(this.raiseError);
} }
/** /**
@ -297,7 +297,7 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
$event.preventDefault(); $event.preventDefault();
const title = this.translate.instant('Are you sure you want to delete this change recommendation?'); const title = this.translate.instant('Are you sure you want to delete this change recommendation?');
if (await this.promptService.open(title)) { if (await this.promptService.open(title)) {
this.recoRepo.delete(reco).then(null, this.raiseError); this.recoRepo.delete(reco).catch(this.raiseError);
} }
} }

View File

@ -73,11 +73,11 @@
></os-projector-button> ></os-projector-button>
<!-- Add/remove to/from agenda --> <!-- Add/remove to/from agenda -->
<div *osPerms="'agenda.can_manage'"> <div *osPerms="'agenda.can_manage'">
<button mat-menu-item (click)="addToAgenda()" *ngIf="motion && !motion.agendaItem"> <button mat-menu-item (click)="addToAgenda()" *ngIf="motion && !motion.item">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
<span translate>Add to agenda</span> <span translate>Add to agenda</span>
</button> </button>
<button mat-menu-item (click)="removeFromAgenda()" *ngIf="motion && motion.agendaItem"> <button mat-menu-item (click)="removeFromAgenda()" *ngIf="motion && motion.item">
<mat-icon>remove</mat-icon> <mat-icon>remove</mat-icon>
<span translate>Remove from agenda</span> <span translate>Remove from agenda</span>
</button> </button>
@ -614,7 +614,6 @@
autofocus autofocus
placeholder="{{ 'Identifier' | translate }}" placeholder="{{ 'Identifier' | translate }}"
formControlName="identifier" formControlName="identifier"
[value]="motionCopy.identifier || ''"
/> />
</mat-form-field> </mat-form-field>
</div> </div>
@ -626,7 +625,6 @@
matInput matInput
placeholder="{{ 'Title' | translate }}" placeholder="{{ 'Title' | translate }}"
formControlName="title" formControlName="title"
[value]="motionCopy.title"
required required
/> />
<mat-error>{{ 'The title is required' | translate }}</mat-error> <mat-error>{{ 'The title is required' | translate }}</mat-error>
@ -844,7 +842,6 @@
matInput matInput
placeholder="{{ 'Origin' | translate }}" placeholder="{{ 'Origin' | translate }}"
formControlName="origin" formControlName="origin"
[value]="motionCopy.origin"
/> />
</mat-form-field> </mat-form-field>
</div> </div>

View File

@ -203,11 +203,6 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
*/ */
public amendmentsEnabled: boolean; public amendmentsEnabled: boolean;
/**
* Copy of the motion that the user might edit
*/
public motionCopy: ViewMotion;
/** /**
* All change recommendations to this motion * All change recommendations to this motion
*/ */
@ -645,12 +640,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
// new motion // new motion
this.newMotion = true; this.newMotion = true;
this.editMotion = true; this.editMotion = true;
// prevent 'undefined' to appear in the ui const defaultMotion: Partial<CreateMotion> = {};
const defaultMotion: Partial<CreateMotion> = {
title: '',
origin: '',
identifier: ''
};
if (this.route.snapshot.queryParams.parent) { if (this.route.snapshot.queryParams.parent) {
this.amendmentEdit = true; this.amendmentEdit = true;
const parentMotion = this.repo.getViewModel(this.route.snapshot.queryParams.parent); 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.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 { private updateMotionFromForm(): void {
const newMotionValues = { ...this.contentForm.value }; const newMotionValues = { ...this.contentForm.value };
this.updateMotion(newMotionValues, this.motionCopy).then(() => { this.updateMotion(newMotionValues, this.motion).then(() => {
this.editMotion = false; this.editMotion = false;
this.amendmentEdit = false; this.amendmentEdit = false;
}, this.raiseError); }, this.raiseError);
@ -1155,8 +1144,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
public setEditMode(mode: boolean): void { public setEditMode(mode: boolean): void {
this.editMotion = mode; this.editMotion = mode;
if (mode) { if (mode) {
this.motionCopy = this.motion.copy(); this.patchForm(this.motion);
this.patchForm(this.motionCopy);
this.editNotificationSubscription = this.listenToEditNotification(); this.editNotificationSubscription = this.listenToEditNotification();
this.sendEditNotification(MotionEditNotificationType.TYPE_BEGIN_EDITING_MOTION); this.sendEditNotification(MotionEditNotificationType.TYPE_BEGIN_EDITING_MOTION);
} }
@ -1247,14 +1235,14 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
* Supports the motion (as requested user) * Supports the motion (as requested user)
*/ */
public support(): void { public support(): void {
this.repo.support(this.motion).then(null, this.raiseError); this.repo.support(this.motion).catch(this.raiseError);
} }
/** /**
* Unsupports the motion * Unsupports the motion
*/ */
public unsupport(): void { 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 * @param id Motion state id
*/ */
public setState(id: number): void { 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. * @param unsubscriptionReason The reason for the unsubscription.
*/ */
private unsubscribeEditNotifications(unsubscriptionReason: MotionEditNotificationType): void { private unsubscribeEditNotifications(unsubscriptionReason: MotionEditNotificationType): void {
if (!!this.editNotificationSubscription && !this.editNotificationSubscription.closed) { if (this.editNotificationSubscription && !this.editNotificationSubscription.closed) {
this.sendEditNotification(unsubscriptionReason); this.sendEditNotification(unsubscriptionReason);
this.closeSnackBar(); this.closeSnackBar();
this.editNotificationSubscription.unsubscribe(); this.editNotificationSubscription.unsubscribe();
@ -1529,7 +1517,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit,
} else { } else {
this.motion.personalNote.star = !this.motion.personalNote.star; 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 { public addToAgenda(): void {
this.itemRepo.addItemToAgenda(this.motion).then(null, this.raiseError); this.itemRepo.addItemToAgenda(this.motion).catch(this.raiseError);
} }
public removeFromAgenda(): void { public removeFromAgenda(): void {
this.itemRepo.removeFromAgenda(this.motion.agendaItem).then(null, this.raiseError); this.itemRepo.removeFromAgenda(this.motion.item).catch(this.raiseError);
} }
} }

View File

@ -289,7 +289,7 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> imple
const localCategories = new Set<ViewCategory>(); const localCategories = new Set<ViewCategory>();
for (const motion of motions) { for (const motion of motions) {
if (!motion.category_id) { if (!motion.category) {
motionsWithoutCategory++; motionsWithoutCategory++;
} else { } else {
localCategories.add(motion.category.oldestParent); localCategories.add(motion.category.oldestParent);
@ -429,7 +429,7 @@ export class MotionListComponent extends BaseListViewComponent<ViewMotion> imple
}; };
// TODO: "only update if different" was another repo-todo // TODO: "only update if different" was another repo-todo
if (!Object.keys(partialUpdate).every(key => partialUpdate[key] === undefined)) { 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);
} }
} }
}); });

View File

@ -177,7 +177,7 @@ export class MotionExportDialogComponent implements OnInit {
* @param nextState The next state the button will assume. * @param nextState The next state the button will assume.
*/ */
private changeStateOfButton(button: MatButtonToggle, nextState: boolean): void { private changeStateOfButton(button: MatButtonToggle, nextState: boolean): void {
if (!!button) { if (button) {
button.disabled = nextState; button.disabled = nextState;
} }
} }
@ -245,7 +245,7 @@ export class MotionExportDialogComponent implements OnInit {
// restore selection or set default // restore selection or set default
this.store.get<MotionExportInfo>('motion_export_selection').then(restored => { this.store.get<MotionExportInfo>('motion_export_selection').then(restored => {
if (!!restored) { if (restored) {
this.exportForm.patchValue(restored); this.exportForm.patchValue(restored);
} else { } else {
this.exportForm.patchValue(this.defaults); this.exportForm.patchValue(this.defaults);

View File

@ -90,7 +90,7 @@ export class AmendmentFilterListService extends MotionFilterListService {
* Function to define a new storage key by parent id * Function to define a new storage key by parent id
*/ */
private updateStorageKey(): void { private updateStorageKey(): void {
if (!!this._parentMotionId) { if (this._parentMotionId) {
this.storageKey = `${this.keyPrefix}_parentId_${this._parentMotionId}`; this.storageKey = `${this.keyPrefix}_parentId_${this._parentMotionId}`;
} else { } else {
this.storageKey = this.keyPrefix; this.storageKey = this.keyPrefix;
@ -104,7 +104,7 @@ export class AmendmentFilterListService extends MotionFilterListService {
*/ */
protected preFilter(motions: ViewMotion[]): ViewMotion[] { protected preFilter(motions: ViewMotion[]): ViewMotion[] {
return motions.filter(motion => { return motions.filter(motion => {
if (!!this._parentMotionId) { if (this._parentMotionId) {
return motion.parent_id === this._parentMotionId; return motion.parent_id === this._parentMotionId;
} else { } else {
return !!motion.parent_id; return !!motion.parent_id;

View File

@ -46,7 +46,7 @@ export class MotionExportService {
if (!exportInfo) { if (!exportInfo) {
return; return;
} }
if (!!exportInfo.format) { if (exportInfo.format) {
if (exportInfo.format === ExportFileFormat.PDF) { if (exportInfo.format === ExportFileFormat.PDF) {
try { try {
this.pdfExport.exportMotionCatalog(data, exportInfo); this.pdfExport.exportMotionCatalog(data, exportInfo);

View File

@ -201,7 +201,7 @@ export class MotionMultiselectService {
let requestData = null; let requestData = null;
if (selectedChoice.action === choices[0]) { if (selectedChoice.action === choices[0]) {
requestData = motions.map(motion => { 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 submitterIds = submitterIds.filter((id, index, self) => self.indexOf(id) === index); // remove duplicates
return { return {
id: motion.id, id: motion.id,
@ -211,7 +211,7 @@ export class MotionMultiselectService {
} else if (selectedChoice.action === choices[1]) { } else if (selectedChoice.action === choices[1]) {
requestData = motions.map(motion => { requestData = motions.map(motion => {
const submitterIdsToRemove = selectedChoice.items as number[]; 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 { return {
id: motion.id, id: motion.id,
submitters: submitterIds submitters: submitterIds

View File

@ -312,7 +312,7 @@ export class MotionPdfService {
// category // category
if (motion.category && (!infoToExport || infoToExport.includes('category'))) { if (motion.category && (!infoToExport || infoToExport.includes('category'))) {
let categoryText = ''; let categoryText = '';
if (!!motion.category.parent) { if (motion.category.parent) {
categoryText = `${motion.category.parent.toString()}\n${this.translate.instant( categoryText = `${motion.category.parent.toString()}\n${this.translate.instant(
'Subcategory' 'Subcategory'
)}: ${motion.category.toString()}`; )}: ${motion.category.toString()}`;

View File

@ -147,11 +147,11 @@ export class MotionXlsxExportService {
data.push( data.push(
...properties.map(property => { ...properties.map(property => {
const motionProp = motion[property]; const motionProp = motion[property];
if (property === 'speakers') { /*if (property === 'speakers') {
return motion.listOfSpeakers && motion.listOfSpeakers.waitingSpeakerAmount > 0 return motion.listOfSpeakers && motion.listOfSpeakers.waitingSpeakerAmount > 0
? motion.listOfSpeakers.waitingSpeakerAmount ? motion.listOfSpeakers.waitingSpeakerAmount
: ''; : '';
} }*/
if (!motionProp) { if (!motionProp) {
return ''; return '';
} }

View File

@ -139,6 +139,6 @@ export class PresentationControlComponent extends BaseViewComponent {
private updateElement(element: MediafileProjectorElement): void { private updateElement(element: MediafileProjectorElement): void {
const idElement = this.slideManager.getIdentifialbeProjectorElement(element); 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);
} }
} }

View File

@ -125,7 +125,7 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
* @param step (optional) The amount of steps to make. * @param step (optional) The amount of steps to make.
*/ */
public scroll(direction: ScrollScaleDirection, step: number = 1): void { 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. * @param step (optional) The amount of steps to make.
*/ */
public scale(direction: ScrollScaleDirection, step: number = 1): void { 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 { 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 { 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<ProjectorElement>): void { public onSortingChange(event: CdkDragDrop<ProjectorElement>): void {
moveItemInArray(this.projector.elements_preview, event.previousIndex, event.currentIndex); 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 { public removePreviewElement(elementIndex: number): void {
this.projector.elements_preview.splice(elementIndex, 1); 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 { 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 { public getSlideTitle(element: ProjectorElement): string {
@ -182,7 +182,7 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
public unprojectCurrent(element: ProjectorElement): void { public unprojectCurrent(element: ProjectorElement): void {
const idElement = this.slideManager.getIdentifialbeProjectorElement(element); 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 { public isClosProjected(stable: boolean): boolean {

View File

@ -244,7 +244,7 @@ export class ProjectorListEntryComponent extends BaseViewComponent implements On
public async onDeleteButton(): Promise<void> { public async onDeleteButton(): Promise<void> {
const title = this.translate.instant('Are you sure you want to delete this projector?'); const title = this.translate.instant('Are you sure you want to delete this projector?');
if (await this.promptService.open(title, this.projector.name)) { 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 width: width
}; };
updateProjector.height = Math.round(width / aspectRatios[aspectRatioKey]); 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);
} }
/** /**

View File

@ -5,7 +5,7 @@
</div> </div>
</os-head-bar> </os-head-bar>
<mat-card *ngIf="!projectorToCreate && projectors && projectors.length > 1"> <mat-card *ngIf="!showCreateForm && projectors && projectors.length > 1">
<span translate> Reference projector for current list of speakers: </span>&nbsp; <span translate> Reference projector for current list of speakers: </span>&nbsp;
<mat-form-field> <mat-form-field>
<mat-select <mat-select
@ -20,7 +20,7 @@
</mat-form-field> </mat-form-field>
</mat-card> </mat-card>
<mat-card *ngIf="projectorToCreate"> <mat-card *ngIf="showCreateForm">
<mat-card-title translate>New Projector</mat-card-title> <mat-card-title translate>New Projector</mat-card-title>
<mat-card-content> <mat-card-content>
<form [formGroup]="createForm" (keydown)="keyDownFunction($event)"> <form [formGroup]="createForm" (keydown)="keyDownFunction($event)">
@ -38,7 +38,7 @@
<button mat-button (click)="create()"> <button mat-button (click)="create()">
<span translate>Create</span> <span translate>Create</span>
</button> </button>
<button mat-button (click)="projectorToCreate = null"> <button mat-button (click)="showCreateForm = false">
<span translate>Cancel</span> <span translate>Cancel</span>
</button> </button>
</mat-card-actions> </mat-card-actions>

View File

@ -35,7 +35,7 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit,
/** /**
* This member is set, if the user is creating a new projector. * This member is set, if the user is creating a new projector.
*/ */
public projectorToCreate: Projector | null; public showCreateForm = false;
/** /**
* The create form. * The create form.
@ -126,8 +126,8 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit,
* Opens the create form. * Opens the create form.
*/ */
public onPlusButton(): void { public onPlusButton(): void {
if (!this.projectorToCreate) { if (!this.showCreateForm) {
this.projectorToCreate = new Projector(); this.showCreateForm = true;
this.createForm.setValue({ name: '' }); this.createForm.setValue({ name: '' });
} }
} }
@ -136,13 +136,13 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit,
* Creates the comment section from the create form. * Creates the comment section from the create form.
*/ */
public create(): void { public create(): void {
if (this.createForm.valid && this.projectorToCreate) { if (this.createForm.valid && this.showCreateForm) {
this.projectorToCreate.patchValues(this.createForm.value as Projector); const projector: Partial<Projector> = {
this.projectorToCreate.patchValues({ name: this.createForm.value.name,
reference_projector_id: this.projectors[0].reference_projector_id reference_projector_id: this.projectors[0].reference_projector_id
}); };
this.repo.create(this.projectorToCreate).then(() => { this.repo.create(projector).then(() => {
this.projectorToCreate = null; this.showCreateForm = false;
this.cd.detectChanges(); this.cd.detectChanges();
}, this.raiseError); }, this.raiseError);
} }
@ -158,7 +158,7 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit,
this.create(); this.create();
} }
if (event.key === 'Escape') { 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 => { const promises = this.projectors.map(projector => {
return this.repo.update(update, projector); return this.repo.update(update, projector);
}); });
Promise.all(promises).then(null, this.raiseError); Promise.all(promises).catch(this.raiseError);
} }
} }

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