Use Proxies for ViewModels

Also reworked creation of assignments, motions and users.
This commit is contained in:
FinnStutzenstein 2019-09-26 14:07:33 +02:00
parent 7282c541dd
commit 7af74fb07a
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]));
});
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS, true);
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS);
await this.DS.set(elements, autoupdate.to_change_id);
this.DSUpdateManager.commit(updateSlot);
this.DSUpdateManager.commit(updateSlot, autoupdate.to_change_id, true);
}
/**
@ -130,7 +130,7 @@ export class AutoupdateService {
await this.DS.flushToStorage(autoupdate.to_change_id);
this.DSUpdateManager.commit(updateSlot);
this.DSUpdateManager.commit(updateSlot, autoupdate.to_change_id);
} else {
// autoupdate fully in the future. we are missing something!
this.requestChanges();
@ -172,7 +172,7 @@ export class AutoupdateService {
const oldChangeId = this.DS.maxChangeId;
const response = await this.websocketService.sendAndGetResponse<{}, AutoupdateFormat>('getElements', {});
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS, true);
const updateSlot = await this.DSUpdateManager.getNewUpdateSlot(this.DS);
let allModels: BaseModel[] = [];
for (const collection of Object.keys(response.changed)) {
if (this.modelMapper.isCollectionRegistered(collection)) {
@ -183,7 +183,7 @@ export class AutoupdateService {
}
await this.DS.set(allModels, response.to_change_id);
this.DSUpdateManager.commit(updateSlot);
this.DSUpdateManager.commit(updateSlot, response.to_change_id, true);
console.log(`Full update done from ${oldChangeId} to ${response.to_change_id}`);
}

View File

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

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 { BaseViewModel, ViewModelConstructor } from 'app/site/base/base-view-model';
import { ModelDescriptor, NestedModelDescriptors } from '../repositories/base-repository';
import { CacheChangeIds, RelationCacheService } from './relation-cache.service';
import {
BaseOrderedRelation,
isCustomRelationDefinition,
isGenericRelationDefinition,
isNestedRelationDefinition,
isNormalRelationDefinition,
isReverseRelationDefinition,
RelationDefinition,
ReverseRelationDefinition
RelationDefinition
} from '../definitions/relations';
import { ViewModelStoreService } from './view-model-store.service';
/**
* Manages relations between view models. This service is and should only used by the
* base repository to offload maanging relations between view models.
* base repository to offload managing relations between view models.
*/
@Injectable({
providedIn: 'root'
})
export class RelationManagerService {
public constructor(private viewModelStoreService: ViewModelStoreService) {}
public constructor(
private viewModelStoreService: ViewModelStoreService,
private relationCacheService: RelationCacheService
) {}
/**
* Sorts the array of foreign view models in the given view models for the given relation.
*/
public sortByRelation<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>(
public handleRelation<M extends BaseModel, V extends BaseViewModel>(
model: M,
viewModel: V,
relation: RelationDefinition,
initialLoading: boolean
): void {
relation: RelationDefinition
): any {
if (isNormalRelationDefinition(relation)) {
if (
(relation.type === 'M2M' || relation.type === 'O2M') &&
@ -80,22 +41,16 @@ export class RelationManagerService {
relation.foreignViewModel,
model[relation.ownIdKey]
);
viewModel['_' + relation.ownKey] = foreignViewModels;
this.sortByRelation(relation, viewModel);
if (relation.afterSetRelation) {
relation.afterSetRelation(viewModel, foreignViewModels);
}
this.sortViewModels(foreignViewModels, relation.order);
return foreignViewModels;
} else if (relation.type === 'M2O') {
const foreignViewModel = this.viewModelStoreService.get(
relation.foreignViewModel,
model[relation.ownIdKey]
);
viewModel['_' + relation.ownKey] = foreignViewModel;
if (relation.afterSetRelation) {
relation.afterSetRelation(viewModel, foreignViewModel);
return foreignViewModel;
}
}
} else if (isReverseRelationDefinition(relation) && !initialLoading) {
} else if (isReverseRelationDefinition(relation)) {
if (relation.type === 'M2M') {
const foreignViewModels = this.viewModelStoreService.filter(
relation.foreignViewModel,
@ -104,16 +59,16 @@ export class RelationManagerService {
foreignViewModel[relation.foreignIdKey].constructor === Array &&
foreignViewModel[relation.foreignIdKey].includes(model.id)
);
viewModel['_' + relation.ownKey] = foreignViewModels;
this.sortByRelation(relation, viewModel);
this.sortViewModels(foreignViewModels, relation.order);
return foreignViewModels;
} else if (relation.type === 'O2M') {
const foreignViewModels = this.viewModelStoreService.filter(
relation.foreignViewModel,
foreignViewModel =>
foreignViewModel[relation.foreignIdKey] && foreignViewModel[relation.foreignIdKey] === model.id
);
viewModel['_' + relation.ownKey] = foreignViewModels;
this.sortByRelation(relation, viewModel);
this.sortViewModels(foreignViewModels, relation.order);
return foreignViewModels;
} else if (relation.type === 'M2O') {
const foreignViewModel = this.viewModelStoreService.find(
relation.foreignViewModel,
@ -121,236 +76,156 @@ export class RelationManagerService {
_foreignViewModel[relation.foreignIdKey] &&
_foreignViewModel[relation.foreignIdKey] === model.id
);
viewModel['_' + relation.ownKey] = foreignViewModel;
return foreignViewModel;
}
} else if (isNestedRelationDefinition(relation)) {
const foreignModels = model[relation.ownKey].map(m => new relation.foreignModel(m));
const foreignViewModels: BaseViewModel[] = foreignModels.map((foreignModel: BaseModel) =>
this.createViewModel(
foreignModel,
relation.foreignViewModel,
relation.relationDefinition || [],
initialLoading
)
);
viewModel['_' + relation.ownKey] = foreignViewModels;
this.sortByRelation(relation, viewModel);
} else if (isGenericRelationDefinition(relation)) {
const contentObject = this.viewModelStoreService.get<BaseViewModel>(
model[relation.ownContentObjectDataKey].collection,
model[relation.ownContentObjectDataKey].id
);
if (contentObject && relation.isVForeign(contentObject)) {
viewModel['_' + relation.ownKey] = contentObject;
return contentObject;
}
} else if (isCustomRelationDefinition(relation)) {
relation.setRelations(model, viewModel);
return relation.get(model, viewModel);
}
}
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
* the collection via the relation.
*
* @return true, if something was updated.
* Sorts the array of foreign view models in the given view models for the given relation.
*/
public updateSingleDependencyForDeletedModel(
ownViewModel: BaseViewModel,
relation: ReverseRelationDefinition,
deletedId: number
): boolean {
// In both relations, the ownViewModel holds an array of foreignViewModels. Try to find the deleted
// foreignViewModel in this array and remove it.
if (relation.type === 'O2M' || relation.type === 'M2M') {
const ownModelArray = <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;
public sortViewModels(viewModels: BaseViewModel[], order?: string): void {
viewModels.sort((a: BaseViewModel, b: BaseViewModel) => {
if (!order || a[order] === b[order]) {
return a.id - b.id;
} else {
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 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;
return a[order] - b[order];
}
});
}
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(
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;
viewModel[modelDescriptor.ownKey] = nestedViewModels;
}
);
return viewModel;
}
}

View File

@ -53,7 +53,7 @@ export class TimeTravelService {
* @param history the desired point in the history of OpenSlides
*/
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);
const historyData: HistoryData = await this.getHistoryData(history);
@ -67,7 +67,7 @@ export class TimeTravelService {
});
await this.DS.set(allModels, 0);
this.DSUpdateManager.commit(updateSlot);
this.DSUpdateManager.commit(updateSlot, 1, true);
}
/**

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';
// All "standard" relations.
export type RelationDefinition<VForeign extends BaseViewModel = BaseViewModel> =
| NormalRelationDefinition<VForeign>
| ReverseRelationDefinition<VForeign>
| NestedRelationDefinition<VForeign>
| CustomRelationDefinition<VForeign>
| CustomRelationDefinition
| GenericRelationDefinition<VForeign>;
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.
*/
ownIdKey: string;
afterDependencyChange?: (ownViewModel: BaseViewModel, foreignViewModel: BaseViewModel) => void;
}
/**
@ -58,19 +55,16 @@ interface NormalM2MRelationDefinition<VForeign extends BaseViewModel>
extends BaseNormalRelationDefinition<VForeign>,
BaseOrderedRelation<VForeign> {
type: 'M2M';
afterSetRelation?: (ownViewModel: BaseViewModel, foreignViewModels: BaseViewModel[]) => void;
}
interface NormalO2MRelationDefinition<VForeign extends BaseViewModel>
extends BaseNormalRelationDefinition<VForeign>,
BaseOrderedRelation<VForeign> {
type: 'O2M';
afterSetRelation?: (ownViewModel: BaseViewModel, foreignViewModels: BaseViewModel[]) => void;
}
interface NormalM2ORelationDefinition<VForeign extends BaseViewModel> extends BaseNormalRelationDefinition<VForeign> {
type: 'M2O';
afterSetRelation?: (ownViewModel: BaseViewModel, foreignViewModel: BaseViewModel | null) => void;
}
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;
}
/**
* 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> {
type: 'generic';
@ -180,21 +151,19 @@ export function isGenericRelationDefinition(obj: RelationDefinition): obj is Gen
/**
* A custom relation with callbacks with things todo.
*/
interface CustomRelationDefinition<VForeign extends BaseViewModel> {
interface CustomRelationDefinition {
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;
/**
* Called, when the dependency was updated.
*/
updateDependency: (ownViewModel: BaseViewModel, foreignViewModel: VForeign) => boolean;
get: (model: BaseModel, viewModel: BaseViewModel) => any;
getCacheObjectToCheck: (viewModel: BaseViewModel) => BaseViewModel | null;
}
export function isCustomRelationDefinition(obj: RelationDefinition): obj is CustomRelationDefinition<BaseViewModel> {
export function isCustomRelationDefinition(obj: RelationDefinition): obj is CustomRelationDefinition {
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
*/
private cancelPdfCreation(): void {
if (!!this.pdfWorker) {
if (this.pdfWorker) {
this.pdfWorker.terminate();
this.pdfWorker = null;
}

View File

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

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 {
BaseViewModelWithAgendaItem,
IBaseViewModelWithAgendaItem,
isBaseViewModelWithAgendaItem
} from 'app/site/base/base-view-model-with-agenda-item';
import { ViewMotion } from 'app/site/motions/models/view-motion';
@ -129,8 +128,8 @@ export class ItemRepositoryService extends BaseHasContentObjectRepository<
*
* @returns {ViewItem} The modified item extended with the `getSubtitle()`-function.
*/
protected createViewModelWithTitles(model: Item, initialLoading: boolean): ViewItem {
const viewModel = super.createViewModelWithTitles(model, initialLoading);
protected createViewModelWithTitles(model: Item): ViewItem {
const viewModel = super.createViewModelWithTitles(model);
viewModel.getSubtitle = () => this.getSubtitle(viewModel);
return viewModel;
}
@ -153,17 +152,13 @@ export class ItemRepositoryService extends BaseHasContentObjectRepository<
* @param viewModel the view model that the update is based on
*/
public async update(update: Partial<Item>, viewModel: ViewItem): Promise<void> {
const sendUpdate = new this.baseModelCtor();
sendUpdate.patchValues(viewModel.getModel());
sendUpdate.patchValues(update);
const sendUpdate = viewModel.getUpdatedModel(update);
const clone = JSON.parse(JSON.stringify(sendUpdate));
clone.item_number = clone._itemNumber;
const restPath = `/rest/${sendUpdate.collectionString}/${sendUpdate.id}/`;
return await this.httpService.put(restPath, clone);
return await this.dataSend.updateModel(clone);
}
public async addItemToAgenda(contentObject: IBaseViewModelWithAgendaItem<any>): Promise<Identifiable> {
public async addItemToAgenda(contentObject: BaseViewModelWithAgendaItem<any>): Promise<Identifiable> {
return await this.httpService.post('/rest/agenda/item/', {
collection: contentObject.collectionString,
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 { BaseHasContentObjectRepository } from '../base-has-content-object-repository';
import { BaseIsListOfSpeakersContentObjectRepository } from '../base-is-list-of-speakers-content-object-repository';
import { NestedModelDescriptors } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { DataStoreService } from '../../core-services/data-store.service';
import { ItemRepositoryService } from './item-repository.service';
@ -36,23 +37,30 @@ const ListOfSpeakersRelations: RelationDefinition[] = [
VForeignVerbose: 'BaseViewModelWithListOfSpeakers',
ownContentObjectDataKey: 'contentObjectData',
ownKey: 'contentObject'
},
}
];
const ListOfSpeakersNestedModelDescriptors: NestedModelDescriptors = {
'agenda/list-of-speakers': [
{
type: 'nested',
ownKey: 'speakers',
foreignViewModel: ViewSpeaker,
foreignModel: Speaker,
order: 'weight',
relationDefinition: [
{
relationDefinitionsByKey: {
user: {
type: 'M2O',
ownIdKey: 'user_id',
ownKey: 'user',
foreignViewModel: ViewUser
}
]
},
titles: {
getTitle: (viewSpeaker: ViewSpeaker) => viewSpeaker.name
}
];
}
]
};
/**
* Repository service for lists of speakers
@ -96,7 +104,8 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit
translate,
relationManager,
ListOfSpeakers,
ListOfSpeakersRelations
ListOfSpeakersRelations,
ListOfSpeakersNestedModelDescriptors
);
}
@ -116,7 +125,7 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit
// TODO: This can be resolved with #4738
const item = this.itemRepo.findByContentObject(titleInformation.contentObjectData);
if (item) {
(<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);

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

View File

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

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

View File

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

View File

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

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 { CollectionStringMapperService } from '../core-services/collection-string-mapper.service';
import { DataSendService } from '../core-services/data-send.service';
import { CollectionIds, DataStoreService } from '../core-services/data-store.service';
import { DataStoreService } from '../core-services/data-store.service';
import { Identifiable } from '../../shared/models/base/identifiable';
import { OnAfterAppsLoaded } from '../definitions/on-after-apps-loaded';
import { RelationManagerService } from '../core-services/relation-manager.service';
import {
isNormalRelationDefinition,
isReverseRelationDefinition,
RelationDefinition,
ReverseRelationDefinition
} from '../definitions/relations';
import { RelationDefinition, ReverseRelationDefinition } from '../definitions/relations';
import { ViewModelStoreService } from '../core-services/view-model-store.service';
export interface ModelDescriptor<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>
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 relationsByKey: { [key: string]: RelationDefinition<BaseViewModel> } = {};
/**
* 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 relationManager: RelationManagerService,
protected baseModelCtor: ModelConstructor<M>,
protected relationDefinitions: RelationDefinition<BaseViewModel>[] = []
protected relationDefinitions: RelationDefinition<BaseViewModel>[] = [],
protected nestedModelDescriptors: NestedModelDescriptors = {}
) {
this._collectionString = baseModelCtor.COLLECTIONSTRING;
this.groupRelationsByCollections();
this.buildReverseRelationsGrouping();
this.extendRelations();
this.relationDefinitions.forEach(relation => {
this.relationsByKey[relation.ownKey] = relation;
});
// All data is piped through an auditTime of 1ms. This is to prevent massive
// updates, if e.g. an autoupdate with a lot motions come in. The result is just one
@ -130,55 +146,7 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
this.languageCollator = new Intl.Collator(this.translate.currentLang);
}
/**
* Reorders the relations to provide faster access.
*/
protected groupRelationsByCollections(): void {
this.relationDefinitions.forEach(relation => {
this._groupRelationsByCollections(relation, relation);
});
}
/**
* Recursive function for reorderung the relations.
*/
protected _groupRelationsByCollections(relation: RelationDefinition, baseRelation: RelationDefinition): void {
if (relation.type === 'nested') {
(relation.relationDefinition || []).forEach(nestedRelation => {
this._groupRelationsByCollections(nestedRelation, baseRelation);
});
} else if (
relation.type === '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;
}
});
}
protected extendRelations(): void {}
public onAfterAppsLoaded(): void {
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.
*/
public changedModels(ids: number[], initialLoading: boolean): void {
public changedModels(ids: number[]): void {
ids.forEach(id => {
this.viewModelStore[id] = this.createViewModelWithTitles(
this.DS.get(this.collectionString, id),
initialLoading
);
this.viewModelStore[id] = this.createViewModelWithTitles(this.DS.get(this.collectionString, id));
this.updateViewModelObservable(id);
});
}
@ -228,115 +193,20 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
* After creating a view model, all functions for models form the repo
* are assigned to the new view model.
*/
protected createViewModelWithTitles(model: M, initialLoading: boolean): V {
protected createViewModelWithTitles(model: M): V {
const viewModel = this.relationManager.createViewModel(
model,
this.baseViewModelCtor,
this.relationDefinitions,
initialLoading
this.relationsByKey,
this.nestedModelDescriptors
);
viewModel.getTitle = () => this.getTitle(viewModel);
viewModel.getListTitle = () => this.getListTitle(viewModel);
viewModel.getVerboseName = this.getVerboseName;
return viewModel;
}
/**
* Updates all models in this repository with all changed models.
*
* @param changedModels A mapping of collections to ids of all changed models.
* @returns if at least one model was affected.
*/
public updateDependenciesForChangedModels(changedModels: CollectionIds): boolean {
if (!this.relationDefinitions.length) {
return false;
}
// Get all viewModels from this repo once.
const ownViewModels = this.getViewModelList();
const updatedIds = [];
Object.keys(changedModels).forEach(collection => {
const dependencyChanged: boolean = Object.keys(this.relationsByCollection).includes(collection);
if (!dependencyChanged) {
return;
}
// Ok, we are affected by this collection. Update all viewModels from this repo.
const relations = this.relationsByCollection[collection];
ownViewModels.forEach(ownViewModel => {
relations.forEach(relation => {
changedModels[collection].forEach(id => {
if (
this.relationManager.updateSingleDependencyForChangedModel(
ownViewModel,
relation,
collection,
id
)
) {
updatedIds.push(ownViewModel.id);
}
});
});
});
// Order all relations, if neeed.
if (updatedIds.length) {
relations.forEach(relation => {
if (
(isNormalRelationDefinition(relation) || isReverseRelationDefinition(relation)) &&
(relation.type === 'M2M' || relation.type === 'O2M') &&
relation.order
) {
ownViewModels.forEach(ownViewModel => {
if (ownViewModel['_' + relation.ownKey]) {
this.relationManager.sortByRelation(relation, ownViewModel);
}
});
}
});
}
// Inform about changes. (List updates is done in `commitUpdate` via `DataStoreUpdateManagerService`)
updatedIds.forEach(id => {
this.updateViewModelObservable(id);
});
});
return !!updatedIds.length;
}
public updateDependenciesForDeletedModels(deletedModels: CollectionIds): boolean {
if (!Object.keys(this.reverseRelationsByCollection).length) {
return false;
}
// Get all viewModels from this repo once.
const ownViewModels = this.getViewModelList();
let somethingChanged = false;
Object.keys(deletedModels).forEach(collection => {
const dependencyChanged: boolean = Object.keys(this.reverseRelationsByCollection).includes(collection);
if (!dependencyChanged) {
return;
}
// Ok, we are affected by this collection. Update all viewModels from this repo.
const relations = this.reverseRelationsByCollection[collection];
ownViewModels.forEach(ownViewModel => {
relations.forEach(relation => {
deletedModels[collection].forEach(id => {
if (this.relationManager.updateSingleDependencyForDeletedModel(ownViewModel, relation, id)) {
// Inform about changes. (List updates is done in `commitUpdate` via `DataStoreUpdateManagerService`)
this.updateViewModelObservable(id);
somethingChanged = true;
}
});
});
});
// Ordering all relations is not needed, because just deleting things out of arrays
// will not unorder them.
});
return somethingChanged;
}
/**
* Saves the (full) update to an existing model. So called "update"-function
* Provides a default procedure, but can be overwritten if required
@ -345,10 +215,8 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
* @param viewModel the view model that the update is based on
*/
public async update(update: Partial<M>, viewModel: V): Promise<void> {
const sendUpdate = new this.baseModelCtor();
sendUpdate.patchValues(viewModel.getModel());
sendUpdate.patchValues(update);
return await this.dataSend.updateModel(sendUpdate);
const data = viewModel.getUpdatedModel(update);
return await this.dataSend.updateModel(data);
}
/**
@ -359,9 +227,8 @@ export abstract class BaseRepository<V extends BaseViewModel & T, M extends Base
* @param viewModel the motion to update
*/
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.patchValues(update);
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> {
// this ensures we get a valid base model, even if the view was just
// sending an object with "as MyModelClass"
const sendModel = new this.baseModelCtor();
sendModel.patchValues(model);
const sendModel = new this.baseModelCtor(model);
// Strips empty fields from the sending mode data (except false)
// required for i.e. users, since group list is mandatory

View File

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

View File

@ -112,41 +112,29 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<
/**
* Sets a change recommendation to accepted.
*
* @param {ViewMotionChangeRecommendation} change
* @param {ViewMotionChangeRecommendation} changeRecommendation
*/
public async setAccepted(change: ViewMotionChangeRecommendation): Promise<void> {
const changeReco = change.changeRecommendation;
changeReco.patchValues({
rejected: false
});
await this.dataSend.partialUpdateModel(changeReco);
public async setAccepted(changeRecommendation: ViewMotionChangeRecommendation): Promise<void> {
await this.patch({ rejected: false }, changeRecommendation);
}
/**
* Sets a change recommendation to rejected.
*
* @param {ViewMotionChangeRecommendation} change
* @param {ViewMotionChangeRecommendation} changeRecommendation
*/
public async setRejected(change: ViewMotionChangeRecommendation): Promise<void> {
const changeReco = change.changeRecommendation;
changeReco.patchValues({
rejected: true
});
await this.dataSend.partialUpdateModel(changeReco);
public async setRejected(changeRecommendation: ViewMotionChangeRecommendation): Promise<void> {
await this.patch({ rejected: true }, changeRecommendation);
}
/**
* Sets if a change recommendation is internal (for the administrators) or not.
*
* @param {ViewMotionChangeRecommendation} change
* @param {ViewMotionChangeRecommendation} changeRecommendation
* @param {boolean} internal
*/
public async setInternal(change: ViewMotionChangeRecommendation, internal: boolean): Promise<void> {
const changeReco = change.changeRecommendation;
changeReco.patchValues({
internal: internal
});
await this.dataSend.partialUpdateModel(changeReco);
public async setInternal(changeRecommendation: ViewMotionChangeRecommendation, internal: boolean): Promise<void> {
await this.patch({ internal: internal }, changeRecommendation);
}
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 { ViewUser } from 'app/site/users/models/view-user';
import { BaseIsAgendaItemAndListOfSpeakersContentObjectRepository } from '../base-is-agenda-item-and-list-of-speakers-content-object-repository';
import { NestedModelDescriptors } from '../base-repository';
import { CollectionStringMapperService } from '../../core-services/collection-string-mapper.service';
import { DataSendService } from '../../core-services/data-send.service';
import { LinenumberingService, LineNumberRange } from '../../ui-services/linenumbering.service';
@ -94,21 +95,6 @@ const MotionRelations: RelationDefinition[] = [
ownKey: 'motion_block',
foreignViewModel: ViewMotionBlock
},
{
type: 'nested',
ownKey: 'submitters',
foreignViewModel: ViewSubmitter,
foreignModel: Submitter,
order: 'weight',
relationDefinition: [
{
type: 'M2O',
ownIdKey: 'user_id',
ownKey: 'user',
foreignViewModel: ViewUser
}
]
},
{
type: 'M2M',
ownIdKey: 'supporters_id',
@ -138,10 +124,39 @@ const MotionRelations: RelationDefinition[] = [
foreignIdKey: 'parent_id',
ownKey: 'amendments',
foreignViewModel: ViewMotion
},
// TMP:
{
type: 'M2O',
ownIdKey: 'parent_id',
ownKey: 'parent',
foreignViewModel: ViewMotion
}
// Personal notes are dynamically added in the repo.
];
const MotionNestedModelDescriptors: NestedModelDescriptors = {
'motions/motion': [
{
ownKey: 'submitters',
foreignViewModel: ViewSubmitter,
foreignModel: Submitter,
order: 'weight',
relationDefinitionsByKey: {
user: {
type: 'M2O',
ownIdKey: 'user_id',
ownKey: 'user',
foreignViewModel: ViewUser
}
},
titles: {
getTitle: (viewSubmitter: ViewSubmitter) => (viewSubmitter.user ? viewSubmitter.user.getTitle() : '')
}
}
]
};
/**
* Repository Services for motions (and potentially categories)
*
@ -198,7 +213,17 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
private readonly diff: DiffService,
private operator: OperatorService
) {
super(DS, dataSend, mapperService, viewModelStoreService, translate, relationManager, Motion, MotionRelations);
super(
DS,
dataSend,
mapperService,
viewModelStoreService,
translate,
relationManager,
Motion,
MotionRelations,
MotionNestedModelDescriptors
);
config.get<SortProperty>('motions_motions_sorting').subscribe(conf => {
this.sortProperty = conf;
this.setConfigSortFn();
@ -209,54 +234,6 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
});
}
/**
* Adds the personal note custom relation to the relation definitions.
* Also adds the parent relation here to get access to methods in this repo.
*/
protected groupRelationsByCollections(): void {
this.relationDefinitions.push({
type: 'custom',
foreignViewModel: ViewPersonalNote,
setRelations: (motion: Motion, viewMotion: ViewMotion) => {
viewMotion.personalNote = this.getPersonalNoteForMotion(motion);
},
updateDependency: (viewMotion: ViewMotion, viewPersonalNote: ViewPersonalNote) => {
const personalNoteContent = viewPersonalNote.getNoteContent(this.collectionString, viewMotion.id);
if (!personalNoteContent) {
return false;
}
viewMotion.personalNote = personalNoteContent;
return true;
}
});
this.relationDefinitions.push({
type: 'M2O',
ownIdKey: 'parent_id',
ownKey: 'parent',
foreignViewModel: ViewMotion,
afterSetRelation: (motion: ViewMotion, foreignViewModel: ViewMotion | null) => {
if (foreignViewModel) {
try {
motion.diffLines = this.getAmendmentParagraphs(motion, this.motionLineLength, false);
} catch (e) {
console.warn('Error with motion or amendment ', motion);
}
}
},
afterDependencyChange: (motion: ViewMotion, parent: ViewMotion) => {
if (motion.parent) {
try {
motion.diffLines = this.getAmendmentParagraphs(motion, this.motionLineLength, false);
} catch (e) {
console.warn('Error with motion or amendment: ', motion);
}
}
}
});
super.groupRelationsByCollections();
}
public getTitle = (titleInformation: MotionTitleInformation) => {
if (titleInformation.identifier) {
return titleInformation.identifier + ': ' + titleInformation.title;
@ -321,8 +298,8 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
return this.translate.instant(plural ? 'Motions' : 'Motion');
};
protected createViewModelWithTitles(model: Motion, initialLoading: boolean): ViewMotion {
const viewModel = super.createViewModelWithTitles(model, initialLoading);
protected createViewModelWithTitles(model: Motion): ViewMotion {
const viewModel = super.createViewModelWithTitles(model);
viewModel.getIdentifierOrTitle = () => this.getIdentifierOrTitle(viewModel);
viewModel.getProjectorTitle = () => this.getAgendaSlideTitle(viewModel);
@ -330,6 +307,37 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
return viewModel;
}
protected extendRelations(): void {
this.relationDefinitions.push({
type: 'custom',
ownKey: 'personalNote',
get: (motion: Motion, viewMotion: ViewMotion) => {
return this.getPersonalNoteForMotion(motion);
},
getCacheObjectToCheck: (viewMotion: ViewMotion) => this.getPersonalNote()
});
this.relationDefinitions.push({
type: 'custom',
ownKey: 'diffLines',
get: (motion: Motion, viewMotion: ViewMotion) => {
if (viewMotion.parent) {
return this.getAmendmentParagraphs(viewMotion, this.motionLineLength, false);
}
},
getCacheObjectToCheck: (viewMotion: ViewMotion) => viewMotion.parent
});
super.extendRelations();
}
/**
* @returns the personal note for the operator.
*/
private getPersonalNote(): ViewPersonalNote | null {
return this.viewModelStoreService.find(ViewPersonalNote, pn => {
return pn.user_id === this.operator.user.id;
});
}
/**
* Get the personal note content for one motion by their id
*
@ -341,9 +349,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
return;
}
const personalNote = this.viewModelStoreService.find(ViewPersonalNote, pn => {
return pn.userId === this.operator.user.id;
});
const personalNote = this.getPersonalNote();
if (!personalNote) {
return;
}
@ -735,7 +741,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
const motion = amendment.parent;
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
return amendment.amendment_paragraphs
return (amendment.amendment_paragraphs || [])
.map(
(newText: string, paraNo: number): DiffLinesInParagraph => {
if (newText !== null) {
@ -779,7 +785,7 @@ export class MotionRepositoryService extends BaseIsAgendaItemAndListOfSpeakersCo
const motion = amendment.parent;
const baseParagraphs = this.getTextParagraphs(motion, true, lineLength);
return amendment.amendment_paragraphs
return (amendment.amendment_paragraphs || [])
.map(
(newText: string, paraNo: number): ViewMotionAmendedParagraph => {
if (newText === null) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -531,7 +531,7 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
propertyAsString = '' + item[prop];
}
if (!!propertyAsString) {
if (propertyAsString) {
const foundProp =
propertyAsString
.trim()
@ -635,9 +635,11 @@ export class ListViewTableComponent<V extends BaseViewModel, M extends BaseModel
* does not work, tying the tables to the same hight.
*/
public async scrollToPreviousPosition(): Promise<void> {
if (this.ngrid) {
const scrollIndex = await this.getScrollIndex(this.listStorageKey);
this.ngrid.viewport.scrollToIndex(scrollIndex);
}
}
/**
* 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()
public set model(value: string) {
if (!!value) {
if (value) {
this.modelForm.setValue(value);
}
}

View File

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

View File

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

View File

@ -2,6 +2,12 @@ import { BaseModelWithContentObject } from '../base/base-model-with-content-obje
import { ContentObject } from '../base/content-object';
import { Speaker } from './speaker';
export interface ListOfSpeakersWithoutNestedModels extends BaseModelWithContentObject<ListOfSpeakers> {
id: number;
title_information: object;
closed: boolean;
}
/**
* Representations of agenda Item
* @ignore
@ -19,3 +25,5 @@ export class ListOfSpeakers extends BaseModelWithContentObject<ListOfSpeakers> {
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 candidate_id: number; // the user id of the candidate
public get user_id(): number {
// to be consistent with user...
return this.candidate_id;
}
public is_elected: boolean;
public votes: AssignmentOptionVote[];
public poll_id: number;

View File

@ -2,6 +2,20 @@ import { AssignmentPollMethod } from 'app/site/assignments/services/assignment-p
import { AssignmentPollOption } from './assignment-poll-option';
import { BaseModel } from '../base/base-model';
export interface AssignmentPollWithoutNestedModels extends BaseModel<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
* @ignore
@ -11,21 +25,8 @@ export class AssignmentPoll extends BaseModel<AssignmentPoll> {
private static DECIMAL_FIELDS = ['votesvalid', 'votesinvalid', 'votescast', 'votesno', 'votesabstain'];
public id: number;
public pollmethod: AssignmentPollMethod;
public description: string;
public published: boolean;
public options: AssignmentPollOption[];
public votesvalid: number;
public votesno: number;
public votesabstain: number;
public votesinvalid: number;
public votescast: number;
public has_votes: boolean;
public assignment_id: number;
/**
* @param input
*/
public constructor(input?: any) {
// cast stringify numbers
if (input) {
@ -38,3 +39,4 @@ export class AssignmentPoll extends BaseModel<AssignmentPoll> {
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 { 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.
* @ignore
@ -10,15 +21,8 @@ export class Assignment extends BaseModelWithAgendaItemAndListOfSpeakers<Assignm
public static COLLECTIONSTRING = 'assignments/assignment';
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 poll_description_default: number;
public polls: AssignmentPoll[];
public tags_id: number[];
public attachments_id: number[];
public constructor(input?: any) {
super(Assignment.COLLECTIONSTRING, input);
@ -31,13 +35,5 @@ export class Assignment extends BaseModelWithAgendaItemAndListOfSpeakers<Assignm
})
.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.
*/
export abstract class BaseModelWithContentObject<T = object> extends BaseModel<T> {
export abstract class BaseModelWithContentObject<T = any> extends BaseModel<T> {
public abstract content_object: ContentObject;
public get contentObjectData(): ContentObject {

View File

@ -1,5 +1,5 @@
import { Collection } from './collection';
import { Deserializable } from './deserializable';
import { Deserializer } from './deserializer';
import { Identifiable } from './identifiable';
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.
* 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 {
/**
* force children of BaseModel to have a collectionString.
*
* Has a getter but no setter.
*/
protected _collectionString: string;
/**
* returns the collectionString.
*
* The server and the dataStore use it to identify the collection.
*/
public get collectionString(): string {
return this._collectionString;
export abstract class BaseModel<T = any> extends Deserializer implements Identifiable, Collection {
public get elementId(): string {
return `${this.collectionString}:${this.id}`;
}
/**
* force children of BaseModel to have an id
*/
public abstract id: number;
/**
* constructor that calls super from parent class
*
* @param collectionString
* @param verboseName
* @param input
*/
protected constructor(collectionString: string, input?: any) {
this._collectionString = collectionString;
if (input) {
this.changeNullValuesToUndef(input);
this.deserialize(input);
}
protected constructor(public readonly collectionString: string, input?: Partial<T>) {
super(input);
}
/**
* Prevent to send literally "null" if should be send
* @param input object to deserialize
*/
public changeNullValuesToUndef(input: any): void {
Object.keys(input).forEach(key => {
if (input[key] === null) {
input[key] = undefined;
}
});
}
/**
* update the values of the base model with new values
*/
public patchValues(update: Partial<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);
public getUpdatedVersion(update: Partial<T>): T {
const copy: T = (<any>Object.assign({}, this)) as T;
return Object.assign(copy, update);
}
}

View File

@ -11,28 +11,16 @@ export abstract class Deserializer implements Deserializable {
*/
protected constructor(input?: any) {
if (input) {
this.changeNullValuesToUndef(input);
this.deserialize(input);
}
}
/**
* should be used to assign JSON values to the object itself.
* @param input
* Most simple and most commonly used deserialize function.
* Inherited to children, can be overwritten for special use cases
* @param input JSON data for deserialization.
*/
public deserialize(input: any): void {
Object.assign(this, input);
}
/**
* Prevent to send literally "null" if should be send
* @param input object to deserialize
*/
public changeNullValuesToUndef(input: any): void {
Object.keys(input).forEach(key => {
if (input[key] === null) {
input[key] = undefined;
}
});
}
}

View File

@ -9,6 +9,21 @@ interface FileMetadata {
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"
* @ignore
@ -16,25 +31,14 @@ interface FileMetadata {
export class Mediafile extends BaseModelWithListOfSpeakers<Mediafile> {
public static COLLECTIONSTRING = 'mediafiles/mediafile';
public id: number;
public title: string;
public mediafile?: FileMetadata;
public media_url_prefix: string;
public filesize?: string;
public access_groups_id: number[];
public create_timestamp: string;
public parent_id: number | null;
public is_directory: boolean;
public path: string;
public inherited_access_groups_id: boolean | number[];
public get has_inherited_access_groups(): boolean {
return typeof this.inherited_access_groups_id !== 'boolean';
}
public constructor(input?: any) {
super(Mediafile.COLLECTIONSTRING);
// Do not change null to undefined...
this.deserialize(input);
super(Mediafile.COLLECTIONSTRING, input);
}
/**
@ -46,3 +50,4 @@ export class Mediafile extends BaseModelWithListOfSpeakers<Mediafile> {
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[];
}
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.
*
@ -20,35 +54,7 @@ export class Motion extends BaseModelWithAgendaItemAndListOfSpeakers<Motion> {
public static COLLECTIONSTRING = 'motions/motion';
public id: number;
public identifier: string;
public title: string;
public text: string;
public reason: string;
public amendment_paragraphs: string[];
public modified_final_version: string;
public parent_id: number;
public category_id: number;
public category_weight: number;
public motion_block_id: number;
public origin: string;
public submitters: Submitter[];
public supporters_id: number[];
public comments: MotionComment[];
public workflow_id: number;
public state_id: number;
public state_extension: string;
public state_required_permission_to_see: string;
public statute_paragraph_id: number;
public recommendation_id: number;
public recommendation_extension: string;
public tags_id: number[];
public attachments_id: number[];
public polls: MotionPoll[];
public weight: number;
public sort_parent_id: number;
public created: string;
public last_modified: string;
public change_recommendations_id: number[];
public constructor(input?: any) {
super(Motion.COLLECTIONSTRING, input);
@ -57,7 +63,7 @@ export class Motion extends BaseModelWithAgendaItemAndListOfSpeakers<Motion> {
/**
* returns the motion submitters user ids
*/
public get sorted_submitters_id(): number[] {
public get sorted_submitter_ids(): number[] {
return this.submitters
.sort((a: Submitter, b: Submitter) => {
return a.weight - b.weight;
@ -65,3 +71,5 @@ export class Motion extends BaseModelWithAgendaItemAndListOfSpeakers<Motion> {
.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) {
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 attachments_id: number[];
public constructor(input?: any) {
public constructor(input?: Partial<Topic>) {
super(Topic.COLLECTIONSTRING, input);
}
}

View File

@ -11,11 +11,7 @@ export class Group extends BaseModel<Group> {
public name: string;
public permissions: string[];
public constructor(input?: any) {
public constructor(input?: Partial<Group>) {
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 notes: PersonalNotesFormat;
public constructor(input: any) {
public constructor(input: Partial<PersonalNote>) {
super(PersonalNote.COLLECTIONSTRING, input);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,34 +18,6 @@ export class ViewItem extends BaseViewModelWithContentObject<Item, BaseViewModel
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;
/**
@ -71,19 +43,5 @@ export class ViewItem extends BaseViewModelWithContentObject<Item, BaseViewModel
const type = ItemVisibilityChoices.find(choice => choice.key === this.type);
return type ? type.csvName : '';
}
/**
* @returns the weight the server assigns to that item. Mostly useful for sorting within
* it's own hierarchy level (items sharing a parent)
*/
public get weight(): number {
return this.item.weight;
}
/**
* @returns the parent's id of that item (0 if no parent is set).
*/
public get parent_id(): number {
return this.item.parent_id;
}
}
export interface ViewItem extends Item {}

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 { BaseViewModelWithContentObject } from 'app/site/base/base-view-model-with-content-object';
import { BaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers';
@ -19,20 +19,10 @@ export class ViewListOfSpeakers extends BaseViewModelWithContentObject<ListOfSpe
public static COLLECTIONSTRING = ListOfSpeakers.COLLECTIONSTRING;
protected _collectionString = ListOfSpeakers.COLLECTIONSTRING;
private _speakers?: ViewSpeaker[];
public get listOfSpeakers(): ListOfSpeakers {
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
*/
@ -40,10 +30,6 @@ export class ViewListOfSpeakers extends BaseViewModelWithContentObject<ListOfSpe
return this.speakers.filter(speaker => speaker.state === SpeakerState.WAITING).length;
}
public get closed(): boolean {
return this.listOfSpeakers.closed;
}
public get listOfSpeakersUrl(): string {
return `/agenda/speakers/${this.id}`;
}
@ -65,3 +51,7 @@ export class ViewListOfSpeakers extends BaseViewModelWithContentObject<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;
protected _collectionString = Speaker.COLLECTIONSTRING;
private _user?: ViewUser;
public get speaker(): Speaker {
return this._model;
}
public get user(): ViewUser | null {
return this._user;
}
public get id(): number {
return this.speaker.id;
}
public get user_id(): number {
return this.speaker.user_id;
}
public get weight(): number {
return this.speaker.weight;
}
public get marked(): boolean {
return this.speaker.marked;
}
/**
* @returns an ISO datetime string or null
*/
public get begin_time(): string | null {
return this.speaker.begin_time;
}
/**
* @returns an ISO datetime string or null
*/
public get end_time(): string | null {
return this.speaker.end_time;
}
/**
* @returns
* - waiting if there is no begin nor end time
@ -82,7 +46,10 @@ export class ViewSpeaker extends BaseViewModel<Speaker> {
return this.user ? this.user.gender : '';
}
public getTitle = () => {
return this.name;
public getListTitle = () => {
return this.getTitle();
};
}
export interface ViewSpeaker extends Speaker {
user?: ViewUser;
}

View File

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

View File

@ -36,11 +36,11 @@
<!-- Add/remove to/from agenda -->
<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>
<span translate>Add to agenda</span>
</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>
<span translate>Remove from agenda</span>
</button>
@ -241,7 +241,6 @@
matInput
placeholder="{{ 'Title' | translate }}"
formControlName="title"
[value]="assignmentCopy.title || ''"
/>
<mat-error>{{ 'The title is required' | translate }}</mat-error>
</mat-form-field>
@ -287,7 +286,6 @@
matInput
placeholder="{{ 'Default comment on the ballot paper' | translate }}"
formControlName="poll_description_default"
[value]="assignmentCopy.assignment.poll_description_default || ''"
/>
</mat-form-field>
</div>

View File

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

View File

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

View File

@ -170,7 +170,7 @@
<h4 translate>Candidates</h4>
<div *ngFor="let option of poll.options">
<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>

View File

@ -161,7 +161,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
public async onDeletePoll(): Promise<void> {
const title = this.translate.instant('Are you sure you want to delete this ballot?');
if (await this.promptService.open(title)) {
await this.assignmentRepo.deletePoll(this.poll).then(null, this.raiseError);
await this.assignmentRepo.deletePoll(this.poll).catch(this.raiseError);
}
}
@ -197,13 +197,12 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
*/
public enterVotes(): void {
const dialogRef = this.dialog.open(AssignmentPollDialogComponent, {
// TODO deep copy of this.poll (JSON parse is ugly workaround) or sending just copy of the options
data: this.poll.copy(),
...mediumDialogSettings
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.assignmentRepo.updateVotes(result, this.poll).then(null, this.raiseError);
this.assignmentRepo.updateVotes(result, this.poll).catch(this.raiseError);
}
});
}
@ -236,7 +235,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
// TODO additional conditions: assignment not finished?
const viewAssignmentRelatedUser = this.assignment.assignment_related_users.find(
user => user.user_id === option.user_id
user => user.user_id === option.candidate_id
);
if (viewAssignmentRelatedUser) {
this.assignmentRepo.markElected(viewAssignmentRelatedUser, this.assignment, !option.is_elected);
@ -249,7 +248,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
*/
public async onEditDescriptionButton(): Promise<void> {
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;
protected _collectionString = AssignmentPollOption.COLLECTIONSTRING;
private _user?: ViewUser; // This is the "candidate". We'll stay consistent wich user here...
public get option(): AssignmentPollOption {
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[] {
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 { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { AssignmentPollMethod } from '../services/assignment-poll.service';
import { ViewAssignmentPollOption } from './view-assignment-poll-option';
export class ViewAssignmentPoll extends BaseProjectableViewModel<AssignmentPoll> {
public static COLLECTIONSTRING = AssignmentPoll.COLLECTIONSTRING;
protected _collectionString = AssignmentPoll.COLLECTIONSTRING;
private _options: ViewAssignmentPollOption[];
public get poll(): AssignmentPoll {
return this._model;
}
public get options(): ViewAssignmentPollOption[] {
return this._options;
}
public get id(): number {
return this.poll.id;
}
public get pollmethod(): AssignmentPollMethod {
return this.poll.pollmethod;
}
public get description(): string {
return this.poll.description;
}
public get published(): boolean {
return this.poll.published;
}
public get votesno(): number {
return this.poll.votesno;
}
public set votesno(amount: number) {
this.poll.votesno = amount;
}
public get votesabstain(): number {
return this.poll.votesabstain;
}
public set votesabstain(amount: number) {
this.poll.votesabstain = amount;
}
public get votesvalid(): number {
return this.poll.votesvalid;
}
public set votesvalid(amount: number) {
this.poll.votesvalid = amount;
}
public get votesinvalid(): number {
return this.poll.votesinvalid;
}
public set votesinvalid(amount: number) {
this.poll.votesinvalid = amount;
}
public get votescast(): number {
return this.poll.votescast;
}
public set votescast(amount: number) {
this.poll.votescast = amount;
}
public get has_votes(): boolean {
return this.poll.has_votes;
}
public get assignment_id(): number {
return this.poll.assignment_id;
}
public getTitle = () => {
return 'Poll';
};
public getListTitle = () => {
return this.getTitle();
};
@ -90,6 +20,20 @@ export class ViewAssignmentPoll extends BaseProjectableViewModel<AssignmentPoll>
return this.getTitle();
};
public getSlide(): ProjectorElementBuildDeskriptor {
return {
getBasicProjectorElement: options => ({
name: 'assignments/poll',
assignment_id: this.assignment_id,
poll_id: this.id,
getIdentifiers: () => ['name', 'assignment_id', 'poll_id']
}),
slideOptions: [],
projectionDefaultName: 'assignments',
getDialogTitle: () => 'TODO'
};
}
/**
* Creates a copy with deep-copy on all changing numerical values,
* but intact uncopied references to the users
@ -107,18 +51,8 @@ export class ViewAssignmentPoll extends BaseProjectableViewModel<AssignmentPoll>
});
return poll;
}
}
public getSlide(): ProjectorElementBuildDeskriptor {
return {
getBasicProjectorElement: options => ({
name: 'assignments/poll',
assignment_id: this.assignment_id,
poll_id: this.id,
getIdentifiers: () => ['name', 'assignment_id', 'poll_id']
}),
slideOptions: [],
projectionDefaultName: 'assignments',
getDialogTitle: () => 'TODO'
};
}
export interface ViewAssignmentPoll extends AssignmentPollWithoutNestedModels {
options: ViewAssignmentPollOption[];
}

View File

@ -6,39 +6,15 @@ export class ViewAssignmentRelatedUser extends BaseViewModel<AssignmentRelatedUs
public static COLLECTIONSTRING = AssignmentRelatedUser.COLLECTIONSTRING;
protected _collectionString = AssignmentRelatedUser.COLLECTIONSTRING;
private _user?: ViewUser;
public get assignmentRelatedUser(): AssignmentRelatedUser {
return this._model;
}
public get user(): ViewUser {
return this._user;
}
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() : '';
public getListTitle = () => {
return this.getTitle();
};
}
export interface ViewAssignmentRelatedUser extends AssignmentRelatedUser {
user?: ViewUser;
}

View File

@ -1,5 +1,5 @@
import { SearchRepresentation } from 'app/core/ui-services/search.service';
import { Assignment } from 'app/shared/models/assignments/assignment';
import { Assignment, AssignmentWithoutNestedModels } from 'app/shared/models/assignments/assignment';
import { TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item';
import { BaseViewModelWithAgendaItemAndListOfSpeakers } from 'app/site/base/base-view-model-with-agenda-item-and-list-of-speakers';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
@ -41,60 +41,18 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers
public static COLLECTIONSTRING = Assignment.COLLECTIONSTRING;
protected _collectionString = Assignment.COLLECTIONSTRING;
private _assignment_related_users?: ViewAssignmentRelatedUser[];
private _polls?: ViewAssignmentPoll[];
private _tags?: ViewTag[];
private _attachments?: ViewMediafile[];
public get assignment(): Assignment {
return this._model;
}
public get polls(): ViewAssignmentPoll[] {
return this._polls || [];
}
public get title(): string {
return this.assignment.title;
}
public get open_posts(): number {
return this.assignment.open_posts;
}
public get description(): string {
return this.assignment.description;
}
public get candidates(): ViewUser[] {
return this.assignment_related_users.map(aru => aru.user).filter(x => !!x);
}
public get assignment_related_users(): ViewAssignmentRelatedUser[] {
return this._assignment_related_users || [];
}
public get tags(): ViewTag[] {
return this._tags || [];
}
public get tags_id(): number[] {
return this.assignment.tags_id;
}
public get attachments(): ViewMediafile[] {
return this._attachments || [];
}
public get attachments_id(): number[] {
return this.assignment.attachments_id;
}
/**
* unknown where the identifier to the phase is get
* TODO: Fix assignment creation: DO NOT create a ViewUser there...
*/
public get phase(): number {
return this.assignment.phase;
public get candidates(): ViewUser[] {
if (!this.assignment_related_users) {
return [];
}
return this.assignment_related_users.map(aru => aru.user).filter(x => !!x);
}
public get phaseString(): string {
@ -123,7 +81,7 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers
* @returns the amount of candidates in the assignment's candidate list
*/
public get candidateAmount(): number {
return this._assignment_related_users ? this._assignment_related_users.length : 0;
return this.assignment_related_users.length;
}
public formatForSearch(): SearchRepresentation {
@ -147,3 +105,11 @@ export class ViewAssignment extends BaseViewModelWithAgendaItemAndListOfSpeakers
};
}
}
interface IAssignmentRelations {
assignment_related_users: ViewAssignmentRelatedUser[];
polls?: ViewAssignmentPoll[];
tags?: ViewTag[];
attachments?: ViewMediafile[];
}
export interface ViewAssignment extends AssignmentWithoutNestedModels, IAssignmentRelations {}

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 { BaseProjectableViewModel } from './base-projectable-view-model';
import { IBaseViewModelWithAgendaItem, isBaseViewModelWithAgendaItem } from './base-view-model-with-agenda-item';
import { BaseViewModelWithAgendaItem, isBaseViewModelWithAgendaItem } from './base-view-model-with-agenda-item';
import {
IBaseViewModelWithListOfSpeakers,
BaseViewModelWithListOfSpeakers,
isBaseViewModelWithListOfSpeakers
} from './base-view-model-with-list-of-speakers';
import { ViewItem } from '../agenda/models/view-item';
import { ViewListOfSpeakers } from '../agenda/models/view-list-of-speakers';
export function isBaseViewModelWithAgendaItemAndListOfSpeakers(
obj: any
@ -15,54 +13,15 @@ export function isBaseViewModelWithAgendaItemAndListOfSpeakers(
return !!obj && isBaseViewModelWithAgendaItem(obj) && isBaseViewModelWithListOfSpeakers(obj);
}
/**
* Base view class for view models with an agenda item and a list of speakers associated.
*/
export abstract class BaseViewModelWithAgendaItemAndListOfSpeakers<
M extends BaseModelWithAgendaItemAndListOfSpeakers = any
> extends BaseProjectableViewModel implements IBaseViewModelWithAgendaItem<M>, IBaseViewModelWithListOfSpeakers<M> {
protected _item?: ViewItem;
protected _list_of_speakers?: ViewListOfSpeakers;
> extends BaseProjectableViewModel<M> {}
public get agendaItem(): ViewItem | null {
return this._item;
}
export interface BaseViewModelWithAgendaItemAndListOfSpeakers<M extends BaseModelWithAgendaItemAndListOfSpeakers = any>
extends BaseViewModelWithAgendaItem<M>,
BaseViewModelWithListOfSpeakers<M> {}
public get agenda_item_id(): number {
return this._model.agenda_item_id;
}
public get agenda_item_number(): string | null {
return this.agendaItem && this.agendaItem.itemNumber ? this.agendaItem.itemNumber : null;
}
public get listOfSpeakers(): ViewListOfSpeakers | null {
return this._list_of_speakers;
}
public get list_of_speakers_id(): number {
return this._model.list_of_speakers_id;
}
public getAgendaSlideTitle: () => string;
public getAgendaListTitle: () => string;
public getAgendaListTitleWithoutItemNumber: () => string;
public getAgendaSubtitle: () => string | null;
public getListOfSpeakersTitle: () => string;
public getListOfSpeakersSlideTitle: () => string;
/**
* @returns the (optional) descriptive text to be exported in the CSV.
* May be overridden by inheriting classes
*/
public getCSVExportText(): string {
return '';
}
public abstract getDetailStateURL(): string;
/**
* Should return a string representation of the object, so there can be searched for.
*/
public abstract formatForSearch(): SearchRepresentation;
}
applyMixins(BaseViewModelWithAgendaItemAndListOfSpeakers, [
BaseViewModelWithAgendaItem,
BaseViewModelWithListOfSpeakers
]);

View File

@ -13,9 +13,11 @@ export function isBaseViewModelWithAgendaItem(obj: any): obj is BaseViewModelWit
isSearchable(model) &&
model.getAgendaSlideTitle !== undefined &&
model.getAgendaListTitle !== undefined &&
model.getAgendaSubtitle !== undefined &&
model.getCSVExportText !== undefined &&
model.agendaItem !== undefined &&
model.agenda_item_id !== undefined
model.item !== undefined &&
model.getModel !== undefined &&
model.getModel().agenda_item_id !== undefined
);
}
@ -26,15 +28,11 @@ export interface TitleInformationWithAgendaItem extends TitleInformation {
/**
* Describes a base class for view models.
*/
export interface IBaseViewModelWithAgendaItem<M extends BaseModelWithAgendaItem = any>
export interface BaseViewModelWithAgendaItem<M extends BaseModelWithAgendaItem = any>
extends BaseProjectableViewModel<M>,
DetailNavigable,
Searchable {
agendaItem: any | null;
agenda_item_id: number;
agenda_item_number: string | null;
item: any | null;
/**
* @returns the agenda title
@ -46,21 +44,10 @@ export interface IBaseViewModelWithAgendaItem<M extends BaseModelWithAgendaItem
*/
getAgendaListTitle: () => string;
/**
* @return an optional subtitle for the agenda.
*/
getAgendaSubtitle: () => string | null;
/**
* @return the agenda title with the verbose name of the content object
*/
getAgendaListTitleWithoutItemNumber: () => string;
/**
* @returns the (optional) descriptive text to be exported in the CSV.
* May be overridden by inheriting classes
*/
getCSVExportText(): string;
}
/**
@ -68,41 +55,11 @@ export interface IBaseViewModelWithAgendaItem<M extends BaseModelWithAgendaItem
*
* TODO: Resolve circular dependencies with `ViewItem` to avoid `any`.
*/
export abstract class BaseViewModelWithAgendaItem<M extends BaseModelWithAgendaItem = any>
extends BaseProjectableViewModel<M>
implements IBaseViewModelWithAgendaItem<M> {
protected _item?: any;
public get agendaItem(): any | null {
return this._item;
}
public get agenda_item_id(): number {
return this._model.agenda_item_id;
}
export abstract class BaseViewModelWithAgendaItem<
M extends BaseModelWithAgendaItem = any
> extends BaseProjectableViewModel<M> {
public get agenda_item_number(): string | null {
return this.agendaItem && this.agendaItem.itemNumber ? this.agendaItem.itemNumber : null;
}
/**
* @returns the agenda title for the item slides
*/
public getAgendaSlideTitle: () => string;
/**
* @return the agenda title for the list view
*/
public getAgendaListTitle: () => string;
/**
* @return the agenda title without any item number.
*/
public getAgendaListTitleWithoutItemNumber: () => string;
public constructor(model: M, item?: any) {
super(model);
this._item = item || null; // Explicit set to null instead of undefined, if not given
return this.item && this.item.item_number ? this.item.item_number : null;
}
/**

View File

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

View File

@ -9,50 +9,19 @@ export function isBaseViewModelWithListOfSpeakers(obj: any): obj is BaseViewMode
isDetailNavigable(model) &&
model.getListOfSpeakersTitle !== undefined &&
model.listOfSpeakers !== undefined &&
model.list_of_speakers_id !== undefined
model.getModel !== undefined &&
model.getModel().list_of_speakers_id !== undefined
);
}
/**
* Describes a base view model with a list of speakers.
*/
export interface IBaseViewModelWithListOfSpeakers<M extends BaseModelWithListOfSpeakers = any>
extends BaseProjectableViewModel<M>,
DetailNavigable {
export interface BaseViewModelWithListOfSpeakers<M extends BaseModelWithListOfSpeakers = any> extends DetailNavigable {
listOfSpeakers: any | null;
list_of_speakers_id: number;
getListOfSpeakersTitle: () => string;
getListOfSpeakersSlideTitle: () => string;
}
/**
* Base view model class for models with a list of speakers.
*
* TODO: Resolve circular dependencies with `ViewListOfSpeakers` to avoid `any`.
*/
export abstract class BaseViewModelWithListOfSpeakers<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
}
export abstract class BaseViewModelWithListOfSpeakers<
M extends BaseModelWithListOfSpeakers = any
> extends BaseProjectableViewModel<M> {
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 {
public get id(): number {
return this._model.id;
}
/**
* force children of BaseModel to have a collectionString.
*
* Has a getter but no setter.
*/
protected _collectionString: string;
/**
* returns the collectionString.
*
* The server and the dataStore use it to identify the collection.
*/
public get collectionString(): string {
return this._collectionString;
}
export abstract class BaseViewModel<M extends BaseModel = any> {
/**
* @returns the element id of the model
*/
public get elementId(): string {
return `${this.collectionString}:${this.id}`;
return this._model.elementId;
}
public getTitle: () => string;
public getListTitle: () => string;
/**
* Returns the verbose name.
*
* @param plural If the name should be plural
* @returns the verbose name of the model
*/
public getVerboseName: (plural?: boolean) => string;
/**
* @param collectionString
* @param model
@ -64,8 +33,25 @@ export abstract class BaseViewModel<M extends BaseModel = any> implements Displa
public getModel(): M {
return this._model;
}
public toString(): string {
return this.getTitle();
}
public toJSON(): M {
return this.getModel();
}
public getUpdatedModel(update: Partial<M>): 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>
<mat-divider [vertical]="true"></mat-divider>
<div *ngIf="showPreview" class="result-preview flex-1">
<div *ngIf="!!selectedModel">
<div *ngIf="selectedModel">
<os-preview [viewModel]="selectedModel"> </os-preview>
</div>
</div>

View File

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

View File

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

View File

@ -20,18 +20,10 @@ export class ViewCategory extends BaseViewModel<Category> implements CategoryTit
public static COLLECTIONSTRING = Category.COLLECTIONSTRING;
protected _collectionString = Category.COLLECTIONSTRING;
private _parent?: ViewCategory;
private _children?: ViewCategory[];
private _motions?: ViewMotion[];
public get category(): Category {
return this._model;
}
public get parent(): ViewCategory | null {
return this._parent;
}
public get oldestParent(): ViewCategory {
if (!this.parent_id) {
return this;
@ -40,34 +32,6 @@ export class ViewCategory extends BaseViewModel<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 {
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 = () => {
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;
protected _collectionString = MotionBlock.COLLECTIONSTRING;
private _motions?: ViewMotion[];
public get motionBlock(): MotionBlock {
return this._model;
}
public get motions(): ViewMotion[] {
return this._motions || [];
}
public get title(): string {
return this.motionBlock.title;
}
public get internal(): boolean {
return this.motionBlock.internal;
}
/**
* Formats the category for search
@ -67,3 +54,8 @@ export class ViewMotionBlock extends BaseViewModelWithAgendaItemAndListOfSpeaker
};
}
}
interface IMotionBLockRelations {
motions?: ViewMotion[];
}
export interface ViewMotionBlock extends MotionBlock, IMotionBLockRelations {}

View File

@ -18,69 +18,13 @@ export class ViewMotionCommentSection extends BaseViewModel<MotionCommentSection
public static COLLECTIONSTRING = MotionCommentSection.COLLECTIONSTRING;
protected _collectionString = MotionCommentSection.COLLECTIONSTRING;
private _read_groups: ViewGroup[];
private _write_groups: ViewGroup[];
public get section(): MotionCommentSection {
return this._model;
}
public get id(): number {
return this.section.id;
}
public get name(): string {
return this.section.name;
}
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;
}
}
}
}
interface IMotionCommentSectionRelations {
read_groups: ViewGroup[];
write_groups: ViewGroup[];
}
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 { DiffLinesInParagraph } from 'app/core/ui-services/diff.service';
import { SearchProperty, SearchRepresentation } from 'app/core/ui-services/search.service';
import { Motion, MotionComment } from 'app/shared/models/motions/motion';
import { Motion, MotionComment, MotionWithoutNestedModels } from 'app/shared/models/motions/motion';
import { PersonalNoteContent } from 'app/shared/models/users/personal-note';
import { TitleInformationWithAgendaItem } from 'app/site/base/base-view-model-with-agenda-item';
import { BaseViewModelWithAgendaItemAndListOfSpeakers } from 'app/site/base/base-view-model-with-agenda-item-and-list-of-speakers';
@ -71,176 +71,26 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
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[] {
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;
return (this.submitters || []).map(submitter => submitter.user);
}
public get identifierOrTitle(): string {
return this.identifier ? this.identifier : this.title;
}
public get text(): string {
return this.motion.text;
}
public get reason(): string {
return this.motion.reason;
}
public get modified_final_version(): string {
return this.motion.modified_final_version;
}
public set modified_final_version(value: string) {
if (this.motion) {
this.motion.modified_final_version = value;
}
}
public get weight(): number {
return this.motion.weight;
}
public get sort_parent_id(): number {
return this.motion.sort_parent_id;
}
public get category_id(): number {
return this.motion.category_id;
}
public get category_weight(): number {
return this.motion.category_weight;
}
public get sorted_submitters_id(): number[] {
return this.motion.sorted_submitters_id;
}
public get supporters_id(): number[] {
return this.motion.supporters_id;
}
public get workflow(): ViewWorkflow {
return this._workflow;
}
public get workflow_id(): number {
return this.motion.workflow_id;
}
public get changeRecommendations(): ViewMotionChangeRecommendation[] {
return this._changeRecommendations;
}
public get change_recommendations_id(): number[] {
return this.motion.change_recommendations_id;
}
public get state_id(): number {
return this.motion.state_id;
}
public get recommendation_id(): number {
return this.motion.recommendation_id;
}
public get statute_paragraph_id(): number {
return this.motion.statute_paragraph_id;
}
public get possibleRecommendations(): ViewState[] {
return this.workflow ? this.workflow.states.filter(state => state.recommendation_label !== undefined) : null;
}
public get origin(): string {
return this.motion.origin;
public get agenda_type(): number | null {
return this.item ? this.item.type : null;
}
public get agenda_type(): number {
return this.agendaItem ? this.agendaItem.type : null;
}
public get motion_block_id(): number {
return this.motion.motion_block_id;
}
public get speakerAmount(): number {
public get speakerAmount(): number | null {
return this.listOfSpeakers ? this.listOfSpeakers.waitingSpeakerAmount : null;
}
public get parent_id(): number {
return this.motion.parent_id;
}
public get amendment_paragraphs(): string[] {
return this.motion.amendment_paragraphs ? this.motion.amendment_paragraphs : [];
}
public get tags_id(): number[] {
return this.motion.tags_id;
}
public get attachments_id(): number[] {
return this.motion.attachments_id;
}
/**
* @returns the creation date as Date object
*/
@ -331,20 +181,6 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
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
*/
@ -359,20 +195,6 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
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
*/
@ -386,19 +208,6 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
public static 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;
// This is set by the repository
@ -412,7 +221,7 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
* @return The lines of the amendment
*/
public getChangeLines(): string {
if (!!this.diffLines) {
if (this.diffLines) {
return this.diffLines
.map(diffLine => {
if (diffLine.diffLineTo === diffLine.diffLineFrom + 1) {
@ -521,7 +330,7 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
* specified by amendment_paragraphs-array
*/
public isParagraphBasedAmendment(): boolean {
return this.amendment_paragraphs.length > 0;
return this.amendment_paragraphs && this.amendment_paragraphs.length > 0;
}
public getSlide(configService: ConfigService): ProjectorElementBuildDeskriptor {
@ -554,11 +363,22 @@ export class ViewMotion extends BaseViewModelWithAgendaItemAndListOfSpeakers<Mot
getDialogTitle: this.getAgendaSlideTitle
};
}
}
/**
* Duplicate this motion into a copy of itself
*/
public copy(): ViewMotion {
return new ViewMotion(this._model);
}
interface TIMotionRelations {
category?: ViewCategory;
submitters: ViewSubmitter[];
supporters?: ViewUser[];
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 { ViewWorkflow } from './view-workflow';
@ -14,83 +14,22 @@ export class ViewState extends BaseViewModel<State> implements StateTitleInforma
public static COLLECTIONSTRING = State.COLLECTIONSTRING;
protected _collectionString = State.COLLECTIONSTRING;
private _next_states?: ViewState[];
public _workflow?: ViewWorkflow;
public get state(): State {
return this._model;
}
public get workflow(): ViewWorkflow | null {
return this._workflow;
}
public get next_states(): ViewState[] {
return this._next_states || [];
}
public get name(): string {
return this.state.name;
}
public get recommendation_label(): string {
return this.state.recommendation_label;
}
public get css_class(): string {
return this.state.css_class;
}
public get restriction(): string[] {
return this.state.restriction;
}
public get allow_support(): boolean {
return this.state.allow_support;
}
public get allow_create_poll(): boolean {
return this.state.allow_create_poll;
}
public get allow_submitter_edit(): boolean {
return this.state.allow_submitter_edit;
}
public get dont_set_identifier(): boolean {
return this.state.dont_set_identifier;
}
public get show_state_extension_field(): boolean {
return this.state.show_state_extension_field;
}
public get merge_amendment_into_final(): MergeAmendment {
return this.state.merge_amendment_into_final;
}
public get show_recommendation_extension_field(): boolean {
return this.state.show_recommendation_extension_field;
}
public get next_states_id(): number[] {
return this.state.next_states_id;
}
public get workflow_id(): number {
return this.state.workflow_id;
}
public get isFinalState(): boolean {
return !this.next_states_id || this.next_states_id.length === 0;
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[] {
if (!this.workflow) {
return [];
}
return this.workflow.states.filter(state => {
return state.next_states_id.includes(this.id);
});
}
interface IStateRelations {
next_states?: ViewState[];
previous_states?: ViewState[];
workflow?: ViewWorkflow;
}
export interface ViewState extends State, IStateRelations {}

View File

@ -23,18 +23,6 @@ export class ViewStatuteParagraph extends BaseViewModel<StatuteParagraph>
return this._model;
}
public get title(): string {
return this.statuteParagraph.title;
}
public get text(): string {
return this.statuteParagraph.text;
}
public get weight(): number {
return this.statuteParagraph.weight;
}
public formatForSearch(): SearchRepresentation {
return { properties: [{ key: 'Title', value: this.getTitle() }], searchValue: [this.getTitle()] };
}
@ -43,3 +31,4 @@ export class ViewStatuteParagraph extends BaseViewModel<StatuteParagraph>
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;
protected _collectionString = Submitter.COLLECTIONSTRING;
private _user?: ViewUser;
public get submitter(): Submitter {
return this._model;
}
public get user(): ViewUser {
return this._user;
}
public get id(): number {
return this.submitter.id;
}
public get user_id(): number {
return this.submitter.user_id;
}
public get motion_id(): number {
return this.submitter.motion_id;
}
public get weight(): number {
return this.submitter.weight;
}
public getTitle = () => {
return this.user ? this.user.getTitle() : '';
};
public getListTitle = () => {
return this.getTitle();
};
}
interface ISubmitterRelations {
user: ViewUser;
}
export interface ViewSubmitter extends Submitter, ISubmitterRelations {}

View File

@ -14,30 +14,12 @@ export class ViewWorkflow extends BaseViewModel<Workflow> implements WorkflowTit
public static COLLECTIONSTRING = Workflow.COLLECTIONSTRING;
protected _collectionString = Workflow.COLLECTIONSTRING;
private _states?: ViewState[];
private _first_state?: ViewState;
public get workflow(): Workflow {
return this._model;
}
public get name(): string {
return this.workflow.name;
}
public get states(): ViewState[] {
return this._states || [];
}
public get states_id(): number[] {
return this.workflow.states_id;
}
public get first_state_id(): number {
return this.workflow.first_state_id;
}
public get first_state(): ViewState | null {
return this._first_state;
}
interface IWorkflowRelations {
states?: ViewState[];
first_state?: ViewState;
}
export interface ViewWorkflow extends Workflow, IWorkflowRelations {}

View File

@ -110,7 +110,7 @@ export class AmendmentListComponent extends BaseListViewComponent<ViewMotion> im
public ngOnInit(): void {
// determine if a paramter exists.
if (!!this.route.snapshot.paramMap.get('id')) {
if (this.route.snapshot.paramMap.get('id')) {
// set the parentMotion observable. This will "only" fire
// if there is a subscription to the parent motion
this.parentMotion = this.route.paramMap.pipe(
@ -133,7 +133,7 @@ export class AmendmentListComponent extends BaseListViewComponent<ViewMotion> im
*/
public getAmendmentSummary(amendment: ViewMotion): string {
const diffLines = amendment.diffLines;
if (!!diffLines) {
if (diffLines) {
return diffLines
.map(diffLine => {
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 content = this.selectedCategory.getTitle();
if (await this.promptService.open(title, content)) {
await this.repo.numberMotionsInCategory(this.selectedCategory).then(null, this.raiseError);
await this.repo.numberMotionsInCategory(this.selectedCategory).catch(this.raiseError);
}
}
}

View File

@ -118,11 +118,11 @@
<os-projector-button *ngIf="block" [object]="block" [menuItem]="true"></os-projector-button>
<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>
<span translate>Add to agenda</span>
</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>
<span translate>Remove from agenda</span>
</button>

View File

@ -254,10 +254,10 @@ export class MotionBlockDetailComponent extends BaseListViewComponent<ViewMotion
}
public addToAgenda(): void {
this.itemRepo.addItemToAgenda(this.block).then(null, this.raiseError);
this.itemRepo.addItemToAgenda(this.block).catch(this.raiseError);
}
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
*/
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
*/
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();
const title = this.translate.instant('Are you sure you want to delete this change recommendation?');
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>
<!-- Add/remove to/from agenda -->
<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>
<span translate>Add to agenda</span>
</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>
<span translate>Remove from agenda</span>
</button>
@ -614,7 +614,6 @@
autofocus
placeholder="{{ 'Identifier' | translate }}"
formControlName="identifier"
[value]="motionCopy.identifier || ''"
/>
</mat-form-field>
</div>
@ -626,7 +625,6 @@
matInput
placeholder="{{ 'Title' | translate }}"
formControlName="title"
[value]="motionCopy.title"
required
/>
<mat-error>{{ 'The title is required' | translate }}</mat-error>
@ -844,7 +842,6 @@
matInput
placeholder="{{ 'Origin' | translate }}"
formControlName="origin"
[value]="motionCopy.origin"
/>
</mat-form-field>
</div>

View File

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

View File

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

View File

@ -177,7 +177,7 @@ export class MotionExportDialogComponent implements OnInit {
* @param nextState The next state the button will assume.
*/
private changeStateOfButton(button: MatButtonToggle, nextState: boolean): void {
if (!!button) {
if (button) {
button.disabled = nextState;
}
}
@ -245,7 +245,7 @@ export class MotionExportDialogComponent implements OnInit {
// restore selection or set default
this.store.get<MotionExportInfo>('motion_export_selection').then(restored => {
if (!!restored) {
if (restored) {
this.exportForm.patchValue(restored);
} else {
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
*/
private updateStorageKey(): void {
if (!!this._parentMotionId) {
if (this._parentMotionId) {
this.storageKey = `${this.keyPrefix}_parentId_${this._parentMotionId}`;
} else {
this.storageKey = this.keyPrefix;
@ -104,7 +104,7 @@ export class AmendmentFilterListService extends MotionFilterListService {
*/
protected preFilter(motions: ViewMotion[]): ViewMotion[] {
return motions.filter(motion => {
if (!!this._parentMotionId) {
if (this._parentMotionId) {
return motion.parent_id === this._parentMotionId;
} else {
return !!motion.parent_id;

View File

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

View File

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

View File

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

View File

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

View File

@ -139,6 +139,6 @@ export class PresentationControlComponent extends BaseViewComponent {
private updateElement(element: MediafileProjectorElement): void {
const idElement = this.slideManager.getIdentifialbeProjectorElement(element);
this.projectorService.updateElement(this.projector.projector, idElement).then(null, this.raiseError);
this.projectorService.updateElement(this.projector.projector, idElement).catch(this.raiseError);
}
}

View File

@ -125,7 +125,7 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
* @param step (optional) The amount of steps to make.
*/
public scroll(direction: ScrollScaleDirection, step: number = 1): void {
this.repo.scroll(this.projector, direction, step).then(null, this.raiseError);
this.repo.scroll(this.projector, direction, step).catch(this.raiseError);
}
/**
@ -135,29 +135,29 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
* @param step (optional) The amount of steps to make.
*/
public scale(direction: ScrollScaleDirection, step: number = 1): void {
this.repo.scale(this.projector, direction, step).then(null, this.raiseError);
this.repo.scale(this.projector, direction, step).catch(this.raiseError);
}
public projectNextSlide(): void {
this.projectorService.projectNextSlide(this.projector.projector).then(null, this.raiseError);
this.projectorService.projectNextSlide(this.projector.projector).catch(this.raiseError);
}
public projectPreviousSlide(): void {
this.projectorService.projectPreviousSlide(this.projector.projector).then(null, this.raiseError);
this.projectorService.projectPreviousSlide(this.projector.projector).catch(this.raiseError);
}
public onSortingChange(event: CdkDragDrop<ProjectorElement>): void {
moveItemInArray(this.projector.elements_preview, event.previousIndex, event.currentIndex);
this.projectorService.savePreview(this.projector.projector).then(null, this.raiseError);
this.projectorService.savePreview(this.projector.projector).catch(this.raiseError);
}
public removePreviewElement(elementIndex: number): void {
this.projector.elements_preview.splice(elementIndex, 1);
this.projectorService.savePreview(this.projector.projector).then(null, this.raiseError);
this.projectorService.savePreview(this.projector.projector).catch(this.raiseError);
}
public projectNow(elementIndex: number): void {
this.projectorService.projectPreviewSlide(this.projector.projector, elementIndex).then(null, this.raiseError);
this.projectorService.projectPreviewSlide(this.projector.projector, elementIndex).catch(this.raiseError);
}
public getSlideTitle(element: ProjectorElement): string {
@ -182,7 +182,7 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
public unprojectCurrent(element: ProjectorElement): void {
const idElement = this.slideManager.getIdentifialbeProjectorElement(element);
this.projectorService.removeFrom(this.projector.projector, idElement).then(null, this.raiseError);
this.projectorService.removeFrom(this.projector.projector, idElement).catch(this.raiseError);
}
public isClosProjected(stable: boolean): boolean {

View File

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

View File

@ -5,7 +5,7 @@
</div>
</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;
<mat-form-field>
<mat-select
@ -20,7 +20,7 @@
</mat-form-field>
</mat-card>
<mat-card *ngIf="projectorToCreate">
<mat-card *ngIf="showCreateForm">
<mat-card-title translate>New Projector</mat-card-title>
<mat-card-content>
<form [formGroup]="createForm" (keydown)="keyDownFunction($event)">
@ -38,7 +38,7 @@
<button mat-button (click)="create()">
<span translate>Create</span>
</button>
<button mat-button (click)="projectorToCreate = null">
<button mat-button (click)="showCreateForm = false">
<span translate>Cancel</span>
</button>
</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.
*/
public projectorToCreate: Projector | null;
public showCreateForm = false;
/**
* The create form.
@ -126,8 +126,8 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit,
* Opens the create form.
*/
public onPlusButton(): void {
if (!this.projectorToCreate) {
this.projectorToCreate = new Projector();
if (!this.showCreateForm) {
this.showCreateForm = true;
this.createForm.setValue({ name: '' });
}
}
@ -136,13 +136,13 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit,
* Creates the comment section from the create form.
*/
public create(): void {
if (this.createForm.valid && this.projectorToCreate) {
this.projectorToCreate.patchValues(this.createForm.value as Projector);
this.projectorToCreate.patchValues({
if (this.createForm.valid && this.showCreateForm) {
const projector: Partial<Projector> = {
name: this.createForm.value.name,
reference_projector_id: this.projectors[0].reference_projector_id
});
this.repo.create(this.projectorToCreate).then(() => {
this.projectorToCreate = null;
};
this.repo.create(projector).then(() => {
this.showCreateForm = false;
this.cd.detectChanges();
}, this.raiseError);
}
@ -158,7 +158,7 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit,
this.create();
}
if (event.key === 'Escape') {
this.projectorToCreate = null;
this.showCreateForm = null;
}
}
@ -169,6 +169,6 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit,
const promises = this.projectors.map(projector => {
return this.repo.update(update, projector);
});
Promise.all(promises).then(null, this.raiseError);
Promise.all(promises).catch(this.raiseError);
}
}

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