diff --git a/client/src/app/core/app-config.ts b/client/src/app/core/app-config.ts index cf278331b..487042193 100644 --- a/client/src/app/core/app-config.ts +++ b/client/src/app/core/app-config.ts @@ -1,20 +1,23 @@ +import { Type } from '@angular/core'; + import { ModelConstructor, BaseModel } from '../shared/models/base/base-model'; import { MainMenuEntry } from './core-services/main-menu.service'; -import { Searchable } from '../shared/models/base/searchable'; -import { Type } from '@angular/core'; +import { Searchable } from '../site/base/searchable'; import { BaseRepository } from './repositories/base-repository'; +import { BaseViewModel, ViewModelConstructor } from 'app/site/base/base-view-model'; interface BaseModelEntry { collectionString: string; repository: Type>; -} - -export interface ModelEntry extends BaseModelEntry { model: ModelConstructor; } +export interface ModelEntry extends BaseModelEntry { + viewModel: ViewModelConstructor; +} + export interface SearchableModelEntry extends BaseModelEntry { - model: new (...args: any[]) => BaseModel & Searchable; + viewModel: new (...args: any[]) => BaseViewModel & Searchable; searchOrder: number; } diff --git a/client/src/app/core/core-services/app-load.service.ts b/client/src/app/core/core-services/app-load.service.ts index e56d5e38e..42d1ee659 100644 --- a/client/src/app/core/core-services/app-load.service.ts +++ b/client/src/app/core/core-services/app-load.service.ts @@ -14,9 +14,11 @@ import { TagAppConfig } from '../../site/tags/tag.config'; import { MainMenuService } from './main-menu.service'; import { HistoryAppConfig } from 'app/site/history/history.config'; import { SearchService } from '../ui-services/search.service'; -import { isSearchable } from '../../shared/models/base/searchable'; +import { isSearchable } from '../../site/base/searchable'; import { ProjectorAppConfig } from 'app/site/projector/projector.config'; import { BaseRepository } from 'app/core/repositories/base-repository'; +import { OnAfterAppsLoaded } from '../onAfterAppsLoaded'; +import { ServicesToLoadOnAppsLoaded } from '../core.module'; /** * A list of all app configurations of all delivered apps. @@ -63,16 +65,21 @@ export class AppLoadService { const plugin = await import('../../../../../plugins/' + pluginName + '/' + pluginName); plugin.main(); }*/ + const repositories: OnAfterAppsLoaded[] = []; appConfigs.forEach((config: AppConfig) => { if (config.models) { config.models.forEach(entry => { let repository: BaseRepository = null; - if (entry.repository) { - repository = this.injector.get(entry.repository); - } - this.modelMapper.registerCollectionElement(entry.collectionString, entry.model, repository); + repository = this.injector.get(entry.repository); + repositories.push(repository); + this.modelMapper.registerCollectionElement( + entry.collectionString, + entry.model, + entry.viewModel, + repository + ); if (this.isSearchableModelEntry(entry)) { - this.searchService.registerModel(entry.collectionString, entry.model, entry.searchOrder); + this.searchService.registerModel(entry.collectionString, entry.viewModel, entry.searchOrder); } }); } @@ -80,6 +87,16 @@ export class AppLoadService { this.mainMenuService.registerEntries(config.mainMenuEntries); } }); + + // Collect all services to notify for the OnAfterAppsLoadedHook + const onAfterAppsLoadedItems = ServicesToLoadOnAppsLoaded.map(service => { + return this.injector.get(service); + }).concat(repositories); + + // Notify them. + onAfterAppsLoadedItems.forEach(repo => { + repo.onAfterAppsLoaded(); + }); } private isSearchableModelEntry(entry: ModelEntry | SearchableModelEntry): entry is SearchableModelEntry { @@ -87,7 +104,7 @@ export class AppLoadService { // We need to double check, because Typescipt cannot check contructors. If typescript could differentiate // between (ModelConstructor) and (new (...args: any[]) => (BaseModel & Searchable)), we would not have // to check if the result of the contructor (the model instance) is really a searchable. - if (!isSearchable(new entry.model())) { + if (!isSearchable(new entry.viewModel())) { throw Error( `Wrong configuration for ${ entry.collectionString diff --git a/client/src/app/core/core-services/autoupdate.service.ts b/client/src/app/core/core-services/autoupdate.service.ts index 09e2bd485..8873789d0 100644 --- a/client/src/app/core/core-services/autoupdate.service.ts +++ b/client/src/app/core/core-services/autoupdate.service.ts @@ -120,7 +120,6 @@ export class AutoupdateService extends OpenSlidesComponent { await this.DS.add(this.mapObjectsToBaseModels(collection, autoupdate.changed[collection])); } - console.log('new max change id', autoupdate.to_change_id); await this.DS.flushToStorage(autoupdate.to_change_id); } else { // autoupdate fully in the future. we are missing something! @@ -135,7 +134,7 @@ export class AutoupdateService extends OpenSlidesComponent { * @returns A list of basemodels constructed from the given models. */ private mapObjectsToBaseModels(collection: string, models: object[]): BaseModel[] { - const targetClass = this.modelMapper.getModelConstructor(collection); + const targetClass = this.modelMapper.getModelConstructorFromCollectionString(collection); if (!targetClass) { throw new Error(`Unregistered resource ${collection}`); } diff --git a/client/src/app/core/core-services/collectionStringMapper.service.ts b/client/src/app/core/core-services/collectionStringMapper.service.ts index b69ae9110..81a348bd9 100644 --- a/client/src/app/core/core-services/collectionStringMapper.service.ts +++ b/client/src/app/core/core-services/collectionStringMapper.service.ts @@ -2,64 +2,134 @@ import { Injectable } from '@angular/core'; import { ModelConstructor, BaseModel } from '../../shared/models/base/base-model'; import { BaseRepository } from 'app/core/repositories/base-repository'; +import { ViewModelConstructor, BaseViewModel } from 'app/site/base/base-view-model'; /** - * Registeres the mapping of collection strings <--> actual types. Every Model should register itself here. + * Holds a mapping entry with the matching collection string, + * model constructor, view model constructor and the repository + */ +type MappingEntry = [ + string, + ModelConstructor, + ViewModelConstructor, + BaseRepository +]; + +/** + * Registeres the mapping between collection strings, models constructors, view + * model constructors and repositories. + * All models ned to be registered! */ @Injectable({ providedIn: 'root' }) export class CollectionStringMapperService { /** - * Mapps collection strings to model constructors. Accessed by {@method registerCollectionElement} and - * {@method getCollectionStringType}. + * Maps collection strings to mapping entries */ - private collectionStringsTypeMapping: { - [collectionString: string]: [ModelConstructor, BaseRepository]; + private collectionStringMapping: { + [collectionString: string]: MappingEntry; } = {}; /** - * Constructor to create the NotifyService. Registers itself to the WebsocketService. - * @param websocketService + * Maps models to mapping entries */ + private modelMapping: { + [modelName: string]: MappingEntry; + } = {}; + + /** + * Maps view models to mapping entries + */ + private viewModelMapping: { + [viewModelname: string]: MappingEntry; + } = {}; + + /** + * Maps repositories to mapping entries + */ + private repositoryMapping: { + [repositoryName: string]: MappingEntry; + } = {}; + public constructor() {} /** - * Registers the type to the collection string + * Registers the combination of a collection string, model, view model and repository * @param collectionString * @param model */ - public registerCollectionElement( + public registerCollectionElement( collectionString: string, - model: ModelConstructor, - repository: BaseRepository + model: ModelConstructor, + viewModel: ViewModelConstructor, + repository: BaseRepository ): void { - this.collectionStringsTypeMapping[collectionString] = [model, repository]; + const entry: MappingEntry = [collectionString, model, viewModel, repository]; + this.collectionStringMapping[collectionString] = entry; + this.modelMapping[model.name] = entry; + this.viewModelMapping[viewModel.name] = entry; + this.repositoryMapping[repository.name] = entry; } - /** - * Returns the constructor of the requested collection or undefined, if it is not registered. - * @param collectionString the requested collection - */ - public getModelConstructor(collectionString: string): ModelConstructor { - return this.collectionStringsTypeMapping[collectionString][0]; + // The following accessors are for giving one of EntryType by given a different object + // of EntryType. + + public getCollectionStringFromModelConstructor(ctor: ModelConstructor): string { + return this.modelMapping[ctor.name][0]; + } + public getCollectionStringFromViewModelConstructor(ctor: ViewModelConstructor): string { + return this.viewModelMapping[ctor.name][0]; + } + public getCollectionStringFromRepository( + repository: BaseRepository + ): string { + return this.repositoryMapping[repository.name][0]; } - /** - * Returns the repository of the requested collection or undefined, if it is not registered. - * @param collectionString the requested collection - */ - public getRepository(collectionString: string): BaseRepository { - return this.collectionStringsTypeMapping[collectionString][1]; + public getModelConstructorFromCollectionString(collectionString: string): ModelConstructor { + return this.collectionStringMapping[collectionString][1] as ModelConstructor; + } + public getModelConstructorFromViewModelConstructor( + ctor: ViewModelConstructor + ): ModelConstructor { + return this.viewModelMapping[ctor.name][1] as ModelConstructor; + } + public getModelConstructorFromRepository( + repository: BaseRepository + ): ModelConstructor { + return this.repositoryMapping[repository.name][1] as ModelConstructor; } - /** - * Returns the collection string of a given ModelConstructor or undefined, if it is not registered. - * @param ctor - */ - public getCollectionString(ctor: ModelConstructor): string { - return Object.keys(this.collectionStringsTypeMapping).find((collectionString: string) => { - return ctor === this.collectionStringsTypeMapping[collectionString][0]; - }); + public getViewModelConstructorFromCollectionString( + collectionString: string + ): ViewModelConstructor { + return this.collectionStringMapping[collectionString][2] as ViewModelConstructor; + } + public getViewModelConstructorFromModelConstructor( + ctor: ModelConstructor + ): ViewModelConstructor { + return this.modelMapping[ctor.name][2] as ViewModelConstructor; + } + public getViewModelConstructorFromRepository( + repository: BaseRepository + ): ViewModelConstructor { + return this.repositoryMapping[repository.name][2] as ViewModelConstructor; + } + + public getRepositoryFromCollectionString( + collectionString: string + ): BaseRepository { + return this.collectionStringMapping[collectionString][3] as BaseRepository; + } + public getRepositoryFromModelConstructor( + ctor: ModelConstructor + ): BaseRepository { + return this.modelMapping[ctor.name][3] as BaseRepository; + } + public getRepositoryFromViewModelConstructor( + ctor: ViewModelConstructor + ): BaseRepository { + return this.viewModelMapping[ctor.name][3] as BaseRepository; } } diff --git a/client/src/app/core/core-services/data-store.service.ts b/client/src/app/core/core-services/data-store.service.ts index f106700f6..895f0c1ab 100644 --- a/client/src/app/core/core-services/data-store.service.ts +++ b/client/src/app/core/core-services/data-store.service.ts @@ -72,6 +72,18 @@ export class DataStoreService { */ private readonly changedSubject: Subject = new Subject(); + /** + * This is subject notify all subscribers _before_ the `secondaryModelChangeSubject`. + * It's the same subject as the changedSubject. + */ + public readonly primaryModelChangeSubject = new Subject(); + + /** + * This is subject notify all subscribers _after_ the `primaryModelChangeSubject`. + * It's the same subject as the changedSubject. + */ + public readonly secondaryModelChangeSubject = new Subject(); + /** * Observe the datastore for changes. * @@ -127,7 +139,12 @@ export class DataStoreService { * @param storageService use StorageService to preserve the DataStore. * @param modelMapper */ - public constructor(private storageService: StorageService, private modelMapper: CollectionStringMapperService) {} + public constructor(private storageService: StorageService, private modelMapper: CollectionStringMapperService) { + this.changeObservable.subscribe(model => { + this.primaryModelChangeSubject.next(model); + this.secondaryModelChangeSubject.next(model); + }); + } /** * Gets the DataStore from cache and instantiate all models out of the serialized version. @@ -170,7 +187,7 @@ export class DataStoreService { const storage: ModelStorage = {}; Object.keys(serializedStore).forEach(collectionString => { storage[collectionString] = {} as ModelCollection; - const target = this.modelMapper.getModelConstructor(collectionString); + const target = this.modelMapper.getModelConstructorFromCollectionString(collectionString); if (target) { Object.keys(serializedStore[collectionString]).forEach(id => { const data = JSON.parse(serializedStore[collectionString][id]); @@ -201,7 +218,7 @@ export class DataStoreService { if (typeof collectionType === 'string') { return collectionType; } else { - return this.modelMapper.getCollectionString(collectionType); + return this.modelMapper.getCollectionStringFromModelConstructor(collectionType); } } diff --git a/client/src/app/core/core-services/operator.service.ts b/client/src/app/core/core-services/operator.service.ts index fbefe381c..132a852fc 100644 --- a/client/src/app/core/core-services/operator.service.ts +++ b/client/src/app/core/core-services/operator.service.ts @@ -9,6 +9,10 @@ import { User } from '../../shared/models/users/user'; import { environment } from 'environments/environment'; import { DataStoreService } from './data-store.service'; import { OfflineService } from './offline.service'; +import { ViewUser } from 'app/site/users/models/view-user'; +import { CollectionStringMapperService } from './collectionStringMapper.service'; +import { OnAfterAppsLoaded } from '../onAfterAppsLoaded'; +import { UserRepositoryService } from '../repositories/users/user-repository.service'; /** * Permissions on the client are just strings. This makes clear, that @@ -36,12 +40,19 @@ export interface WhoAmIResponse { @Injectable({ providedIn: 'root' }) -export class OperatorService extends OpenSlidesComponent { +export class OperatorService extends OpenSlidesComponent implements OnAfterAppsLoaded { /** * The operator. */ private _user: User; + /** + * The operator as a view user. We need a separation here, because + * we need to acces the operators permissions, before we get data + * from the server to build the view user. + */ + private _viewUser: ViewUser; + /** * Get the user that corresponds to operator. */ @@ -49,14 +60,20 @@ export class OperatorService extends OpenSlidesComponent { return this._user; } + /** + * Get the user that corresponds to operator. + */ + public get viewUser(): ViewUser { + return this._viewUser; + } + /** * Sets the current operator. * * The permissions are updated and the new user published. */ public set user(user: User) { - this._user = user; - this.updatePermissions(); + this.updateUser(user); } public get isAnonymous(): boolean { @@ -78,6 +95,16 @@ export class OperatorService extends OpenSlidesComponent { */ private operatorSubject: BehaviorSubject = new BehaviorSubject(null); + /** + * Subject for the operator as a view user. + */ + private viewOperatorSubject: BehaviorSubject = new BehaviorSubject(null); + + /** + * The user repository. Will be filled by the `onAfterAppsLoaded`. + */ + private userRepository: UserRepositoryService; + /** * Sets up an observer for watching changes in the DS. If the operator user or groups are changed, * the operator's permissions are updated. @@ -86,7 +113,12 @@ export class OperatorService extends OpenSlidesComponent { * @param DS * @param offlineService */ - public constructor(private http: HttpClient, private DS: DataStoreService, private offlineService: OfflineService) { + public constructor( + private http: HttpClient, + private DS: DataStoreService, + private offlineService: OfflineService, + private collectionStringMapperService: CollectionStringMapperService + ) { super(); this.DS.changeObservable.subscribe(newModel => { @@ -96,8 +128,7 @@ export class OperatorService extends OpenSlidesComponent { } if (newModel instanceof User && this._user.id === newModel.id) { - this._user = newModel; - this.updatePermissions(); + this.updateUser(newModel); } } else if (newModel instanceof Group && newModel.id === 1) { // Group 1 (default) for anonymous changed @@ -106,6 +137,33 @@ export class OperatorService extends OpenSlidesComponent { }); } + /** + * Load the repo to get a view user. + */ + public onAfterAppsLoaded(): void { + this.userRepository = this.collectionStringMapperService.getRepositoryFromModelConstructor( + User + ) as UserRepositoryService; + if (this.user) { + this._viewUser = this.userRepository.getViewModel(this.user.id); + } + } + + /** + * Updates the user and update the permissions. + * + * @param user The user to set. + */ + private updateUser(user: User | null): void { + this._user = user; + if (user && this.userRepository) { + this._viewUser = this.userRepository.getViewModel(user.id); + } else { + this._viewUser = null; + } + this.updatePermissions(); + } + /** * Calls `/apps/users/whoami` to find out the real operator. * @returns The response of the WhoAmI request. @@ -131,10 +189,14 @@ export class OperatorService extends OpenSlidesComponent { * Services an components can use it to get informed when something changes in * the operator */ - public getObservable(): Observable { + public getUserObservable(): Observable { return this.operatorSubject.asObservable(); } + public getViewUserObservable(): Observable { + return this.viewOperatorSubject.asObservable(); + } + /** * Checks, if the operator has at least one of the given permissions. * @param checkPerms The permissions to check, if at least one matches. @@ -193,5 +255,6 @@ export class OperatorService extends OpenSlidesComponent { } // publish changes in the operator. this.operatorSubject.next(this.user); + this.viewOperatorSubject.next(this.viewUser); } } diff --git a/client/src/app/core/core-services/projector.service.ts b/client/src/app/core/core-services/projector.service.ts index 6926cc35a..b98a5e096 100644 --- a/client/src/app/core/core-services/projector.service.ts +++ b/client/src/app/core/core-services/projector.service.ts @@ -17,6 +17,8 @@ import { import { HttpService } from './http.service'; import { SlideManager } from 'app/slides/services/slide-manager.service'; import { BaseModel } from 'app/shared/models/base/base-model'; +import { BaseViewModel } from 'app/site/base/base-view-model'; +import { ViewModelStoreService } from './view-model-store.service'; /** * This service cares about Projectables being projected and manage all projection-related @@ -34,7 +36,12 @@ export class ProjectorService extends OpenSlidesComponent { * @param DS * @param dataSend */ - public constructor(private DS: DataStoreService, private http: HttpService, private slideManager: SlideManager) { + public constructor( + private DS: DataStoreService, + private http: HttpService, + private slideManager: SlideManager, + private viewModelStore: ViewModelStoreService + ) { super(); } @@ -222,13 +229,12 @@ export class ProjectorService extends OpenSlidesComponent { } /** - * Returns a model associated with the identifiable projector element. Throws an error, - * if the element is not mappable. + * Asserts, that the given element is mappable to a model or view model. + * Throws an error, if this assertion fails. * - * @param element The projector element - * @returns the model from the projector element + * @param element The element to check */ - public getModelFromProjectorElement(element: IdentifiableProjectorElement): T { + private assertElementIsMappable(element: IdentifiableProjectorElement): void { if (!this.slideManager.canSlideBeMappedToModel(element.name)) { throw new Error('This projector element cannot be mapped to a model'); } @@ -236,9 +242,32 @@ export class ProjectorService extends OpenSlidesComponent { if (!identifiers.includes('name') || !identifiers.includes('id')) { throw new Error('To map this element to a model, a name and id is needed.'); } + } + + /** + * Returns a model associated with the identifiable projector element. Throws an error, + * if the element is not mappable. + * + * @param element The projector element + * @returns the model from the projector element + */ + public getModelFromProjectorElement(element: IdentifiableProjectorElement): T { + this.assertElementIsMappable(element); return this.DS.get(element.name, element.id); } + /** + * Returns a view model associated with the identifiable projector element. Throws an error, + * if the element is not mappable. + * + * @param element The projector element + * @returns the view model from the projector element + */ + public getViewModelFromProjectorElement(element: IdentifiableProjectorElement): T { + this.assertElementIsMappable(element); + return this.viewModelStore.get(element.name, element.id); + } + /** * Projects the next slide in the queue. Moves all currently projected * non-stable slides to the history. diff --git a/client/src/app/core/core-services/time-travel.service.ts b/client/src/app/core/core-services/time-travel.service.ts index e36e155c7..86de9a804 100644 --- a/client/src/app/core/core-services/time-travel.service.ts +++ b/client/src/app/core/core-services/time-travel.service.ts @@ -68,7 +68,7 @@ export class TimeTravelService { [collectionString, id] = historyObject.element_id.split(':'); if (historyObject.full_data) { - const targetClass = this.modelMapperService.getModelConstructor(collectionString); + const targetClass = this.modelMapperService.getModelConstructorFromCollectionString(collectionString); await this.DS.add([new targetClass(historyObject.full_data)]); } else { await this.DS.remove(collectionString, [+id]); diff --git a/client/src/app/core/core-services/view-model-store.service.spec.ts b/client/src/app/core/core-services/view-model-store.service.spec.ts new file mode 100644 index 000000000..73284ca13 --- /dev/null +++ b/client/src/app/core/core-services/view-model-store.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, inject } from '@angular/core/testing'; +import { ViewModelStoreService } from './view-model-store.service'; +import { E2EImportsModule } from '../../../e2e-imports.module'; + +describe('ViewModelStoreService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule], + providers: [ViewModelStoreService] + }); + }); + it('should be created', inject([ViewModelStoreService], (service: ViewModelStoreService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/client/src/app/core/core-services/view-model-store.service.ts b/client/src/app/core/core-services/view-model-store.service.ts new file mode 100644 index 000000000..941aafbf2 --- /dev/null +++ b/client/src/app/core/core-services/view-model-store.service.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@angular/core'; +import { CollectionStringMapperService } from './collectionStringMapper.service'; +import { BaseViewModel, ViewModelConstructor } from 'app/site/base/base-view-model'; +import { BaseRepository } from '../repositories/base-repository'; + +/** + * This service takes care of handling view models. + */ +@Injectable({ + providedIn: 'root' +}) +export class ViewModelStoreService { + /** + * @param mapperService + */ + public constructor(private mapperService: CollectionStringMapperService) {} + + /** + * gets the repository from a collection string or a view model constructor. + * + * @param collectionType The collection string or constructor. + */ + private getRepository( + collectionType: ViewModelConstructor | string + ): BaseRepository { + if (typeof collectionType === 'string') { + return this.mapperService.getRepositoryFromCollectionString(collectionType) as BaseRepository; + } else { + return this.mapperService.getRepositoryFromViewModelConstructor(collectionType as ViewModelConstructor); + } + } + + /** + * Returns the view model identified by the collectionString and id + * + * @param collectionString The collection of the view model + * @param id The id of the view model + */ + public get(collectionType: ViewModelConstructor | string, id: number): T { + return this.getRepository(collectionType).getViewModel(id); + } + + /** + * Returns all view models for the given ids. + * + * @param collectionType The collection of the view model + * @param ids All ids to match + */ + public getMany(collectionType: ViewModelConstructor | string, ids: number[]): T[] { + const repository = this.getRepository(collectionType); + + return ids + .map(id => { + return repository.getViewModel(id); + }) + .filter(model => !!model); // remove non valid models. + } + + /** + * Gets all view models from a collection + * + * @param collectionString The collection + * @returns all models from the collection + */ + public getAll(collectionType: ViewModelConstructor | string): T[] { + return this.getRepository(collectionType).getViewModelList(); + } + + /** + * Get all view modles from a collection, that satisfy the callback + * + * @param collectionString The collection + * @param callback The function to check + * @returns all matched view models of the collection + */ + public filter(collectionString: string, callback: (model: T) => boolean): T[] { + return this.getAll(collectionString).filter(callback); + } + + /** + * Finds one view model from the collection, that satifies the callback + * + * @param collectionString The collection + * @param callback THe callback to satisfy + * @returns a found view model or null, if nothing was found. + */ + public find(collectionString: string, callback: (model: T) => boolean): T { + return this.getAll(collectionString).find(callback); + } +} diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts index f7d2e7da1..f3fbf837a 100644 --- a/client/src/app/core/core.module.ts +++ b/client/src/app/core/core.module.ts @@ -1,4 +1,4 @@ -import { NgModule, Optional, SkipSelf } from '@angular/core'; +import { NgModule, Optional, SkipSelf, Type } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Title } from '@angular/platform-browser'; @@ -6,6 +6,10 @@ import { Title } from '@angular/platform-browser'; import { PromptDialogComponent } from '../shared/components/prompt-dialog/prompt-dialog.component'; import { ChoiceDialogComponent } from '../shared/components/choice-dialog/choice-dialog.component'; import { ProjectionDialogComponent } from 'app/shared/components/projection-dialog/projection-dialog.component'; +import { OperatorService } from './core-services/operator.service'; +import { OnAfterAppsLoaded } from './onAfterAppsLoaded'; + +export const ServicesToLoadOnAppsLoaded: Type[] = [OperatorService]; /** * Global Core Module. diff --git a/client/src/app/core/onAfterAppsLoaded.ts b/client/src/app/core/onAfterAppsLoaded.ts new file mode 100644 index 000000000..4ce74d849 --- /dev/null +++ b/client/src/app/core/onAfterAppsLoaded.ts @@ -0,0 +1,9 @@ +/** + * A lifecyclehook to be called, after all apps are loaded. + */ +export interface OnAfterAppsLoaded { + /** + * The hook to call + */ + onAfterAppsLoaded(): void; +} diff --git a/client/src/app/core/repositories/agenda/agenda-repository.service.spec.ts b/client/src/app/core/repositories/agenda/item-repository.service.spec.ts similarity index 52% rename from client/src/app/core/repositories/agenda/agenda-repository.service.spec.ts rename to client/src/app/core/repositories/agenda/item-repository.service.spec.ts index ab4c893ae..7b99f19d3 100644 --- a/client/src/app/core/repositories/agenda/agenda-repository.service.spec.ts +++ b/client/src/app/core/repositories/agenda/item-repository.service.spec.ts @@ -1,17 +1,17 @@ import { TestBed, inject } from '@angular/core/testing'; -import { AgendaRepositoryService } from './agenda-repository.service'; +import { ItemRepositoryService } from './item-repository.service'; import { E2EImportsModule } from 'e2e-imports.module'; -describe('AgendaRepositoryService', () => { +describe('ItemRepositoryService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [E2EImportsModule], - providers: [AgendaRepositoryService] + providers: [ItemRepositoryService] }); }); - it('should be created', inject([AgendaRepositoryService], (service: AgendaRepositoryService) => { + it('should be created', inject([ItemRepositoryService], (service: ItemRepositoryService) => { expect(service).toBeTruthy(); })); }); diff --git a/client/src/app/core/repositories/agenda/agenda-repository.service.ts b/client/src/app/core/repositories/agenda/item-repository.service.ts similarity index 91% rename from client/src/app/core/repositories/agenda/agenda-repository.service.ts rename to client/src/app/core/repositories/agenda/item-repository.service.ts index d448c6f15..bb477ca69 100644 --- a/client/src/app/core/repositories/agenda/agenda-repository.service.ts +++ b/client/src/app/core/repositories/agenda/item-repository.service.ts @@ -3,8 +3,6 @@ import { tap, map } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { BaseRepository } from '../base-repository'; -import { AgendaBaseModel } from 'app/shared/models/base/agenda-base-model'; -import { BaseModel } from 'app/shared/models/base/base-model'; import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service'; import { ConfigService } from 'app/core/ui-services/config.service'; import { DataSendService } from 'app/core/core-services/data-send.service'; @@ -14,10 +12,13 @@ import { Identifiable } from 'app/shared/models/base/identifiable'; import { Item } from 'app/shared/models/agenda/item'; import { OSTreeSortEvent } from 'app/shared/components/sorting-tree/sorting-tree.component'; import { Speaker } from 'app/shared/models/agenda/speaker'; -import { User } from 'app/shared/models/users/user'; import { ViewItem } from 'app/site/agenda/models/view-item'; import { ViewSpeaker } from 'app/site/agenda/models/view-speaker'; import { TreeService } from 'app/core/ui-services/tree.service'; +import { BaseAgendaViewModel } from 'app/site/base/base-agenda-view-model'; +import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; +import { BaseViewModel } from 'app/site/base/base-view-model'; +import { ViewUser } from 'app/site/users/models/view-user'; /** * Repository service for users @@ -27,7 +28,7 @@ import { TreeService } from 'app/core/ui-services/tree.service'; @Injectable({ providedIn: 'root' }) -export class AgendaRepositoryService extends BaseRepository { +export class ItemRepositoryService extends BaseRepository { /** * Contructor for agenda repository. * @@ -39,38 +40,39 @@ export class AgendaRepositoryService extends BaseRepository { * @param treeService sort the data according to weight and parents */ public constructor( - protected DS: DataStoreService, - private httpService: HttpService, + DS: DataStoreService, mapperService: CollectionStringMapperService, + viewModelStoreService: ViewModelStoreService, + private httpService: HttpService, private config: ConfigService, private dataSend: DataSendService, private treeService: TreeService ) { - super(DS, mapperService, Item); + super(DS, mapperService, viewModelStoreService, Item); } /** - * Returns the corresponding content object to a given {@link Item} as an {@link AgendaBaseModel} + * Returns the corresponding content object to a given {@link Item} as an {@link AgendaBaseViewModel} * Used dynamically because of heavy race conditions * * @param agendaItem the target agenda Item * @returns the content object of the given item. Might be null if it was not found. */ - public getContentObject(agendaItem: Item): AgendaBaseModel { - const contentObject = this.DS.get( + public getContentObject(agendaItem: Item): BaseAgendaViewModel { + const contentObject = this.viewModelStoreService.get( agendaItem.content_object.collection, agendaItem.content_object.id ); if (!contentObject) { return null; } - if (contentObject instanceof AgendaBaseModel) { - return contentObject as AgendaBaseModel; + if (contentObject instanceof BaseAgendaViewModel) { + return contentObject as BaseAgendaViewModel; } else { throw new Error( `The content object (${agendaItem.content_object.collection}, ${ agendaItem.content_object.id - }) of item ${agendaItem.id} is not a AgendaBaseModel.` + }) of item ${agendaItem.id} is not a AgendaBaseViewModel.` ); } } @@ -86,7 +88,7 @@ export class AgendaRepositoryService extends BaseRepository { const speakers = item.speakers; if (speakers && speakers.length > 0) { speakers.forEach((speaker: Speaker) => { - const user = this.DS.get(User, speaker.user_id); + const user = this.viewModelStoreService.get(ViewUser, speaker.user_id); viewSpeakers.push(new ViewSpeaker(speaker, user)); }); } diff --git a/client/src/app/core/repositories/agenda/topic-repository.service.ts b/client/src/app/core/repositories/agenda/topic-repository.service.ts index 3281d0a5f..8e4e3d83d 100644 --- a/client/src/app/core/repositories/agenda/topic-repository.service.ts +++ b/client/src/app/core/repositories/agenda/topic-repository.service.ts @@ -10,6 +10,9 @@ import { ViewTopic } from 'app/site/agenda/models/view-topic'; import { Identifiable } from 'app/shared/models/base/identifiable'; import { CollectionStringMapperService } from 'app/core/core-services/collectionStringMapper.service'; import { CreateTopic } from 'app/site/agenda/models/create-topic'; +import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; +import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; +import { ViewItem } from 'app/site/agenda/models/view-item'; /** * Repository for topics @@ -28,9 +31,10 @@ export class TopicRepositoryService extends BaseRepository { public constructor( DS: DataStoreService, mapperService: CollectionStringMapperService, + viewModelStoreService: ViewModelStoreService, private dataSend: DataSendService ) { - super(DS, mapperService, Topic, [Mediafile, Item]); + super(DS, mapperService, viewModelStoreService, Topic, [Mediafile, Item]); } /** @@ -40,22 +44,11 @@ export class TopicRepositoryService extends BaseRepository { * @returns a new view topic */ public createViewModel(topic: Topic): ViewTopic { - const attachments = this.DS.getMany(Mediafile, topic.attachments_id); - const item = this.getAgendaItem(topic); + const attachments = this.viewModelStoreService.getMany(ViewMediafile, topic.attachments_id); + const item = this.viewModelStoreService.get(ViewItem, topic.agenda_item_id); return new ViewTopic(topic, attachments, item); } - /** - * Gets the corresponding agendaItem to the topic. - * Used to deal with race conditions - * - * @param topic the topic for the agenda item - * @returns an agenda item that fits for the topic - */ - public getAgendaItem(topic: Topic): Item { - return this.DS.get(Item, topic.agenda_item_id); - } - /** * Save a new topic * diff --git a/client/src/app/core/repositories/assignments/assignment-repository.service.ts b/client/src/app/core/repositories/assignments/assignment-repository.service.ts index 206262706..07a29da2b 100644 --- a/client/src/app/core/repositories/assignments/assignment-repository.service.ts +++ b/client/src/app/core/repositories/assignments/assignment-repository.service.ts @@ -8,6 +8,10 @@ import { BaseRepository } from '../base-repository'; import { DataStoreService } from '../../core-services/data-store.service'; import { Identifiable } from 'app/shared/models/base/identifiable'; import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service'; +import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; +import { ViewItem } from 'app/site/agenda/models/view-item'; +import { ViewUser } from 'app/site/users/models/view-user'; +import { ViewTag } from 'app/site/tags/models/view-tag'; /** * Repository Service for Assignments. @@ -24,8 +28,12 @@ export class AssignmentRepositoryService extends BaseRepository, viewAssignment: ViewAssignment): Promise { @@ -41,9 +49,9 @@ export class AssignmentRepositoryService extends BaseRepository extends OpenSlidesComponent { +export abstract class BaseRepository extends OpenSlidesComponent + implements OnAfterAppsLoaded { /** * Stores all the viewModel in an object */ @@ -29,26 +32,33 @@ export abstract class BaseRepository = new Subject(); + private _name: string; + + public get name(): string { + return this._name; + } + /** * Construction routine for the base repository * * @param DS: The DataStore - * @param collectionStringModelMapperService Mapping strings to their corresponding classes + * @param collectionStringMapperService Mapping strings to their corresponding classes * @param baseModelCtor The model constructor of which this repository is about. * @param depsModelCtors A list of constructors that are used in the view model. * If one of those changes, the view models will be updated. */ public constructor( protected DS: DataStoreService, - protected collectionStringModelMapperService: CollectionStringMapperService, + protected collectionStringMapperService: CollectionStringMapperService, + protected viewModelStoreService: ViewModelStoreService, protected baseModelCtor: ModelConstructor, protected depsModelCtors?: ModelConstructor[] ) { super(); - this.setup(); + this._name = baseModelCtor.name; } - protected setup(): void { + public onAfterAppsLoaded(): void { // Populate the local viewModelStore with ViewModel Objects. this.DS.getAll(this.baseModelCtor).forEach((model: M) => { this.viewModelStore[model.id] = this.createViewModel(model); @@ -60,28 +70,40 @@ export abstract class BaseRepository { + this.DS.primaryModelChangeSubject.subscribe(model => { if (model instanceof this.baseModelCtor) { // Add new and updated motions to the viewModelStore this.viewModelStore[model.id] = this.createViewModel(model as M); this.updateAllObservables(model.id); - } else if (this.depsModelCtors) { + } + }); + + if (this.depsModelCtors) { + this.DS.secondaryModelChangeSubject.subscribe(model => { const dependencyChanged: boolean = this.depsModelCtors.some(ctor => { return model instanceof ctor; }); if (dependencyChanged) { + const viewModel = this.viewModelStoreService.get(model.collectionString, model.id); + // if an domain object we need was added or changed, update viewModelStore - this.getViewModelList().forEach(viewModel => { - viewModel.updateValues(model); + this.getViewModelList().forEach(ownViewModel => { + ownViewModel.updateDependencies(viewModel); }); this.updateAllObservables(model.id); } - } - }); + }); + } // Watch the Observables for deleting + // TODO: What happens, if some related object was deleted? + // My quess: This must trigger an autoupdate also for this model, because some IDs changed, so the + // affected models will be newly created by the primaryModelChangeSubject. this.DS.deletedObservable.subscribe(model => { - if (model.collection === this.collectionStringModelMapperService.getCollectionString(this.baseModelCtor)) { + if ( + model.collection === + this.collectionStringMapperService.getCollectionStringFromModelConstructor(this.baseModelCtor) + ) { delete this.viewModelStore[model.id]; this.updateAllObservables(model.id); } @@ -167,8 +189,8 @@ export abstract class BaseRepository { - public constructor(DS: DataStoreService, mapperService: CollectionStringMapperService) { - super(DS, mapperService, ChatMessage); + public constructor( + DS: DataStoreService, + mapperService: CollectionStringMapperService, + viewModelStoreService: ViewModelStoreService + ) { + super(DS, mapperService, viewModelStoreService, ChatMessage); } protected createViewModel(message: ChatMessage): ViewChatMessage { diff --git a/client/src/app/site/config/services/config-repository.service.spec.ts b/client/src/app/core/repositories/config/config-repository.service.spec.ts similarity index 100% rename from client/src/app/site/config/services/config-repository.service.spec.ts rename to client/src/app/core/repositories/config/config-repository.service.spec.ts diff --git a/client/src/app/site/config/services/config-repository.service.ts b/client/src/app/core/repositories/config/config-repository.service.ts similarity index 96% rename from client/src/app/site/config/services/config-repository.service.ts rename to client/src/app/core/repositories/config/config-repository.service.ts index 2efa7308e..9fb892ddd 100644 --- a/client/src/app/site/config/services/config-repository.service.ts +++ b/client/src/app/core/repositories/config/config-repository.service.ts @@ -1,14 +1,16 @@ import { Injectable } from '@angular/core'; -import { BaseRepository } from 'app/core/repositories/base-repository'; -import { ViewConfig } from '../models/view-config'; -import { Config } from 'app/shared/models/core/config'; import { Observable, BehaviorSubject } from 'rxjs'; + +import { BaseRepository } from 'app/core/repositories/base-repository'; +import { Config } from 'app/shared/models/core/config'; import { DataStoreService } from 'app/core/core-services/data-store.service'; import { ConstantsService } from 'app/core/ui-services/constants.service'; import { HttpService } from 'app/core/core-services/http.service'; import { Identifiable } from 'app/shared/models/base/identifiable'; import { CollectionStringMapperService } from 'app/core/core-services/collectionStringMapper.service'; +import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; +import { ViewConfig } from 'app/site/config/models/view-config'; /** * Holds a single config item. @@ -95,10 +97,11 @@ export class ConfigRepositoryService extends BaseRepository public constructor( DS: DataStoreService, mapperService: CollectionStringMapperService, + viewModelStoreService: ViewModelStoreService, private constantsService: ConstantsService, private http: HttpService ) { - super(DS, mapperService, Config); + super(DS, mapperService, viewModelStoreService, Config); this.constantsService.get('OpenSlidesConfigVariables').subscribe(constant => { this.createConfigStructure(constant); @@ -111,7 +114,7 @@ export class ConfigRepositoryService extends BaseRepository * Overwritten setup. Does only care about the custom list observable and inserts changed configs into the * config group structure. */ - protected setup(): void { + public onAfterAppsLoaded(): void { if (!this.configListSubject) { this.configListSubject = new BehaviorSubject(null); } diff --git a/client/src/app/core/repositories/history/history-repository.service.ts b/client/src/app/core/repositories/history/history-repository.service.ts index ba35245f2..b3c0f2aa9 100644 --- a/client/src/app/core/repositories/history/history-repository.service.ts +++ b/client/src/app/core/repositories/history/history-repository.service.ts @@ -9,7 +9,8 @@ import { Identifiable } from 'app/shared/models/base/identifiable'; import { HttpService } from 'app/core/core-services/http.service'; import { ViewHistory } from 'app/site/history/models/view-history'; import { TimeTravelService } from 'app/core/core-services/time-travel.service'; -import { BaseModel } from 'app/shared/models/base/base-model'; +import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; +import { ViewUser } from 'app/site/users/models/view-user'; /** * Repository for the history. @@ -31,10 +32,11 @@ export class HistoryRepositoryService extends BaseRepository { +export class ChangeRecommendationRepositoryService extends BaseRepository< + ViewMotionChangeRecommendation, + MotionChangeRecommendation +> { /** * Creates a MotionRepository * @@ -41,18 +45,19 @@ export class ChangeRecommendationRepositoryService extends BaseRepository { + public async create(changeReco: MotionChangeRecommendation): Promise { return await this.dataSend.createModel(changeReco); } @@ -61,17 +66,17 @@ export class ChangeRecommendationRepositoryService extends BaseRepository { + public async createByViewModel(view: ViewMotionChangeRecommendation): Promise { return await this.dataSend.createModel(view.changeRecommendation); } /** * Creates this view wrapper based on an actual Change Recommendation model * - * @param {MotionChangeReco} model + * @param {MotionChangeRecommendation} model */ - protected createViewModel(model: MotionChangeReco): ViewChangeReco { - return new ViewChangeReco(model); + protected createViewModel(model: MotionChangeRecommendation): ViewMotionChangeRecommendation { + return new ViewMotionChangeRecommendation(model); } /** @@ -79,9 +84,9 @@ export class ChangeRecommendationRepositoryService extends BaseRepository { + public async delete(viewModel: ViewMotionChangeRecommendation): Promise { await this.dataSend.deleteModel(viewModel.changeRecommendation); } @@ -91,10 +96,13 @@ export class ChangeRecommendationRepositoryService extends BaseRepository} update the form data containing the update values - * @param {ViewChangeReco} viewModel The View Change Recommendation. If not present, a new motion will be created + * @param {Partial} update the form data containing the update values + * @param {ViewMotionChangeRecommendation} viewModel The View Change Recommendation. If not present, a new motion will be created */ - public async update(update: Partial, viewModel: ViewChangeReco): Promise { + public async update( + update: Partial, + viewModel: ViewMotionChangeRecommendation + ): Promise { const changeReco = viewModel.changeRecommendation; changeReco.patchValues(update); await this.dataSend.partialUpdateModel(changeReco); @@ -103,9 +111,9 @@ export class ChangeRecommendationRepositoryService extends BaseRepository { + public getChangeRecosOfMotionObservable(motion_id: number): Observable { return this.viewModelListSubject.asObservable().pipe( - map((recos: ViewChangeReco[]) => { + map((recos: ViewMotionChangeRecommendation[]) => { return recos.filter(reco => reco.motion_id === motion_id); }) ); @@ -117,16 +125,16 @@ export class ChangeRecommendationRepositoryService extends BaseRepository reco.motion_id === motion_id); } /** * Sets a change recommendation to accepted. * - * @param {ViewChangeReco} change + * @param {ViewMotionChangeRecommendation} change */ - public async setAccepted(change: ViewChangeReco): Promise { + public async setAccepted(change: ViewMotionChangeRecommendation): Promise { const changeReco = change.changeRecommendation; changeReco.patchValues({ rejected: false @@ -137,9 +145,9 @@ export class ChangeRecommendationRepositoryService extends BaseRepository { + public async setRejected(change: ViewMotionChangeRecommendation): Promise { const changeReco = change.changeRecommendation; changeReco.patchValues({ rejected: true @@ -150,10 +158,10 @@ export class ChangeRecommendationRepositoryService extends BaseRepository { + public async setInternal(change: ViewMotionChangeRecommendation, internal: boolean): Promise { const changeReco = change.changeRecommendation; changeReco.patchValues({ internal: internal diff --git a/client/src/app/core/repositories/motions/motion-block-repository.service.ts b/client/src/app/core/repositories/motions/motion-block-repository.service.ts index 35f4ec405..598922b44 100644 --- a/client/src/app/core/repositories/motions/motion-block-repository.service.ts +++ b/client/src/app/core/repositories/motions/motion-block-repository.service.ts @@ -14,6 +14,9 @@ import { MotionBlock } from 'app/shared/models/motions/motion-block'; import { MotionRepositoryService } from './motion-repository.service'; import { ViewMotion } from 'app/site/motions/models/view-motion'; import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block'; +import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; +import { Item } from 'app/shared/models/agenda/item'; +import { ViewItem } from 'app/site/agenda/models/view-item'; /** * Repository service for motion blocks @@ -34,11 +37,12 @@ export class MotionBlockRepositoryService extends BaseRepository public constructor( DS: DataStoreService, mapperService: CollectionStringMapperService, + viewModelStoreService: ViewModelStoreService, private dataSend: DataSendService, private httpService: HttpService, private readonly lineNumbering: LinenumberingService, @@ -72,7 +81,15 @@ export class MotionRepositoryService extends BaseRepository private personalNoteService: PersonalNoteService, private translate: TranslateService ) { - super(DS, mapperService, Motion, [Category, User, Workflow, Item, MotionBlock, Mediafile, Tag]); + super(DS, mapperService, viewModelStoreService, Motion, [ + Category, + User, + Workflow, + Item, + MotionBlock, + Mediafile, + Tag + ]); } /** @@ -84,15 +101,15 @@ export class MotionRepositoryService extends BaseRepository * @param motion blank motion domain object */ protected createViewModel(motion: Motion): ViewMotion { - const category = this.DS.get(Category, motion.category_id); - const submitters = this.DS.getMany(User, motion.submitterIds); - const supporters = this.DS.getMany(User, motion.supporters_id); - const workflow = this.DS.get(Workflow, motion.workflow_id); - const item = this.DS.get(Item, motion.agenda_item_id); - const block = this.DS.get(MotionBlock, motion.motion_block_id); - const attachments = this.DS.getMany(Mediafile, motion.attachments_id); - const tags = this.DS.getMany(Tag, motion.tags_id); - const parent = this.DS.get(Motion, motion.parent_id); + const category = this.viewModelStoreService.get(ViewCategory, motion.category_id); + const submitters = this.viewModelStoreService.getMany(ViewUser, motion.submitterIds); + const supporters = this.viewModelStoreService.getMany(ViewUser, motion.supporters_id); + const workflow = this.viewModelStoreService.get(ViewWorkflow, motion.workflow_id); + const item = this.viewModelStoreService.get(ViewItem, motion.agenda_item_id); + const block = this.viewModelStoreService.get(ViewMotionBlock, motion.motion_block_id); + const attachments = this.viewModelStoreService.getMany(ViewMediafile, motion.attachments_id); + const tags = this.viewModelStoreService.getMany(ViewTag, motion.tags_id); + const parent = this.viewModelStoreService.get(ViewMotion, motion.parent_id); let state: WorkflowState = null; if (workflow) { state = workflow.getStateById(motion.state_id); @@ -260,7 +277,7 @@ export class MotionRepositoryService extends BaseRepository * @param viewMotion The motion to change the submitters from * @param submitters The submitters to set */ - public async setSubmitters(viewMotion: ViewMotion, submitters: User[]): Promise { + public async setSubmitters(viewMotion: ViewMotion, submitters: ViewUser[]): Promise { const requestData = { motions: [ { @@ -525,8 +542,8 @@ export class MotionRepositoryService extends BaseRepository motionId: number, lineRange: LineRange, lineLength: number - ): ViewChangeReco { - const changeReco = new MotionChangeReco(); + ): ViewMotionChangeRecommendation { + const changeReco = new MotionChangeRecommendation(); changeReco.line_from = lineRange.from; changeReco.line_to = lineRange.to; changeReco.type = ModificationType.TYPE_REPLACEMENT; @@ -534,7 +551,7 @@ export class MotionRepositoryService extends BaseRepository changeReco.rejected = false; changeReco.motion_id = motionId; - return new ViewChangeReco(changeReco); + return new ViewMotionChangeRecommendation(changeReco); } /** diff --git a/client/src/app/core/repositories/motions/statute-paragraph-repository.service.ts b/client/src/app/core/repositories/motions/statute-paragraph-repository.service.ts index e8a6ebe2f..3fa433ed4 100644 --- a/client/src/app/core/repositories/motions/statute-paragraph-repository.service.ts +++ b/client/src/app/core/repositories/motions/statute-paragraph-repository.service.ts @@ -7,6 +7,7 @@ import { ViewStatuteParagraph } from 'app/site/motions/models/view-statute-parag import { StatuteParagraph } from 'app/shared/models/motions/statute-paragraph'; import { Identifiable } from 'app/shared/models/base/identifiable'; import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service'; +import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; /** * Repository Services for statute paragraphs @@ -31,9 +32,10 @@ export class StatuteParagraphRepositoryService extends BaseRepository { - public constructor(DS: DataStoreService, mapperService: CollectionStringMapperService) { - super(DS, mapperService, ProjectorMessage); + public constructor( + DS: DataStoreService, + mapperService: CollectionStringMapperService, + viewModelStoreService: ViewModelStoreService + ) { + super(DS, mapperService, viewModelStoreService, ProjectorMessage); } protected createViewModel(message: ProjectorMessage): ViewProjectorMessage { diff --git a/client/src/app/core/repositories/tags/tag-repository.service.ts b/client/src/app/core/repositories/tags/tag-repository.service.ts index ea81504db..1d2380d62 100644 --- a/client/src/app/core/repositories/tags/tag-repository.service.ts +++ b/client/src/app/core/repositories/tags/tag-repository.service.ts @@ -7,6 +7,7 @@ import { DataStoreService } from '../../core-services/data-store.service'; import { BaseRepository } from '../base-repository'; import { Identifiable } from 'app/shared/models/base/identifiable'; import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service'; +import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; /** * Repository Services for Tags @@ -34,9 +35,10 @@ export class TagRepositoryService extends BaseRepository { public constructor( protected DS: DataStoreService, mapperService: CollectionStringMapperService, + viewModelStoreService: ViewModelStoreService, private dataSend: DataSendService ) { - super(DS, mapperService, Tag); + super(DS, mapperService, viewModelStoreService, Tag); } protected createViewModel(tag: Tag): ViewTag { diff --git a/client/src/app/core/repositories/users/group-repository.service.ts b/client/src/app/core/repositories/users/group-repository.service.ts index 33349aaec..542787a1f 100644 --- a/client/src/app/core/repositories/users/group-repository.service.ts +++ b/client/src/app/core/repositories/users/group-repository.service.ts @@ -8,6 +8,7 @@ import { DataStoreService } from '../../core-services/data-store.service'; import { Group } from 'app/shared/models/users/group'; import { Identifiable } from 'app/shared/models/base/identifiable'; import { ViewGroup } from 'app/site/users/models/view-group'; +import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; /** * Shape of a permission @@ -49,10 +50,11 @@ export class GroupRepositoryService extends BaseRepository { public constructor( DS: DataStoreService, mapperService: CollectionStringMapperService, + viewModelStoreService: ViewModelStoreService, private dataSend: DataSendService, private constants: ConstantsService ) { - super(DS, mapperService, Group); + super(DS, mapperService, viewModelStoreService, Group); this.sortPermsPerApp(); } diff --git a/client/src/app/core/repositories/users/personal-note-repository.service.ts b/client/src/app/core/repositories/users/personal-note-repository.service.ts index e2aa6797e..15726c1d8 100644 --- a/client/src/app/core/repositories/users/personal-note-repository.service.ts +++ b/client/src/app/core/repositories/users/personal-note-repository.service.ts @@ -5,34 +5,40 @@ import { BaseRepository } from '../base-repository'; import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service'; import { PersonalNote } from 'app/shared/models/users/personal-note'; import { Identifiable } from 'app/shared/models/base/identifiable'; +import { ViewPersonalNote } from 'app/site/users/models/view-personal-note'; +import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; /** */ @Injectable({ providedIn: 'root' }) -export class PersonalNoteRepositoryService extends BaseRepository { +export class PersonalNoteRepositoryService extends BaseRepository { /** * @param DS The DataStore * @param mapperService Maps collection strings to classes */ - public constructor(protected DS: DataStoreService, mapperService: CollectionStringMapperService) { - super(DS, mapperService, PersonalNote); + public constructor( + DS: DataStoreService, + mapperService: CollectionStringMapperService, + viewModelStoreService: ViewModelStoreService + ) { + super(DS, mapperService, viewModelStoreService, PersonalNote); } - protected createViewModel(personalNote: PersonalNote): any { - return {}; + protected createViewModel(personalNote: PersonalNote): ViewPersonalNote { + return new ViewPersonalNote(); } public async create(personalNote: PersonalNote): Promise { throw new Error('TODO'); } - public async update(personalNote: Partial, viewPersonalNote: any): Promise { + public async update(personalNote: Partial, viewPersonalNote: ViewPersonalNote): Promise { throw new Error('TODO'); } - public async delete(viewPersonalNote: any): Promise { + public async delete(viewPersonalNote: ViewPersonalNote): Promise { throw new Error('TODO'); } } diff --git a/client/src/app/core/repositories/users/user-repository.service.ts b/client/src/app/core/repositories/users/user-repository.service.ts index 404c88b37..f061848d7 100644 --- a/client/src/app/core/repositories/users/user-repository.service.ts +++ b/client/src/app/core/repositories/users/user-repository.service.ts @@ -12,6 +12,8 @@ import { ConfigService } from 'app/core/ui-services/config.service'; import { HttpService } from 'app/core/core-services/http.service'; import { TranslateService } from '@ngx-translate/core'; import { environment } from '../../../../environments/environment'; +import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; +import { ViewGroup } from 'app/site/users/models/view-group'; /** * type for determining the user name from a string during import. @@ -38,12 +40,13 @@ export class UserRepositoryService extends BaseRepository { public constructor( DS: DataStoreService, mapperService: CollectionStringMapperService, + viewModelStoreService: ViewModelStoreService, private dataSend: DataSendService, private translate: TranslateService, private httpService: HttpService, private configService: ConfigService ) { - super(DS, mapperService, User, [Group]); + super(DS, mapperService, viewModelStoreService, User, [Group]); } /** @@ -104,7 +107,7 @@ export class UserRepositoryService extends BaseRepository { } public createViewModel(user: User): ViewUser { - const groups = this.DS.getMany(Group, user.groups_id); + const groups = this.viewModelStoreService.getMany(ViewGroup, user.groups_id); return new ViewUser(user, groups); } @@ -218,20 +221,9 @@ export class UserRepositoryService extends BaseRepository { * @returns all users matching that name */ public getUsersByName(name: string): ViewUser[] { - const results: ViewUser[] = []; - const users = this.DS.getAll(User).filter(user => { - if (user.full_name === name || user.short_name === name) { - return true; - } - if (user.number === name) { - return true; - } - return false; + return this.getViewModelList().filter(user => { + return user.full_name === name || user.short_name === name || user.number === name; }); - users.forEach(user => { - results.push(this.createViewModel(user)); - }); - return results; } /** @@ -241,7 +233,7 @@ export class UserRepositoryService extends BaseRepository { * @returns all users matching that number */ public getUsersByNumber(number: string): ViewUser[] { - return this.getViewModelList().filter(user => user.participant_number === number); + return this.getViewModelList().filter(user => user.number === number); } /** diff --git a/client/src/app/core/ui-services/count-users.service.ts b/client/src/app/core/ui-services/count-users.service.ts index 2c4a54ce2..5fcab88a5 100644 --- a/client/src/app/core/ui-services/count-users.service.ts +++ b/client/src/app/core/ui-services/count-users.service.ts @@ -61,7 +61,7 @@ export class CountUsersService extends OpenSlidesComponent { }); // Look for the current user. - operator.getObservable().subscribe(user => (this.currentUserId = user ? user.id : null)); + operator.getUserObservable().subscribe(user => (this.currentUserId = user ? user.id : null)); } /** diff --git a/client/src/app/core/ui-services/personal-note.service.ts b/client/src/app/core/ui-services/personal-note.service.ts index f6453b9a4..7f7943d3c 100644 --- a/client/src/app/core/ui-services/personal-note.service.ts +++ b/client/src/app/core/ui-services/personal-note.service.ts @@ -41,7 +41,7 @@ export class PersonalNoteService { * Watches for changes in the personal note model. */ public constructor(private operator: OperatorService, private DS: DataStoreService, private http: HttpService) { - operator.getObservable().subscribe(() => this.updatePersonalNoteObject()); + operator.getUserObservable().subscribe(() => this.updatePersonalNoteObject()); this.DS.changeObservable.subscribe(model => { if (model instanceof PersonalNote) { this.updatePersonalNoteObject(); diff --git a/client/src/app/core/ui-services/search.service.ts b/client/src/app/core/ui-services/search.service.ts index 201e7abff..63b7e6cb8 100644 --- a/client/src/app/core/ui-services/search.service.ts +++ b/client/src/app/core/ui-services/search.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; -import { BaseModel } from '../../shared/models/base/base-model'; -import { DataStoreService } from '../core-services/data-store.service'; -import { Searchable } from '../../shared/models/base/searchable'; +import { Searchable } from '../../site/base/searchable'; +import { BaseViewModel } from 'app/site/base/base-view-model'; /** * The representation every searchable model should use to represent their data. @@ -46,7 +45,7 @@ export interface SearchResult { /** * All matched models. */ - models: (BaseModel & Searchable)[]; + models: (BaseViewModel & Searchable)[]; } /** @@ -61,7 +60,6 @@ export class SearchService { */ private searchModels: { collectionString: string; - ctor: new (...args: any[]) => Searchable & BaseModel; verboseNameSingular: string; verboseNamePlural: string; displayOrder: number; @@ -70,7 +68,7 @@ export class SearchService { /** * @param DS The DataStore to search in. */ - public constructor(private DS: DataStoreService) {} + public constructor() {} /** * Registers a model by the given attributes. @@ -81,13 +79,12 @@ export class SearchService { */ public registerModel( collectionString: string, - ctor: new (...args: any[]) => Searchable & BaseModel, + ctor: new (...args: any[]) => Searchable & BaseViewModel, displayOrder: number ): void { const instance = new ctor(); this.searchModels.push({ collectionString: collectionString, - ctor: ctor, verboseNameSingular: instance.getVerboseName(), verboseNamePlural: instance.getVerboseName(true), displayOrder: displayOrder @@ -115,10 +112,10 @@ export class SearchService { */ public search(query: string, inCollectionStrings: string[]): SearchResult[] { query = query.toLowerCase(); - return this.searchModels + /*return this.searchModels .filter(s => inCollectionStrings.includes(s.collectionString)) .map(searchModel => { - const results = this.DS.filter(searchModel.ctor, model => + const results = this.viewModelStore.filter(searchModel.collectionString, model => model.formatForSearch().some(text => text.toLowerCase().includes(query)) ); return { @@ -126,6 +123,8 @@ export class SearchService { verboseName: results.length === 1 ? searchModel.verboseNameSingular : searchModel.verboseNamePlural, models: results }; - }); + });*/ + throw new Error('Todo'); + return []; } } diff --git a/client/src/app/core/ui-services/tree.service.ts b/client/src/app/core/ui-services/tree.service.ts index ace025b55..c02fc40d8 100644 --- a/client/src/app/core/ui-services/tree.service.ts +++ b/client/src/app/core/ui-services/tree.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { OpenSlidesComponent } from 'app/openslides.component'; -import { Displayable } from 'app/shared/models/base/displayable'; +import { Displayable } from 'app/site/base/displayable'; import { Identifiable } from 'app/shared/models/base/identifiable'; /** diff --git a/client/src/app/fullscreen-projector/fullscreen-projector/fullscreen-projector.component.ts b/client/src/app/fullscreen-projector/fullscreen-projector/fullscreen-projector.component.ts index f186080c0..a646382a2 100644 --- a/client/src/app/fullscreen-projector/fullscreen-projector/fullscreen-projector.component.ts +++ b/client/src/app/fullscreen-projector/fullscreen-projector/fullscreen-projector.component.ts @@ -88,7 +88,7 @@ export class FullscreenProjectorComponent implements OnInit { this.isLoading = false; }); - this.operator.getObservable().subscribe(() => { + this.operator.getUserObservable().subscribe(() => { this.canSeeProjector = this.operator.hasPerms('projector.can_see'); }); } diff --git a/client/src/app/shared/components/choice-dialog/choice-dialog.component.ts b/client/src/app/shared/components/choice-dialog/choice-dialog.component.ts index 9ee58b9ee..54b37461c 100644 --- a/client/src/app/shared/components/choice-dialog/choice-dialog.component.ts +++ b/client/src/app/shared/components/choice-dialog/choice-dialog.component.ts @@ -1,7 +1,7 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; -import { Displayable } from 'app/shared/models/base/displayable'; +import { Displayable } from 'app/site/base/displayable'; import { Identifiable } from 'app/shared/models/base/identifiable'; /** diff --git a/client/src/app/shared/components/copyright-sign/copyright-sign.component.ts b/client/src/app/shared/components/copyright-sign/copyright-sign.component.ts index 5b3a695d2..cbe7b3b26 100644 --- a/client/src/app/shared/components/copyright-sign/copyright-sign.component.ts +++ b/client/src/app/shared/components/copyright-sign/copyright-sign.component.ts @@ -373,7 +373,7 @@ export class C4DialogComponent implements OnInit, OnDestroy { * Returns the operators name. */ public getPlayerName(): string { - return this.op.user.short_name; + return this.op.viewUser.short_name; } /** diff --git a/client/src/app/shared/components/projector/projector.component.ts b/client/src/app/shared/components/projector/projector.component.ts index 5b8c06206..e030a079b 100644 --- a/client/src/app/shared/components/projector/projector.component.ts +++ b/client/src/app/shared/components/projector/projector.component.ts @@ -221,9 +221,9 @@ export class ProjectorComponent extends BaseComponent implements OnDestroy { this.projectorDataService.projectorClosed(from); } - this.dataSubscription = this.projectorDataService - .getProjectorObservable(to) - .subscribe(data => (this.slides = data || [])); + this.dataSubscription = this.projectorDataService.getProjectorObservable(to).subscribe(data => { + this.slides = data || []; + }); this.projectorSubscription = this.projectorRepository.getViewModelObservable(to).subscribe(projector => { if (projector) { this.scroll = projector.scroll || 0; diff --git a/client/src/app/shared/components/selectable.ts b/client/src/app/shared/components/selectable.ts index fafb52f1a..ad5bafb19 100644 --- a/client/src/app/shared/components/selectable.ts +++ b/client/src/app/shared/components/selectable.ts @@ -1,4 +1,4 @@ -import { Displayable } from '../models/base/displayable'; +import { Displayable } from '../../site/base/displayable'; import { Identifiable } from '../models/base/identifiable'; /** diff --git a/client/src/app/shared/components/sorting-tree/sorting-tree.component.spec.ts b/client/src/app/shared/components/sorting-tree/sorting-tree.component.spec.ts index 53edf85e9..3a2b78135 100644 --- a/client/src/app/shared/components/sorting-tree/sorting-tree.component.spec.ts +++ b/client/src/app/shared/components/sorting-tree/sorting-tree.component.spec.ts @@ -2,7 +2,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { E2EImportsModule } from '../../../../e2e-imports.module'; import { SortingTreeComponent } from './sorting-tree.component'; import { Component, ViewChild } from '@angular/core'; -import { Displayable } from 'app/shared/models/base/displayable'; +import { Displayable } from 'app/site/base/displayable'; import { Identifiable } from 'app/shared/models/base/identifiable'; import { BehaviorSubject } from 'rxjs'; diff --git a/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts b/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts index 65821fccd..24814f657 100644 --- a/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts +++ b/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts @@ -6,7 +6,7 @@ import { auditTime } from 'rxjs/operators'; import { Subscription, Observable } from 'rxjs'; import { Identifiable } from 'app/shared/models/base/identifiable'; -import { Displayable } from 'app/shared/models/base/displayable'; +import { Displayable } from 'app/site/base/displayable'; import { OSTreeNode, TreeService } from 'app/core/ui-services/tree.service'; /** diff --git a/client/src/app/shared/directives/perms.directive.ts b/client/src/app/shared/directives/perms.directive.ts index e9623e870..d5f9f2863 100644 --- a/client/src/app/shared/directives/perms.directive.ts +++ b/client/src/app/shared/directives/perms.directive.ts @@ -68,7 +68,7 @@ export class PermsDirective extends OpenSlidesComponent implements OnInit, OnDes public ngOnInit(): void { // observe groups of operator, so the directive can actively react to changes - this.operatorSubscription = this.operator.getObservable().subscribe(() => { + this.operatorSubscription = this.operator.getUserObservable().subscribe(() => { this.updateView(); }); } diff --git a/client/src/app/shared/models/agenda/item.ts b/client/src/app/shared/models/agenda/item.ts index fe918a589..ce9d18179 100644 --- a/client/src/app/shared/models/agenda/item.ts +++ b/client/src/app/shared/models/agenda/item.ts @@ -41,7 +41,7 @@ export class Item extends BaseModel { public parent_id: number; public constructor(input?: any) { - super('agenda/item', 'Item', input); + super('agenda/item', input); } public deserialize(input: any): void { @@ -84,16 +84,4 @@ export class Item extends BaseModel { const type = itemVisibilityChoices.find(choice => choice.key === this.type); return type ? type.csvName : ''; } - - public getTitle(): string { - return this.title; - } - - public getListTitle(): string { - return this.title_with_type; - } - - public getProjectorTitle(): string { - return this.getListTitle(); - } } diff --git a/client/src/app/shared/models/agenda/speaker.ts b/client/src/app/shared/models/agenda/speaker.ts index a8751628f..2480eb1eb 100644 --- a/client/src/app/shared/models/agenda/speaker.ts +++ b/client/src/app/shared/models/agenda/speaker.ts @@ -59,12 +59,4 @@ export class Speaker extends Deserializer { return SpeakerState.FINISHED; } } - - /** - * Getting the title of a speaker does not make much sense. - * Usually it would refer to the title of a user. - */ - public getTitle(): string { - return ''; - } } diff --git a/client/src/app/shared/models/assignments/assignment.ts b/client/src/app/shared/models/assignments/assignment.ts index d013044c3..f0702a826 100644 --- a/client/src/app/shared/models/assignments/assignment.ts +++ b/client/src/app/shared/models/assignments/assignment.ts @@ -1,7 +1,6 @@ import { AssignmentUser } from './assignment-user'; import { Poll } from './poll'; -import { AgendaBaseModel } from '../base/agenda-base-model'; -import { SearchRepresentation } from 'app/core/ui-services/search.service'; +import { BaseModel } from '../base/base-model'; export const assignmentPhase = [ { key: 0, name: 'Searching for candidates' }, @@ -13,7 +12,7 @@ export const assignmentPhase = [ * Representation of an assignment. * @ignore */ -export class Assignment extends AgendaBaseModel { +export class Assignment extends BaseModel { public id: number; public title: string; public description: string; @@ -26,7 +25,7 @@ export class Assignment extends AgendaBaseModel { public tags_id: number[]; public constructor(input?: any) { - super('assignments/assignment', 'Election', input); + super('assignments/assignment', input); } public get candidateIds(): number[] { @@ -54,16 +53,4 @@ export class Assignment extends AgendaBaseModel { }); } } - - public getTitle(): string { - return this.title; - } - - public formatForSearch(): SearchRepresentation { - return [this.title, this.description]; - } - - public getDetailStateURL(): string { - return 'TODO'; - } } diff --git a/client/src/app/shared/models/base/base-model.ts b/client/src/app/shared/models/base/base-model.ts index b6f052e5a..06e1fac43 100644 --- a/client/src/app/shared/models/base/base-model.ts +++ b/client/src/app/shared/models/base/base-model.ts @@ -1,7 +1,7 @@ import { OpenSlidesComponent } from 'app/openslides.component'; import { Deserializable } from './deserializable'; -import { Displayable } from './displayable'; import { Identifiable } from './identifiable'; +import { Collection } from './collection'; export type ModelConstructor> = new (...args: any[]) => T; @@ -10,7 +10,7 @@ export type ModelConstructor> = new (...args: any[]) => T * When inherit from this class, give the subclass as the type. E.g. `class Motion extends BaseModel` */ export abstract class BaseModel extends OpenSlidesComponent - implements Deserializable, Displayable, Identifiable { + implements Deserializable, Identifiable, Collection { /** * force children of BaseModel to have a collectionString. * @@ -27,11 +27,6 @@ export abstract class BaseModel extends OpenSlidesComponent return this._collectionString; } - /** - * Children should also have a verbose name for generic display purposes - */ - protected _verboseName: string; - /** * force children of BaseModel to have an id */ @@ -44,10 +39,9 @@ export abstract class BaseModel extends OpenSlidesComponent * @param verboseName * @param input */ - protected constructor(collectionString: string, verboseName: string, input?: any) { + protected constructor(collectionString: string, input?: any) { super(); this._collectionString = collectionString; - this._verboseName = verboseName; if (input) { this.changeNullValuesToUndef(input); @@ -74,32 +68,6 @@ export abstract class BaseModel extends OpenSlidesComponent Object.assign(this, update); } - public abstract getTitle(): string; - - public getListTitle(): string { - return this.getTitle(); - } - - public toString(): string { - return this.getTitle(); - } - - /** - * Returns the verbose name. Makes it plural by adding a 's'. - * - * @param plural If the name should be plural - * @returns the verbose name of the model - */ - public getVerboseName(plural: boolean = false): string { - if (plural) { - return this._verboseName + 's'; // I love english. This works for all our models (participantS, electionS, - // topicS, motionS, (media)fileS, motion blockS, commentS, personal noteS, projectorS, messageS, countdownS, ...) - // Just categorIES need to overwrite this... - } else { - return this._verboseName; - } - } - /** * Most simple and most commonly used deserialize function. * Inherited to children, can be overwritten for special use cases diff --git a/client/src/app/shared/models/base/collection.ts b/client/src/app/shared/models/base/collection.ts new file mode 100644 index 000000000..71cf1a453 --- /dev/null +++ b/client/src/app/shared/models/base/collection.ts @@ -0,0 +1,6 @@ +/** + * Every implementing object should have a collection string. + */ +export interface Collection { + readonly collectionString: string; +} diff --git a/client/src/app/shared/models/core/chat-message.ts b/client/src/app/shared/models/core/chat-message.ts index f13226cfb..ebe877c84 100644 --- a/client/src/app/shared/models/core/chat-message.ts +++ b/client/src/app/shared/models/core/chat-message.ts @@ -5,16 +5,13 @@ import { BaseModel } from '../base/base-model'; * @ignore */ export class ChatMessage extends BaseModel { + public static COLLECTIONSTRING = 'core/chat-message'; public id: number; public message: string; public timestamp: string; // TODO: Type for timestamp public user_id: number; public constructor(input?: any) { - super('core/chat-message', 'Chatmessage', input); - } - - public getTitle(): string { - return 'Chatmessage'; + super(ChatMessage.COLLECTIONSTRING, input); } } diff --git a/client/src/app/shared/models/core/config.ts b/client/src/app/shared/models/core/config.ts index 9f7d5ee74..080202170 100644 --- a/client/src/app/shared/models/core/config.ts +++ b/client/src/app/shared/models/core/config.ts @@ -5,15 +5,12 @@ import { BaseModel } from '../base/base-model'; * @ignore */ export class Config extends BaseModel { + public static COLLECTIONSTRING = 'core/config'; public id: number; public key: string; public value: Object; public constructor(input?: any) { - super('core/config', 'Config', input); - } - - public getTitle(): string { - return this.key; + super(Config.COLLECTIONSTRING, input); } } diff --git a/client/src/app/shared/models/core/countdown.ts b/client/src/app/shared/models/core/countdown.ts index ea6d31ae1..f68d95da7 100644 --- a/client/src/app/shared/models/core/countdown.ts +++ b/client/src/app/shared/models/core/countdown.ts @@ -14,10 +14,6 @@ export class Countdown extends BaseModel { public running: boolean; public constructor(input?: any) { - super(Countdown.COLLECTIONSTRING, 'Countdown', input); - } - - public getTitle(): string { - return this.description; + super(Countdown.COLLECTIONSTRING, input); } } diff --git a/client/src/app/shared/models/core/history.ts b/client/src/app/shared/models/core/history.ts index 70b41b36f..b894a3fa4 100644 --- a/client/src/app/shared/models/core/history.ts +++ b/client/src/app/shared/models/core/history.ts @@ -6,6 +6,7 @@ import { BaseModel } from '../base/base-model'; * @ignore */ export class History extends BaseModel { + public static COLLECTIONSTRING = 'core/history'; public id: number; public element_id: string; public now: string; @@ -29,10 +30,6 @@ export class History extends BaseModel { } public constructor(input?: any) { - super('core/history', 'History', input); - } - - public getTitle(): string { - return this.element_id; + super(History.COLLECTIONSTRING, input); } } diff --git a/client/src/app/shared/models/core/projector-message.ts b/client/src/app/shared/models/core/projector-message.ts index 654d03408..34cd375d9 100644 --- a/client/src/app/shared/models/core/projector-message.ts +++ b/client/src/app/shared/models/core/projector-message.ts @@ -11,10 +11,6 @@ export class ProjectorMessage extends BaseModel { public message: string; public constructor(input?: any) { - super(ProjectorMessage.COLLECTIONSTRING, 'Message', input); - } - - public getTitle(): string { - return 'Projectormessage'; + super(ProjectorMessage.COLLECTIONSTRING, input); } } diff --git a/client/src/app/shared/models/core/projector.ts b/client/src/app/shared/models/core/projector.ts index 0da905990..c8ae50056 100644 --- a/client/src/app/shared/models/core/projector.ts +++ b/client/src/app/shared/models/core/projector.ts @@ -44,9 +44,14 @@ export interface ProjectionDefault { /** * Representation of a projector. Has the nested property "projectiondefaults" + * + * TODO: Move all function to the viewprojector. + * * @ignore */ export class Projector extends BaseModel { + public static COLLECTIONSTRING = 'core/projector'; + public id: number; public elements: ProjectorElements; public elements_preview: ProjectorElements; @@ -60,7 +65,7 @@ export class Projector extends BaseModel { public projectiondefaults: ProjectionDefault[]; public constructor(input?: any) { - super('core/projector', 'Projector', input); + super(Projector.COLLECTIONSTRING, input); } /** @@ -139,8 +144,4 @@ export class Projector extends BaseModel { [[], []] as [T[], T[]] ); } - - public getTitle(): string { - return this.name; - } } diff --git a/client/src/app/shared/models/core/tag.ts b/client/src/app/shared/models/core/tag.ts index 149e7cbd8..b349b8788 100644 --- a/client/src/app/shared/models/core/tag.ts +++ b/client/src/app/shared/models/core/tag.ts @@ -1,27 +1,16 @@ import { BaseModel } from '../base/base-model'; -import { Searchable } from '../base/searchable'; /** * Representation of a tag. * @ignore */ -export class Tag extends BaseModel implements Searchable { +export class Tag extends BaseModel { + public static COLLECTIONSTRING = 'core/tag'; + public id: number; public name: string; public constructor(input?: any) { - super('core/tag', 'Tag', input); - } - - public getTitle(): string { - return this.name; - } - - public formatForSearch(): string[] { - return [this.name]; - } - - public getDetailStateURL(): string { - return '/tags'; + super(Tag.COLLECTIONSTRING, input); } } diff --git a/client/src/app/shared/models/mediafiles/mediafile.ts b/client/src/app/shared/models/mediafiles/mediafile.ts index f26684009..58570b746 100644 --- a/client/src/app/shared/models/mediafiles/mediafile.ts +++ b/client/src/app/shared/models/mediafiles/mediafile.ts @@ -1,12 +1,11 @@ import { File } from './file'; -import { Searchable } from '../base/searchable'; import { BaseModel } from '../base/base-model'; /** * Representation of MediaFile. Has the nested property "File" * @ignore */ -export class Mediafile extends BaseModel implements Searchable { +export class Mediafile extends BaseModel { public id: number; public title: string; public mediafile: File; @@ -17,7 +16,7 @@ export class Mediafile extends BaseModel implements Searchable { public timestamp: string; public constructor(input?: any) { - super('mediafiles/mediafile', 'Mediafile', input); + super('mediafiles/mediafile', input); } public deserialize(input: any): void { @@ -30,19 +29,7 @@ export class Mediafile extends BaseModel implements Searchable { * * @returns the download URL for the specific file as string */ - public getDownloadUrl(): string { + public get downloadUrl(): string { return `${this.media_url_prefix}${this.mediafile.name}`; } - - public getTitle(): string { - return this.title; - } - - public formatForSearch(): string[] { - return [this.title, this.mediafile.name]; - } - - public getDetailStateURL(): string { - return this.getDownloadUrl(); - } } diff --git a/client/src/app/shared/models/motions/category.ts b/client/src/app/shared/models/motions/category.ts index 761a60307..540473db9 100644 --- a/client/src/app/shared/models/motions/category.ts +++ b/client/src/app/shared/models/motions/category.ts @@ -1,52 +1,17 @@ import { BaseModel } from '../base/base-model'; -import { Searchable } from '../base/searchable'; -import { SearchRepresentation } from 'app/core/ui-services/search.service'; /** * Representation of a motion category. Has the nested property "File" * @ignore */ -export class Category extends BaseModel implements Searchable { +export class Category extends BaseModel { + public static COLLECTIONSTRING = 'motions/category'; + public id: number; public name: string; public prefix: string; public constructor(input?: any) { - super('motions/category', 'Category', input); - } - - public getTitle(): string { - return this.prefix ? this.prefix + ' - ' + this.name : this.name; - } - - /** - * Returns the verbose name of this model. - * - * @override - * @param plural If the name should be plural - * @param The verbose name - */ - public getVerboseName(plural: boolean = false): string { - if (plural) { - return 'Categories'; - } else { - return this._verboseName; - } - } - - /** - * Formats the category for search - * - * @override - */ - public formatForSearch(): SearchRepresentation { - return [this.getTitle()]; - } - - /** - * TODO: add an id as url parameter, so the category auto-opens. - */ - public getDetailStateURL(): string { - return '/motions/category'; + super(Category.COLLECTIONSTRING, input); } } diff --git a/client/src/app/shared/models/motions/motion-block.ts b/client/src/app/shared/models/motions/motion-block.ts index 0dc597b03..c75c990d9 100644 --- a/client/src/app/shared/models/motions/motion-block.ts +++ b/client/src/app/shared/models/motions/motion-block.ts @@ -1,38 +1,17 @@ -import { AgendaBaseModel } from '../base/agenda-base-model'; -import { SearchRepresentation } from 'app/core/ui-services/search.service'; +import { BaseModel } from '../base/base-model'; /** * Representation of a motion block. * @ignore */ -export class MotionBlock extends AgendaBaseModel { +export class MotionBlock extends BaseModel { + public static COLLECTIONSTRING = 'motions/motion-block'; + public id: number; public title: string; public agenda_item_id: number; public constructor(input?: any) { - super('motions/motion-block', 'Motion block', input); - } - - public getTitle(): string { - return this.title; - } - - /** - * Formats the category for search - * - * @override - */ - public formatForSearch(): SearchRepresentation { - return [this.title]; - } - - /** - * Get the URL to the motion block - * - * @returns the URL as string - */ - public getDetailStateURL(): string { - return `/motions/blocks/${this.id}`; + super(MotionBlock.COLLECTIONSTRING, input); } } diff --git a/client/src/app/shared/models/motions/motion-change-reco.ts b/client/src/app/shared/models/motions/motion-change-reco.ts index bc1a4a6c5..083e55e1c 100644 --- a/client/src/app/shared/models/motions/motion-change-reco.ts +++ b/client/src/app/shared/models/motions/motion-change-reco.ts @@ -4,7 +4,9 @@ import { BaseModel } from '../base/base-model'; * Representation of a motion change recommendation. * @ignore */ -export class MotionChangeReco extends BaseModel { +export class MotionChangeRecommendation extends BaseModel { + public static COLLECTIONSTRING = 'motions/motion-change-recommendation'; + public id: number; public motion_id: number; public rejected: boolean; @@ -17,10 +19,6 @@ export class MotionChangeReco extends BaseModel { public creation_time: string; public constructor(input?: any) { - super('motions/motion-change-recommendation', 'Change recommendation', input); - } - - public getTitle(): string { - return 'Changerecommendation'; + super(MotionChangeRecommendation.COLLECTIONSTRING, input); } } diff --git a/client/src/app/shared/models/motions/motion-comment-section.ts b/client/src/app/shared/models/motions/motion-comment-section.ts index aa68240da..0e4a3d304 100644 --- a/client/src/app/shared/models/motions/motion-comment-section.ts +++ b/client/src/app/shared/models/motions/motion-comment-section.ts @@ -1,20 +1,18 @@ import { BaseModel } from '../base/base-model'; /** - * Representation of a motion category. Has the nested property "File" + * Representation of a comment section. * @ignore */ export class MotionCommentSection extends BaseModel { + public static COLLECTIONSTRING = 'motions/motion-comment-section'; + public id: number; public name: string; public read_groups_id: number[]; public write_groups_id: number[]; public constructor(input?: any) { - super('motions/motion-comment-section', 'Comment section', input); - } - - public getTitle(): string { - return this.name; + super(MotionCommentSection.COLLECTIONSTRING, input); } } diff --git a/client/src/app/shared/models/motions/motion.ts b/client/src/app/shared/models/motions/motion.ts index 3f6fa7e9f..feb9803f3 100644 --- a/client/src/app/shared/models/motions/motion.ts +++ b/client/src/app/shared/models/motions/motion.ts @@ -1,8 +1,7 @@ import { MotionSubmitter } from './motion-submitter'; import { MotionComment } from './motion-comment'; -import { AgendaBaseModel } from '../base/agenda-base-model'; -import { SearchRepresentation } from 'app/core/ui-services/search.service'; import { MotionPoll } from './motion-poll'; +import { BaseModel } from '../base/base-model'; /** * Representation of Motion. @@ -11,7 +10,7 @@ import { MotionPoll } from './motion-poll'; * * @ignore */ -export class Motion extends AgendaBaseModel { +export class Motion extends BaseModel { public static COLLECTIONSTRING = 'motions/motion'; public id: number; @@ -45,7 +44,7 @@ export class Motion extends AgendaBaseModel { public last_modified: string; public constructor(input?: any) { - super(Motion.COLLECTIONSTRING, 'Motion', input); + super(Motion.COLLECTIONSTRING, input); } /** @@ -59,49 +58,6 @@ export class Motion extends AgendaBaseModel { .map((submitter: MotionSubmitter) => submitter.user_id); } - public getTitle(): string { - if (this.identifier) { - return this.identifier + ': ' + this.title; - } else { - return this.title; - } - } - - public getAgendaTitle(): string { - // if the identifier is set, the title will be 'Motion '. - if (this.identifier) { - return 'Motion ' + this.identifier; - } else { - return this.getTitle(); - } - } - - public getAgendaTitleWithType(): string { - // Append the verbose name only, if not the special format 'Motion ' is used. - if (this.identifier) { - return 'Motion ' + this.identifier; - } else { - return this.getTitle() + ' (' + this.getVerboseName() + ')'; - } - } - - /** - * Formats the category for search - * - * @override - */ - public formatForSearch(): SearchRepresentation { - let searchValues = [this.title, this.text, this.reason]; - if (this.amendment_paragraphs) { - searchValues = searchValues.concat(this.amendment_paragraphs.filter(x => !!x)); - } - return searchValues; - } - - public getDetailStateURL(): string { - return `/motions/${this.id}`; - } - public deserialize(input: any): void { Object.assign(this, input); diff --git a/client/src/app/shared/models/motions/statute-paragraph.ts b/client/src/app/shared/models/motions/statute-paragraph.ts index 037b132d5..366c7336e 100644 --- a/client/src/app/shared/models/motions/statute-paragraph.ts +++ b/client/src/app/shared/models/motions/statute-paragraph.ts @@ -1,38 +1,18 @@ import { BaseModel } from '../base/base-model'; -import { Searchable } from '../base/searchable'; -import { SearchRepresentation } from 'app/core/ui-services/search.service'; /** * Representation of a statute paragraph. * @ignore */ -export class StatuteParagraph extends BaseModel implements Searchable { +export class StatuteParagraph extends BaseModel { + public static COLLECTIONSTRING = 'motions/statute-paragraph'; + public id: number; public title: string; public text: string; public weight: number; public constructor(input?: any) { - super('motions/statute-paragraph', 'Statute paragraph', input); - } - - public getTitle(): string { - return this.title; - } - - /** - * Formats the category for search - * - * @override - */ - public formatForSearch(): SearchRepresentation { - return [this.title, this.text]; - } - - /** - * TODO: add an id as url parameter, so the statute paragraph auto-opens. - */ - public getDetailStateURL(): string { - return '/motions/statute-paragraphs'; + super(StatuteParagraph.COLLECTIONSTRING, input); } } diff --git a/client/src/app/shared/models/motions/workflow.ts b/client/src/app/shared/models/motions/workflow.ts index 75bf9a278..9b45f8a0b 100644 --- a/client/src/app/shared/models/motions/workflow.ts +++ b/client/src/app/shared/models/motions/workflow.ts @@ -6,17 +6,15 @@ import { WorkflowState } from './workflow-state'; * @ignore */ export class Workflow extends BaseModel { + public static COLLECTIONSTRING = 'motions/workflow'; + public id: number; public name: string; public states: WorkflowState[]; public first_state_id: number; - public get firstState(): WorkflowState { - return this.getStateById(this.first_state_id); - } - public constructor(input?: any) { - super('motions/workflow', 'Workflow', input); + super(Workflow.COLLECTIONSTRING, input); } /** @@ -38,10 +36,6 @@ export class Workflow extends BaseModel { }); } - public getStateById(id: number): WorkflowState { - return this.states.find(state => state.id === id); - } - public deserialize(input: any): void { Object.assign(this, input); if (input.states instanceof Array) { @@ -51,8 +45,4 @@ export class Workflow extends BaseModel { }); } } - - public getTitle(): string { - return this.name; - } } diff --git a/client/src/app/shared/models/topics/topic.ts b/client/src/app/shared/models/topics/topic.ts index a4125d0f5..8a5aa4d76 100644 --- a/client/src/app/shared/models/topics/topic.ts +++ b/client/src/app/shared/models/topics/topic.ts @@ -1,11 +1,12 @@ -import { AgendaBaseModel } from '../base/agenda-base-model'; -import { SearchRepresentation } from 'app/core/ui-services/search.service'; +import { BaseModel } from '../base/base-model'; /** * Representation of a topic. * @ignore */ -export class Topic extends AgendaBaseModel { +export class Topic extends BaseModel { + public static COLLECTIONSTRING = 'topics/topic'; + public id: number; public title: string; public text: string; @@ -13,36 +14,6 @@ export class Topic extends AgendaBaseModel { public agenda_item_id: number; public constructor(input?: any) { - super('topics/topic', 'Topic', input); - } - - public getTitle(): string { - return this.title; - } - - public getAgendaTitleWithType(): string { - // Do not append ' (Topic)' to the title. - return this.getAgendaTitle(); - } - - /** - * Formats the category for search - * - * @override - */ - public formatForSearch(): SearchRepresentation { - return [this.title, this.text]; - } - - public getDetailStateURL(): string { - return `/agenda/topics/${this.id}`; - } - - /** - * Returns the text to be inserted in csv exports - * @override - */ - public getCSVExportText(): string { - return this.text; + super(Topic.COLLECTIONSTRING, input); } } diff --git a/client/src/app/shared/models/users/group.ts b/client/src/app/shared/models/users/group.ts index 801baf359..40b269de5 100644 --- a/client/src/app/shared/models/users/group.ts +++ b/client/src/app/shared/models/users/group.ts @@ -5,12 +5,13 @@ import { BaseModel } from '../base/base-model'; * @ignore */ export class Group extends BaseModel { + public static COLLECTIONSTRING = 'users/group'; public id: number; public name: string; public permissions: string[]; public constructor(input?: any) { - super('users/group', 'Group', input); + super(Group.COLLECTIONSTRING, input); if (!input) { // permissions are required for new groups this.permissions = []; diff --git a/client/src/app/shared/models/users/personal-note.ts b/client/src/app/shared/models/users/personal-note.ts index 4f60b558b..538ebeb45 100644 --- a/client/src/app/shared/models/users/personal-note.ts +++ b/client/src/app/shared/models/users/personal-note.ts @@ -49,12 +49,14 @@ export interface PersonalNoteObject { * @ignore */ export class PersonalNote extends BaseModel implements PersonalNoteObject { + public static COLLECTIONSTRING = 'users/personal-note'; + public id: number; public user_id: number; public notes: PersonalNotesFormat; public constructor(input: any) { - super('users/personal-note', 'Personal note', input); + super(PersonalNote.COLLECTIONSTRING, input); } public getTitle(): string { diff --git a/client/src/app/shared/models/users/user.ts b/client/src/app/shared/models/users/user.ts index 9fe7eb1f0..3334c5e7b 100644 --- a/client/src/app/shared/models/users/user.ts +++ b/client/src/app/shared/models/users/user.ts @@ -1,7 +1,4 @@ -import { Searchable } from '../base/searchable'; -import { SearchRepresentation } from 'app/core/ui-services/search.service'; import { BaseModel } from '../base/base-model'; -import { DetailNavigable } from '../base/detail-navigable'; /** * Iterable pre selection of genders (sexes) @@ -12,7 +9,7 @@ export const genders = ['Female', 'Male', 'Diverse']; * Representation of a user in contrast to the operator. * @ignore */ -export class User extends BaseModel implements Searchable, DetailNavigable { +export class User extends BaseModel { public static COLLECTIONSTRING = 'users/user'; public id: number; @@ -34,89 +31,10 @@ export class User extends BaseModel implements Searchable, DetailNavigable public default_password: string; public constructor(input?: any) { - super(User.COLLECTIONSTRING, 'Participant', input); - } - - public get full_name(): string { - let name = this.short_name; - const additions: string[] = []; - - // addition: add number and structure level - const structure_level = this.structure_level ? this.structure_level.trim() : ''; - if (structure_level) { - additions.push(structure_level); - } - - const number = this.number ? this.number.trim() : null; - if (number) { - // TODO Translate - additions.push('No. ' + number); - } - - if (additions.length > 0) { - name += ' (' + additions.join(' ยท ') + ')'; - } - return name.trim(); + super(User.COLLECTIONSTRING, input); } public containsGroupId(id: number): boolean { return this.groups_id.some(groupId => groupId === id); } - - // TODO read config values for "users_sort_by" - - /** - * Getter for the short name (Title, given name, surname) - * - * @returns a non-empty string - */ - public get short_name(): string { - const title = this.title ? this.title.trim() : ''; - const firstName = this.first_name ? this.first_name.trim() : ''; - const lastName = this.last_name ? this.last_name.trim() : ''; - - // TODO need DS adjustment first first - // if (this.DS.getConfig('users_sort_by').value === 'last_name') { - // if (lastName && firstName) { - // shortName += `${lastName}, ${firstName}`; - // } else { - // shortName += lastName || firstName; - // } - // } - - let shortName = `${firstName} ${lastName}`; - - if (shortName.length <= 1) { - // We have at least one space from the concatination of - // first- and lastname. - shortName = this.username; - } - - if (title) { - shortName = `${title} ${shortName}`; - } - - return shortName; - } - - public getTitle(): string { - return this.full_name; - } - - public getListViewTitle(): string { - return this.short_name; - } - - public getDetailStateURL(): string { - return `/users/${this.id}`; - } - - /** - * Formats the category for search - * - * @override - */ - public formatForSearch(): SearchRepresentation { - return [this.title, this.first_name, this.last_name, this.structure_level, this.number]; - } } diff --git a/client/src/app/site/agenda/agenda.config.ts b/client/src/app/site/agenda/agenda.config.ts index 43dfac779..e65a82046 100644 --- a/client/src/app/site/agenda/agenda.config.ts +++ b/client/src/app/site/agenda/agenda.config.ts @@ -1,14 +1,22 @@ import { AppConfig } from '../../core/app-config'; import { Item } from '../../shared/models/agenda/item'; import { Topic } from '../../shared/models/topics/topic'; -import { AgendaRepositoryService } from 'app/core/repositories/agenda/agenda-repository.service'; +import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service'; import { TopicRepositoryService } from 'app/core/repositories/agenda/topic-repository.service'; +import { ViewTopic } from './models/view-topic'; +import { ViewItem } from './models/view-item'; export const AgendaAppConfig: AppConfig = { name: 'agenda', models: [ - { collectionString: 'agenda/item', model: Item, repository: AgendaRepositoryService }, - { collectionString: 'topics/topic', model: Topic, searchOrder: 1, repository: TopicRepositoryService } + { collectionString: 'agenda/item', model: Item, viewModel: ViewItem, repository: ItemRepositoryService }, + { + collectionString: 'topics/topic', + model: Topic, + viewModel: ViewTopic, + searchOrder: 1, + repository: TopicRepositoryService + } ], mainMenuEntries: [ { diff --git a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts index 542b50bcd..458b15810 100644 --- a/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts +++ b/client/src/app/site/agenda/components/agenda-list/agenda-list.component.ts @@ -5,7 +5,7 @@ import { Title } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; import { AgendaFilterListService } from '../../services/agenda-filter-list.service'; -import { AgendaRepositoryService } from 'app/core/repositories/agenda/agenda-repository.service'; +import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service'; import { ListViewBaseComponent } from 'app/site/base/list-view-base'; import { PromptService } from 'app/core/ui-services/prompt.service'; import { ViewItem } from '../../models/view-item'; @@ -63,7 +63,7 @@ export class AgendaListComponent extends ListViewBaseComponent impleme matSnackBar: MatSnackBar, private route: ActivatedRoute, private router: Router, - private repo: AgendaRepositoryService, + private repo: ItemRepositoryService, private promptService: PromptService, private dialog: MatDialog, private config: ConfigService, diff --git a/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.ts b/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.ts index 15182960d..d96bfb8b6 100644 --- a/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.ts +++ b/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.ts @@ -5,7 +5,7 @@ import { MatSnackBar } from '@angular/material'; import { TranslateService } from '@ngx-translate/core'; import { Observable } from 'rxjs'; -import { AgendaRepositoryService } from 'app/core/repositories/agenda/agenda-repository.service'; +import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service'; import { BaseViewComponent } from '../../../base/base-view'; import { OSTreeSortEvent } from 'app/shared/components/sorting-tree/sorting-tree.component'; import { ViewItem } from '../../models/view-item'; @@ -39,7 +39,7 @@ export class AgendaSortComponent extends BaseViewComponent { title: Title, translate: TranslateService, matSnackBar: MatSnackBar, - private agendaRepo: AgendaRepositoryService + private agendaRepo: ItemRepositoryService ) { super(title, translate, matSnackBar); this.itemsObservable = this.agendaRepo.getViewModelListObservable(); diff --git a/client/src/app/site/agenda/components/speaker-list/speaker-list.component.ts b/client/src/app/site/agenda/components/speaker-list/speaker-list.component.ts index d4734f5aa..85317d4aa 100644 --- a/client/src/app/site/agenda/components/speaker-list/speaker-list.component.ts +++ b/client/src/app/site/agenda/components/speaker-list/speaker-list.component.ts @@ -1,25 +1,25 @@ import { ActivatedRoute } from '@angular/router'; -import { BehaviorSubject, Subscription } from 'rxjs'; import { Component, OnInit } from '@angular/core'; import { FormGroup, FormControl } from '@angular/forms'; import { MatSnackBar, MatSelectChange } from '@angular/material'; import { TranslateService } from '@ngx-translate/core'; +import { BehaviorSubject, Subscription } from 'rxjs'; -import { AgendaRepositoryService } from 'app/core/repositories/agenda/agenda-repository.service'; +import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service'; import { BaseViewComponent } from 'app/site/base/base-view'; import { CurrentListOfSpeakersSlideService } from 'app/site/projector/services/current-list-of-of-speakers-slide.service'; -import { DataStoreService } from 'app/core/core-services/data-store.service'; -import { DurationService } from 'app/core/ui-services/duration.service'; import { OperatorService } from 'app/core/core-services/operator.service'; import { ProjectorRepositoryService } from 'app/core/repositories/projector/projector-repository.service'; import { PromptService } from 'app/core/ui-services/prompt.service'; import { SpeakerState } from 'app/shared/models/agenda/speaker'; import { Title } from '@angular/platform-browser'; -import { User } from 'app/shared/models/users/user'; import { ViewItem } from '../../models/view-item'; import { ViewSpeaker } from '../../models/view-speaker'; import { ViewProjector } from 'app/site/projector/models/view-projector'; +import { ViewUser } from 'app/site/users/models/view-user'; +import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service'; +import { DurationService } from 'app/core/ui-services/duration.service'; /** * The list of speakers for agenda items. @@ -68,7 +68,7 @@ export class SpeakerListComponent extends BaseViewComponent implements OnInit { /** * Hold the users */ - public users: BehaviorSubject; + public users: BehaviorSubject; /** * Required for the user search selector @@ -113,12 +113,12 @@ export class SpeakerListComponent extends BaseViewComponent implements OnInit { snackBar: MatSnackBar, projectorRepo: ProjectorRepositoryService, private route: ActivatedRoute, - private DS: DataStoreService, - private itemRepo: AgendaRepositoryService, + private itemRepo: ItemRepositoryService, private op: OperatorService, private promptService: PromptService, private currentListOfSpeakersService: CurrentListOfSpeakersSlideService, - private durationService: DurationService + private durationService: DurationService, + private userRepository: UserRepositoryService ) { super(title, translate, snackBar); this.isCurrentListOfSpeakers(); @@ -143,12 +143,8 @@ export class SpeakerListComponent extends BaseViewComponent implements OnInit { */ public ngOnInit(): void { // load and observe users - this.users = new BehaviorSubject(this.DS.getAll(User)); - this.DS.changeObservable.subscribe(model => { - if (model instanceof User) { - this.users.next(this.DS.getAll(User)); - } - }); + this.users = new BehaviorSubject(this.userRepository.getViewModelList()); + this.userRepository.getViewModelListObservable().subscribe(users => this.users.next(users)); // detect changes in the form this.addSpeakerForm.valueChanges.subscribe(formResult => { @@ -190,10 +186,10 @@ export class SpeakerListComponent extends BaseViewComponent implements OnInit { } this.projectorSubscription = this.currentListOfSpeakersService - .getAgendaItemIdObservable(projector) - .subscribe(agendaId => { - if (agendaId) { - this.setSpeakerList(agendaId); + .getAgendaItemObservable(projector) + .subscribe(item => { + if (item) { + this.setSpeakerList(item.id); } }); } diff --git a/client/src/app/site/agenda/components/topic-detail/topic-detail.component.html b/client/src/app/site/agenda/components/topic-detail/topic-detail.component.html index 643c9acdb..ecbb57e08 100644 --- a/client/src/app/site/agenda/components/topic-detail/topic-detail.component.html +++ b/client/src/app/site/agenda/components/topic-detail/topic-detail.component.html @@ -24,90 +24,89 @@ + +
+

{{ topic.title }}

+
+
+ + +
+
+
- -
-

{{ topic.title }}

-
+
+

+ Attachments: + + + {{ file.title }} + + + +

+
+ +
- - -
-
+ + + A name is required +
-
-

- Attachments: - - - {{ file.title }} - - - -

+ +
+

Text

+
- + + + +
+
- - A name is required + + + {{ type.name | translate }} + +
- -
-

Text

- + +
+
- - - - -
- -
- - - - {{ type.name | translate }} - - - -
- - -
- -
-
- - +
+ +
diff --git a/client/src/app/site/users/components/user-detail/user-detail.component.ts b/client/src/app/site/users/components/user-detail/user-detail.component.ts index 6f92d9165..c0dcabf6b 100644 --- a/client/src/app/site/users/components/user-detail/user-detail.component.ts +++ b/client/src/app/site/users/components/user-detail/user-detail.component.ts @@ -8,7 +8,7 @@ import { TranslateService } from '@ngx-translate/core'; import { BaseViewComponent } from '../../../base/base-view'; import { DataStoreService } from 'app/core/core-services/data-store.service'; -import { genders } from 'app/shared/models/users/user'; +import { genders, User } from 'app/shared/models/users/user'; import { Group } from 'app/shared/models/users/group'; import { OperatorService } from 'app/core/core-services/operator.service'; import { PromptService } from 'app/core/ui-services/prompt.service'; @@ -99,8 +99,25 @@ export class UserDetailComponent extends BaseViewComponent implements OnInit { private pdfService: UserPdfExportService ) { super(title, translate, matSnackBar); - - this.user = new ViewUser(); + // prevent 'undefined' to appear in the ui + const defaultUser: any = {}; + // tslint:disable-next-line + [ + 'username', + 'title', + 'first_name', + 'last_name', + 'gender', + 'structure_level', + 'number', + 'about_me', + 'email', + 'comment', + 'default_password' + ].forEach(property => { + defaultUser[property] = ''; + }); + this.user = new ViewUser(new User(defaultUser)); if (route.snapshot.url[0] && route.snapshot.url[0].path === 'new') { this.newUser = true; this.setEditMode(true); @@ -112,7 +129,7 @@ export class UserDetailComponent extends BaseViewComponent implements OnInit { this.ownPage = this.opOwnsPage(Number(params.id)); // observe operator to find out if we see our own page or not - this.operator.getObservable().subscribe(newOp => { + this.operator.getUserObservable().subscribe(newOp => { if (newOp) { this.ownPage = this.opOwnsPage(Number(params.id)); } diff --git a/client/src/app/site/users/components/user-import/user-import-list.component.html b/client/src/app/site/users/components/user-import/user-import-list.component.html index 0b86ed606..24625ff8d 100644 --- a/client/src/app/site/users/components/user-import/user-import-list.component.html +++ b/client/src/app/site/users/components/user-import/user-import-list.component.html @@ -222,7 +222,7 @@ {{ entry.newEntry.structure_level }} - + Participant number {{ entry.newEntry.user.number }} diff --git a/client/src/app/site/users/components/user-list/user-list.component.ts b/client/src/app/site/users/components/user-list/user-list.component.ts index 9ee9b31c4..d24c813aa 100644 --- a/client/src/app/site/users/components/user-list/user-list.component.ts +++ b/client/src/app/site/users/components/user-list/user-list.component.ts @@ -141,7 +141,7 @@ export class UserListComponent extends ListViewBaseComponent implement { property: 'first_name', label: 'Given name' }, { property: 'last_name', label: 'Surname' }, { property: 'structure_level', label: 'Structure level' }, - { property: 'participant_number', label: 'Participant number' }, + { property: 'number', label: 'Participant number' }, { label: 'groups', map: user => user.groups.map(group => group.name).join(',') }, { property: 'comment' }, { property: 'is_active', label: 'Is active' }, diff --git a/client/src/app/site/users/models/view-group.ts b/client/src/app/site/users/models/view-group.ts index 1d9b211d1..89b33fbaa 100644 --- a/client/src/app/site/users/models/view-group.ts +++ b/client/src/app/site/users/models/view-group.ts @@ -1,20 +1,19 @@ import { BaseViewModel } from '../../base/base-view-model'; import { Group } from 'app/shared/models/users/group'; -import { BaseModel } from 'app/shared/models/base/base-model'; export class ViewGroup extends BaseViewModel { private _group: Group; public get group(): Group { - return this._group ? this._group : null; + return this._group; } public get id(): number { - return this.group ? this.group.id : null; + return this.group.id; } public get name(): string { - return this.group ? this.group.name : null; + return this.group.name; } /** @@ -27,11 +26,11 @@ export class ViewGroup extends BaseViewModel { } public get permissions(): string[] { - return this.group ? this.group.permissions : null; + return this.group.permissions; } public constructor(group?: Group) { - super(); + super('Group'); this._group = group; } @@ -70,7 +69,7 @@ export class ViewGroup extends BaseViewModel { return this.name; } - public updateValues(update: BaseModel): void { + public updateDependencies(update: BaseViewModel): void { console.log('ViewGroups wants to update Values with : ', update); } } diff --git a/client/src/app/site/users/models/view-personal-note.ts b/client/src/app/site/users/models/view-personal-note.ts new file mode 100644 index 000000000..0edc632b2 --- /dev/null +++ b/client/src/app/site/users/models/view-personal-note.ts @@ -0,0 +1,27 @@ +import { BaseViewModel } from 'app/site/base/base-view-model'; +import { PersonalNote } from 'app/shared/models/users/personal-note'; + +export class ViewPersonalNote extends BaseViewModel { + private _personalNote: PersonalNote; + + public get personalNote(): PersonalNote { + return this._personalNote ? this._personalNote : null; + } + + public get id(): number { + return this.personalNote ? this.personalNote.id : null; + } + + public constructor(personalNote?: PersonalNote) { + super('Personal note'); + this._personalNote = personalNote; + } + + public getTitle(): string { + return this.personalNote ? this.personalNote.toString() : null; + } + + public updateDependencies(update: BaseViewModel): void { + throw new Error('Todo'); + } +} diff --git a/client/src/app/site/users/models/view-user.ts b/client/src/app/site/users/models/view-user.ts index ebcec97a7..3ce83d2df 100644 --- a/client/src/app/site/users/models/view-user.ts +++ b/client/src/app/site/users/models/view-user.ts @@ -1,67 +1,61 @@ import { User } from 'app/shared/models/users/user'; -import { Group } from 'app/shared/models/users/group'; -import { BaseModel } from 'app/shared/models/base/base-model'; -import { BaseProjectableModel } from 'app/site/base/base-projectable-model'; +import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model'; import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; +import { Searchable } from 'app/site/base/searchable'; +import { SearchRepresentation } from 'app/core/ui-services/search.service'; +import { ViewGroup } from './view-group'; +import { BaseViewModel } from 'app/site/base/base-view-model'; -export class ViewUser extends BaseProjectableModel { +export class ViewUser extends BaseProjectableViewModel implements Searchable { private _user: User; - private _groups: Group[]; + private _groups: ViewGroup[]; public get user(): User { - return this._user ? this._user : null; + return this._user; } - public get groups(): Group[] { + public get groups(): ViewGroup[] { return this._groups; } public get id(): number { - return this.user ? this.user.id : null; + return this.user.id; } public get username(): string { - return this.user ? this.user.username : null; + return this.user.username; } public get title(): string { - return this.user ? this.user.title : null; + return this.user.title; } public get first_name(): string { - return this.user ? this.user.first_name : null; + return this.user.first_name; } public get last_name(): string { - return this.user ? this.user.last_name : null; - } - - public get full_name(): string { - return this.user ? this.user.full_name : null; - } - - public get short_name(): string { - return this.user ? this.user.short_name : null; + return this.user.last_name; } public get email(): string { - return this.user ? this.user.email : null; + return this.user.email; } public get gender(): string { - return this.user ? this.user.gender : null; + return this.user.gender; } public get structure_level(): string { - return this.user ? this.user.structure_level : null; + return this.user.structure_level; } - public get participant_number(): string { - return this.user ? this.user.number : null; + public get number(): string { + return this.user.number; } public get groups_id(): number[] { - return this.user ? this.user.groups_id : null; + return this.user.groups_id; } /** @@ -74,42 +68,117 @@ export class ViewUser extends BaseProjectableModel { } public get default_password(): string { - return this.user ? this.user.default_password : null; + return this.user.default_password; } public get comment(): string { - return this.user ? this.user.comment : null; + return this.user.comment; } public get is_present(): boolean { - return this.user ? this.user.is_present : null; + return this.user.is_present; } public get is_active(): boolean { - return this.user ? this.user.is_active : null; + return this.user.is_active; } public get is_committee(): boolean { - return this.user ? this.user.is_committee : null; + return this.user.is_committee; } public get about_me(): string { - return this.user ? this.user.about_me : null; + return this.user.about_me; } public get is_last_email_send(): boolean { - if (this.user && this.user.last_email_send) { - return true; - } - return false; + return this.user && !!this.user.last_email_send; } - public constructor(user?: User, groups?: Group[]) { - super(); + // TODO read config values for "users_sort_by" + /** + * Getter for the short name (Title, given name, surname) + * + * @returns a non-empty string + */ + public get short_name(): string { + if (!this.user) { + return ''; + } + + const title = this.title ? this.title.trim() : ''; + const firstName = this.first_name ? this.first_name.trim() : ''; + const lastName = this.last_name ? this.last_name.trim() : ''; + + // TODO need DS adjustment first first + // if (this.DS.getConfig('users_sort_by').value === 'last_name') { + // if (lastName && firstName) { + // shortName += `${lastName}, ${firstName}`; + // } else { + // shortName += lastName || firstName; + // } + // } + + let shortName = `${firstName} ${lastName}`; + + if (shortName.length <= 1) { + // We have at least one space from the concatination of + // first- and lastname. + shortName = this.username; + } + + if (title) { + shortName = `${title} ${shortName}`; + } + + return shortName; + } + + public get full_name(): string { + if (!this.user) { + return ''; + } + + let name = this.short_name; + const additions: string[] = []; + + // addition: add number and structure level + const structure_level = this.structure_level ? this.structure_level.trim() : ''; + if (structure_level) { + additions.push(structure_level); + } + + const number = this.number ? this.number.trim() : ''; + if (number) { + // TODO Translate + additions.push('No. ' + number); + } + + if (additions.length > 0) { + name += ' (' + additions.join(' ยท ') + ')'; + } + return name.trim(); + } + + public constructor(user: User, groups?: ViewGroup[]) { + super('Participant'); this._user = user; this._groups = groups; } + /** + * Formats the category for search + * + * @override + */ + public formatForSearch(): SearchRepresentation { + return [this.title, this.first_name, this.last_name, this.structure_level, this.number]; + } + + public getDetailStateURL(): string { + throw new Error('TODO'); + } + public getSlide(): ProjectorElementBuildDeskriptor { return { getBasicProjectorElement: () => ({ @@ -127,38 +196,25 @@ export class ViewUser extends BaseProjectableModel { * required by BaseViewModel. Don't confuse with the users title. */ public getTitle(): string { - return this.user ? this.user.toString() : null; + return this.full_name; } /** * TODO: Implement */ - public replaceGroup(newGroup: Group): void {} + public replaceGroup(newGroup: ViewGroup): void {} - public updateValues(update: BaseModel): void { - if (update instanceof Group) { - this.updateGroup(update as Group); - } - if (update instanceof User) { - this.updateUser(update as User); + public updateDependencies(update: BaseViewModel): void { + if (update instanceof ViewGroup) { + this.updateGroup(update); } } - public updateGroup(update: Group): void { + public updateGroup(group: ViewGroup): void { if (this.user && this.user.groups_id) { - if (this.user.containsGroupId(update.id)) { - this.replaceGroup(update); + if (this.user.containsGroupId(group.id)) { + this.replaceGroup(group); } } } - /** - * Updates values. Triggered through observables. - * - * @param update a new User or Group - */ - public updateUser(update: User): void { - if (this.user.id === update.id) { - this._user = update; - } - } } diff --git a/client/src/app/site/users/services/user-import.service.ts b/client/src/app/site/users/services/user-import.service.ts index 5c23b4acc..779a4d94b 100644 --- a/client/src/app/site/users/services/user-import.service.ts +++ b/client/src/app/site/users/services/user-import.service.ts @@ -24,7 +24,7 @@ export class UserImportService extends BaseImportService { 'first_name', 'last_name', 'structure_level', - 'participant_number', + 'number', 'groups_id', 'comment', 'is_active', @@ -111,7 +111,7 @@ export class UserImportService extends BaseImportService { } } break; - case 'participant_number': + case 'number': newViewUser.user.number = line[idx]; break; default: diff --git a/client/src/app/site/users/services/user-sort-list.service.ts b/client/src/app/site/users/services/user-sort-list.service.ts index 3813c58bb..af9adee7c 100644 --- a/client/src/app/site/users/services/user-sort-list.service.ts +++ b/client/src/app/site/users/services/user-sort-list.service.ts @@ -16,7 +16,7 @@ export class UserSortListService extends BaseSortListService { { property: 'is_present', label: 'Presence' }, { property: 'is_active', label: 'Is active' }, { property: 'is_committee', label: 'Is Committee' }, - { property: 'participant_number', label: 'Number' }, + { property: 'number', label: 'Number' }, { property: 'structure_level', label: 'Structure level' }, { property: 'comment' } // TODO email send? diff --git a/client/src/app/site/users/users.config.ts b/client/src/app/site/users/users.config.ts index ecea1c412..83823b0b1 100644 --- a/client/src/app/site/users/users.config.ts +++ b/client/src/app/site/users/users.config.ts @@ -5,13 +5,27 @@ import { PersonalNote } from '../../shared/models/users/personal-note'; import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service'; import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service'; import { PersonalNoteRepositoryService } from 'app/core/repositories/users/personal-note-repository.service'; +import { ViewUser } from './models/view-user'; +import { ViewGroup } from './models/view-group'; +import { ViewPersonalNote } from './models/view-personal-note'; export const UsersAppConfig: AppConfig = { name: 'users', models: [ - { collectionString: 'users/user', model: User, searchOrder: 4, repository: UserRepositoryService }, - { collectionString: 'users/group', model: Group, repository: GroupRepositoryService }, - { collectionString: 'users/personal-note', model: PersonalNote, repository: PersonalNoteRepositoryService } + { + collectionString: 'users/user', + model: User, + viewModel: ViewUser, + searchOrder: 4, + repository: UserRepositoryService + }, + { collectionString: 'users/group', model: Group, viewModel: ViewGroup, repository: GroupRepositoryService }, + { + collectionString: 'users/personal-note', + model: PersonalNote, + viewModel: ViewPersonalNote, + repository: PersonalNoteRepositoryService + } ], mainMenuEntries: [ { diff --git a/openslides/utils/consumers.py b/openslides/utils/consumers.py index cbc12466c..6fa219552 100644 --- a/openslides/utils/consumers.py +++ b/openslides/utils/consumers.py @@ -119,9 +119,7 @@ class SiteConsumer(ProtocollAsyncJsonWebsocketConsumer): all_projector_data = event["data"] projector_data: Dict[int, Dict[str, Any]] = {} for projector_id in self.listen_projector_ids: - data = all_projector_data.get( - projector_id, {"error": f"No data for projector {projector_id}"} - ) + data = all_projector_data.get(projector_id, []) new_hash = hash(str(data)) if new_hash != self.projector_hash.get(projector_id): projector_data[projector_id] = data