Merge pull request #4240 from FinnStutzenstein/viewModelStore

first work on moving some logic to view models
This commit is contained in:
Sean 2019-02-08 11:52:39 +01:00 committed by GitHub
commit 392cb7fdaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
150 changed files with 1943 additions and 1519 deletions

View File

@ -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<BaseRepository<any, any>>;
}
export interface ModelEntry extends BaseModelEntry {
model: ModelConstructor<BaseModel>;
}
export interface ModelEntry extends BaseModelEntry {
viewModel: ViewModelConstructor<BaseViewModel>;
}
export interface SearchableModelEntry extends BaseModelEntry {
model: new (...args: any[]) => BaseModel & Searchable;
viewModel: new (...args: any[]) => BaseViewModel & Searchable;
searchOrder: number;
}

View File

@ -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<any, any> = 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<BaseModel>) 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

View File

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

View File

@ -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<BaseModel>,
ViewModelConstructor<BaseViewModel>,
BaseRepository<BaseViewModel, BaseModel>
];
/**
* 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<BaseModel>, BaseRepository<any, any>];
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<V extends BaseViewModel, M extends BaseModel>(
collectionString: string,
model: ModelConstructor<BaseModel>,
repository: BaseRepository<any, any>
model: ModelConstructor<M>,
viewModel: ViewModelConstructor<V>,
repository: BaseRepository<V, M>
): 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<BaseModel> {
return this.collectionStringsTypeMapping[collectionString][0];
// The following accessors are for giving one of EntryType by given a different object
// of EntryType.
public getCollectionStringFromModelConstructor<M extends BaseModel>(ctor: ModelConstructor<M>): string {
return this.modelMapping[ctor.name][0];
}
public getCollectionStringFromViewModelConstructor<V extends BaseViewModel>(ctor: ViewModelConstructor<V>): string {
return this.viewModelMapping[ctor.name][0];
}
public getCollectionStringFromRepository<M extends BaseModel, V extends BaseViewModel>(
repository: BaseRepository<V, M>
): 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<any, any> {
return this.collectionStringsTypeMapping[collectionString][1];
public getModelConstructorFromCollectionString<M extends BaseModel>(collectionString: string): ModelConstructor<M> {
return this.collectionStringMapping[collectionString][1] as ModelConstructor<M>;
}
public getModelConstructorFromViewModelConstructor<V extends BaseViewModel, M extends BaseModel>(
ctor: ViewModelConstructor<V>
): ModelConstructor<M> {
return this.viewModelMapping[ctor.name][1] as ModelConstructor<M>;
}
public getModelConstructorFromRepository<V extends BaseViewModel, M extends BaseModel>(
repository: BaseRepository<V, M>
): ModelConstructor<M> {
return this.repositoryMapping[repository.name][1] as ModelConstructor<M>;
}
/**
* Returns the collection string of a given ModelConstructor or undefined, if it is not registered.
* @param ctor
*/
public getCollectionString(ctor: ModelConstructor<BaseModel>): string {
return Object.keys(this.collectionStringsTypeMapping).find((collectionString: string) => {
return ctor === this.collectionStringsTypeMapping[collectionString][0];
});
public getViewModelConstructorFromCollectionString<M extends BaseViewModel>(
collectionString: string
): ViewModelConstructor<M> {
return this.collectionStringMapping[collectionString][2] as ViewModelConstructor<M>;
}
public getViewModelConstructorFromModelConstructor<V extends BaseViewModel, M extends BaseModel>(
ctor: ModelConstructor<M>
): ViewModelConstructor<V> {
return this.modelMapping[ctor.name][2] as ViewModelConstructor<V>;
}
public getViewModelConstructorFromRepository<V extends BaseViewModel, M extends BaseModel>(
repository: BaseRepository<V, M>
): ViewModelConstructor<V> {
return this.repositoryMapping[repository.name][2] as ViewModelConstructor<V>;
}
public getRepositoryFromCollectionString<V extends BaseViewModel, M extends BaseModel>(
collectionString: string
): BaseRepository<V, M> {
return this.collectionStringMapping[collectionString][3] as BaseRepository<V, M>;
}
public getRepositoryFromModelConstructor<V extends BaseViewModel, M extends BaseModel>(
ctor: ModelConstructor<M>
): BaseRepository<V, M> {
return this.modelMapping[ctor.name][3] as BaseRepository<V, M>;
}
public getRepositoryFromViewModelConstructor<V extends BaseViewModel, M extends BaseModel>(
ctor: ViewModelConstructor<V>
): BaseRepository<V, M> {
return this.viewModelMapping[ctor.name][3] as BaseRepository<V, M>;
}
}

View File

@ -72,6 +72,18 @@ export class DataStoreService {
*/
private readonly changedSubject: Subject<BaseModel> = new Subject<BaseModel>();
/**
* This is subject notify all subscribers _before_ the `secondaryModelChangeSubject`.
* It's the same subject as the changedSubject.
*/
public readonly primaryModelChangeSubject = new Subject<BaseModel>();
/**
* This is subject notify all subscribers _after_ the `primaryModelChangeSubject`.
* It's the same subject as the changedSubject.
*/
public readonly secondaryModelChangeSubject = new Subject<BaseModel>();
/**
* 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);
}
}

View File

@ -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<User> = new BehaviorSubject<User>(null);
/**
* Subject for the operator as a view user.
*/
private viewOperatorSubject: BehaviorSubject<ViewUser> = new BehaviorSubject<ViewUser>(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<User> {
public getUserObservable(): Observable<User> {
return this.operatorSubject.asObservable();
}
public getViewUserObservable(): Observable<ViewUser> {
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);
}
}

View File

@ -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<T extends BaseModel>(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<T extends BaseModel>(element: IdentifiableProjectorElement): T {
this.assertElementIsMappable(element);
return this.DS.get<T>(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<T extends BaseViewModel>(element: IdentifiableProjectorElement): T {
this.assertElementIsMappable(element);
return this.viewModelStore.get<T>(element.name, element.id);
}
/**
* Projects the next slide in the queue. Moves all currently projected
* non-stable slides to the history.

View File

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

View File

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

View File

@ -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<T extends BaseViewModel>(
collectionType: ViewModelConstructor<T> | string
): BaseRepository<T, any> {
if (typeof collectionType === 'string') {
return this.mapperService.getRepositoryFromCollectionString(collectionType) as BaseRepository<T, any>;
} else {
return this.mapperService.getRepositoryFromViewModelConstructor(collectionType as ViewModelConstructor<T>);
}
}
/**
* 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<T extends BaseViewModel>(collectionType: ViewModelConstructor<T> | 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<T extends BaseViewModel>(collectionType: ViewModelConstructor<T> | string, ids: number[]): T[] {
const repository = this.getRepository<T>(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<T extends BaseViewModel>(collectionType: ViewModelConstructor<T> | 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<T extends BaseViewModel>(collectionString: string, callback: (model: T) => boolean): T[] {
return this.getAll<T>(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<T extends BaseViewModel>(collectionString: string, callback: (model: T) => boolean): T {
return this.getAll<T>(collectionString).find(callback);
}
}

View File

@ -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<OnAfterAppsLoaded>[] = [OperatorService];
/**
* Global Core Module.

View File

@ -0,0 +1,9 @@
/**
* A lifecyclehook to be called, after all apps are loaded.
*/
export interface OnAfterAppsLoaded {
/**
* The hook to call
*/
onAfterAppsLoaded(): void;
}

View File

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

View File

@ -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<ViewItem, Item> {
export class ItemRepositoryService extends BaseRepository<ViewItem, Item> {
/**
* Contructor for agenda repository.
*
@ -39,38 +40,39 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
* @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<BaseModel>(
public getContentObject(agendaItem: Item): BaseAgendaViewModel {
const contentObject = this.viewModelStoreService.get<BaseViewModel>(
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<ViewItem, Item> {
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));
});
}

View File

@ -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<ViewTopic, Topic> {
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<ViewTopic, Topic> {
* @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
*

View File

@ -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,
* @param DS The DataStore
* @param mapperService Maps collection strings to classes
*/
public constructor(DS: DataStoreService, mapperService: CollectionStringMapperService) {
super(DS, mapperService, Assignment, [User, Item, Tag]);
public constructor(
DS: DataStoreService,
mapperService: CollectionStringMapperService,
viewModelStoreService: ViewModelStoreService
) {
super(DS, mapperService, viewModelStoreService, Assignment, [User, Item, Tag]);
}
public async update(assignment: Partial<Assignment>, viewAssignment: ViewAssignment): Promise<void> {
@ -41,9 +49,9 @@ export class AssignmentRepositoryService extends BaseRepository<ViewAssignment,
}
public createViewModel(assignment: Assignment): ViewAssignment {
const relatedUser = this.DS.getMany(User, assignment.candidateIds);
const agendaItem = this.DS.get(Item, assignment.agenda_item_id);
const tags = this.DS.getMany(Tag, assignment.tags_id);
const relatedUser = this.viewModelStoreService.getMany(ViewUser, assignment.candidateIds);
const agendaItem = this.viewModelStoreService.get(ViewItem, assignment.agenda_item_id);
const tags = this.viewModelStoreService.getMany(ViewTag, assignment.tags_id);
return new ViewAssignment(assignment, relatedUser, agendaItem, tags);
}

View File

@ -7,8 +7,11 @@ import { CollectionStringMapperService } from '../core-services/collectionString
import { DataStoreService } from '../core-services/data-store.service';
import { Identifiable } from '../../shared/models/base/identifiable';
import { auditTime } from 'rxjs/operators';
import { ViewModelStoreService } from '../core-services/view-model-store.service';
import { OnAfterAppsLoaded } from '../onAfterAppsLoaded';
export abstract class BaseRepository<V extends BaseViewModel, M extends BaseModel> extends OpenSlidesComponent {
export abstract class BaseRepository<V extends BaseViewModel, M extends BaseModel> extends OpenSlidesComponent
implements OnAfterAppsLoaded {
/**
* Stores all the viewModel in an object
*/
@ -29,26 +32,33 @@ export abstract class BaseRepository<V extends BaseViewModel, M extends BaseMode
*/
protected readonly generalViewModelSubject: Subject<V> = new Subject<V>();
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<M>,
protected depsModelCtors?: ModelConstructor<BaseModel>[]
) {
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<V extends BaseViewModel, M extends BaseMode
});
// Could be raise in error if the root injector is not known
this.DS.changeObservable.subscribe(model => {
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<V extends BaseViewModel, M extends BaseMode
protected updateViewModelObservable(id: number): void {
if (this.viewModelSubjects[id]) {
this.viewModelSubjects[id].next(this.viewModelStore[id]);
this.generalViewModelSubject.next(this.viewModelStore[id]);
}
this.generalViewModelSubject.next(this.viewModelStore[id]);
}
/**

View File

@ -5,13 +5,18 @@ import { Identifiable } from 'app/shared/models/base/identifiable';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { ChatMessage } from 'app/shared/models/core/chat-message';
import { ViewChatMessage } from 'app/site/common/models/view-chatmessage';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
@Injectable({
providedIn: 'root'
})
export class ChatMessageRepositoryService extends BaseRepository<ViewChatMessage, ChatMessage> {
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 {

View File

@ -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<ViewConfig, Config>
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<ViewConfig, Config>
* 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<ConfigGroup[]>(null);
}

View File

@ -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<ViewHistory, Histor
public constructor(
DS: DataStoreService,
mapperService: CollectionStringMapperService,
viewModelStoreService: ViewModelStoreService,
private httpService: HttpService,
private timeTravel: TimeTravelService
) {
super(DS, mapperService, History, [User]);
super(DS, mapperService, viewModelStoreService, History, [User]);
}
/**
@ -70,12 +72,11 @@ export class HistoryRepositoryService extends BaseRepository<ViewHistory, Histor
* @returns the ListTitle or null if the model was deleted already
*/
public getOldModelInfo(collectionString: string, id: number): string {
const oldModel: BaseModel = this.DS.get(collectionString, id);
if (oldModel) {
return oldModel.getListTitle();
} else {
return null;
const model = this.viewModelStoreService.get(collectionString, id);
if (model) {
return model.getListTitle();
}
return null;
}
/**
@ -85,7 +86,7 @@ export class HistoryRepositoryService extends BaseRepository<ViewHistory, Histor
* @return a new ViewHistory object
*/
public createViewModel(history: History): ViewHistory {
const user = this.DS.get(User, history.user_id);
const user = this.viewModelStoreService.get(ViewUser, history.user_id);
return new ViewHistory(history, user);
}

View File

@ -10,6 +10,8 @@ import { CollectionStringMapperService } from '../../core-services/collectionStr
import { DataSendService } from 'app/core/core-services/data-send.service';
import { HttpService } from 'app/core/core-services/http.service';
import { HttpHeaders } from '@angular/common/http';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewUser } from 'app/site/users/models/view-user';
/**
* Repository for MediaFiles
@ -28,10 +30,11 @@ export class MediafileRepositoryService extends BaseRepository<ViewMediafile, Me
public constructor(
DS: DataStoreService,
mapperService: CollectionStringMapperService,
viewModelStoreService: ViewModelStoreService,
private dataSend: DataSendService,
private httpService: HttpService
) {
super(DS, mapperService, Mediafile, [User]);
super(DS, mapperService, viewModelStoreService, Mediafile, [User]);
}
/**
@ -88,7 +91,7 @@ export class MediafileRepositoryService extends BaseRepository<ViewMediafile, Me
* @returns a new mediafile ViewModel
*/
public createViewModel(file: Mediafile): ViewMediafile {
const uploader = this.DS.get(User, file.uploader_id);
const uploader = this.viewModelStoreService.get(ViewUser, file.uploader_id);
return new ViewMediafile(file, uploader);
}
}

View File

@ -9,6 +9,7 @@ import { Motion } from 'app/shared/models/motions/motion';
import { HttpService } from '../../core-services/http.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';
/**
* Repository Services for Categories
@ -37,10 +38,11 @@ export class CategoryRepositoryService extends BaseRepository<ViewCategory, Cate
public constructor(
protected DS: DataStoreService,
mapperService: CollectionStringMapperService,
viewModelStoreService: ViewModelStoreService,
private dataSend: DataSendService,
private httpService: HttpService
) {
super(DS, mapperService, Category);
super(DS, mapperService, viewModelStoreService, Category);
}
protected createViewModel(category: Category): ViewCategory {

View File

@ -3,16 +3,17 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { DataSendService } from '../../core-services/data-send.service';
import { DataSendService } from 'app/core/core-services/data-send.service';
import { User } from 'app/shared/models/users/user';
import { Category } from 'app/shared/models/motions/category';
import { Workflow } from 'app/shared/models/motions/workflow';
import { BaseRepository } from '../base-repository';
import { DataStoreService } from '../../core-services/data-store.service';
import { MotionChangeReco } from 'app/shared/models/motions/motion-change-reco';
import { ViewChangeReco } from 'app/site/motions/models/view-change-reco';
import { DataStoreService } from 'app/core/core-services/data-store.service';
import { MotionChangeRecommendation } from 'app/shared/models/motions/motion-change-reco';
import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-change-recommendation';
import { Identifiable } from 'app/shared/models/base/identifiable';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { CollectionStringMapperService } from 'app/core/core-services/collectionStringMapper.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
/**
* Repository Services for change recommendations
@ -27,7 +28,10 @@ import { CollectionStringMapperService } from '../../core-services/collectionStr
@Injectable({
providedIn: 'root'
})
export class ChangeRecommendationRepositoryService extends BaseRepository<ViewChangeReco, MotionChangeReco> {
export class ChangeRecommendationRepositoryService extends BaseRepository<
ViewMotionChangeRecommendation,
MotionChangeRecommendation
> {
/**
* Creates a MotionRepository
*
@ -41,18 +45,19 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<ViewCh
public constructor(
DS: DataStoreService,
mapperService: CollectionStringMapperService,
viewModelStoreService: ViewModelStoreService,
private dataSend: DataSendService
) {
super(DS, mapperService, MotionChangeReco, [Category, User, Workflow]);
super(DS, mapperService, viewModelStoreService, MotionChangeRecommendation, [Category, User, Workflow]);
}
/**
* Creates a change recommendation
* Creates a (real) change recommendation and delegates it to the {@link DataSendService}
*
* @param {MotionChangeReco} changeReco
* @param {MotionChangeRecommendation} changeReco
*/
public async create(changeReco: MotionChangeReco): Promise<Identifiable> {
public async create(changeReco: MotionChangeRecommendation): Promise<Identifiable> {
return await this.dataSend.createModel(changeReco);
}
@ -61,17 +66,17 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<ViewCh
* @param view
* @returns The id of the created change recommendation
*/
public async createByViewModel(view: ViewChangeReco): Promise<Identifiable> {
public async createByViewModel(view: ViewMotionChangeRecommendation): Promise<Identifiable> {
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<ViewCh
*
* Extract the change recommendation out of the viewModel and delegate
* to {@link DataSendService}
* @param {ViewChangeReco} viewModel
* @param {ViewMotionChangeRecommendation} viewModel
*/
public async delete(viewModel: ViewChangeReco): Promise<void> {
public async delete(viewModel: ViewMotionChangeRecommendation): Promise<void> {
await this.dataSend.deleteModel(viewModel.changeRecommendation);
}
@ -91,10 +96,13 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<ViewCh
* Updates a (real) change recommendation with patched data and delegate it
* to the {@link DataSendService}
*
* @param {Partial<MotionChangeReco>} 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<MotionChangeRecommendation>} 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<MotionChangeReco>, viewModel: ViewChangeReco): Promise<void> {
public async update(
update: Partial<MotionChangeRecommendation>,
viewModel: ViewMotionChangeRecommendation
): Promise<void> {
const changeReco = viewModel.changeRecommendation;
changeReco.patchValues(update);
await this.dataSend.partialUpdateModel(changeReco);
@ -103,9 +111,9 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<ViewCh
/**
* return the Observable of all change recommendations belonging to the given motion
*/
public getChangeRecosOfMotionObservable(motion_id: number): Observable<ViewChangeReco[]> {
public getChangeRecosOfMotionObservable(motion_id: number): Observable<ViewMotionChangeRecommendation[]> {
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<ViewCh
* @param motionId the id of the target motion
* @returns the array of change recommendations to the motions.
*/
public getChangeRecoOfMotion(motion_id: number): ViewChangeReco[] {
public getChangeRecoOfMotion(motion_id: number): ViewMotionChangeRecommendation[] {
return this.getViewModelList().filter(reco => reco.motion_id === motion_id);
}
/**
* Sets a change recommendation to accepted.
*
* @param {ViewChangeReco} change
* @param {ViewMotionChangeRecommendation} change
*/
public async setAccepted(change: ViewChangeReco): Promise<void> {
public async setAccepted(change: ViewMotionChangeRecommendation): Promise<void> {
const changeReco = change.changeRecommendation;
changeReco.patchValues({
rejected: false
@ -137,9 +145,9 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<ViewCh
/**
* Sets a change recommendation to rejected.
*
* @param {ViewChangeReco} change
* @param {ViewMotionChangeRecommendation} change
*/
public async setRejected(change: ViewChangeReco): Promise<void> {
public async setRejected(change: ViewMotionChangeRecommendation): Promise<void> {
const changeReco = change.changeRecommendation;
changeReco.patchValues({
rejected: true
@ -150,10 +158,10 @@ export class ChangeRecommendationRepositoryService extends BaseRepository<ViewCh
/**
* Sets if a change recommendation is internal (for the administrators) or not.
*
* @param {ViewChangeReco} change
* @param {ViewMotionChangeRecommendation} change
* @param {boolean} internal
*/
public async setInternal(change: ViewChangeReco, internal: boolean): Promise<void> {
public async setInternal(change: ViewMotionChangeRecommendation, internal: boolean): Promise<void> {
const changeReco = change.changeRecommendation;
changeReco.patchValues({
internal: internal

View File

@ -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<ViewMotionBlock
public constructor(
DS: DataStoreService,
mapperService: CollectionStringMapperService,
viewModelStoreService: ViewModelStoreService,
private dataSend: DataSendService,
private motionRepo: MotionRepositoryService,
private httpService: HttpService
) {
super(DS, mapperService, MotionBlock);
super(DS, mapperService, viewModelStoreService, MotionBlock, [Item]);
}
/**
@ -80,7 +84,8 @@ export class MotionBlockRepositoryService extends BaseRepository<ViewMotionBlock
* @returns a new ViewMotionBlock
*/
protected createViewModel(block: MotionBlock): ViewMotionBlock {
return new ViewMotionBlock(block);
const item = this.viewModelStoreService.get(ViewItem, block.agenda_item_id);
return new ViewMotionBlock(block, item);
}
/**

View File

@ -9,6 +9,8 @@ import { Group } from 'app/shared/models/users/group';
import { Identifiable } from 'app/shared/models/base/identifiable';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { HttpService } from 'app/core/core-services/http.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewGroup } from 'app/site/users/models/view-group';
/**
* Repository Services for Categories
@ -38,12 +40,13 @@ export class MotionCommentSectionRepositoryService extends BaseRepository<
* @param http Service to handle direct http-communication
*/
public constructor(
DS: DataStoreService,
mapperService: CollectionStringMapperService,
protected DS: DataStoreService,
viewModelStoreService: ViewModelStoreService,
private dataSend: DataSendService,
private http: HttpService
) {
super(DS, mapperService, MotionCommentSection, [Group]);
super(DS, mapperService, viewModelStoreService, MotionCommentSection, [Group]);
}
/**
@ -53,9 +56,9 @@ export class MotionCommentSectionRepositoryService extends BaseRepository<
* @returns the View Model representation of the MotionCommentSection
*/
protected createViewModel(section: MotionCommentSection): ViewMotionCommentSection {
const read_groups = this.DS.getMany(Group, section.read_groups_id);
const write_groups = this.DS.getMany(Group, section.write_groups_id);
return new ViewMotionCommentSection(section, read_groups, write_groups);
const readGroups = this.viewModelStoreService.getMany(ViewGroup, section.read_groups_id);
const writeGroups = this.viewModelStoreService.getMany(ViewGroup, section.write_groups_id);
return new ViewMotionCommentSection(section, readGroups, writeGroups);
}
/**

View File

@ -19,19 +19,27 @@ import { LinenumberingService } from '../../ui-services/linenumbering.service';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
import { Motion } from 'app/shared/models/motions/motion';
import { MotionBlock } from 'app/shared/models/motions/motion-block';
import { MotionChangeReco } from 'app/shared/models/motions/motion-change-reco';
import { MotionChangeRecommendation } from 'app/shared/models/motions/motion-change-reco';
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
import { OSTreeSortEvent } from 'app/shared/components/sorting-tree/sorting-tree.component';
import { PersonalNoteService } from '../../ui-services/personal-note.service';
import { TreeService } from 'app/core/ui-services/tree.service';
import { User } from '../../../shared/models/users/user';
import { ViewChangeReco } from '../../../site/motions/models/view-change-reco';
import { ViewMotionAmendedParagraph } from '../../../site/motions/models/view-motion-amended-paragraph';
import { ViewUnifiedChange } from '../../../site/motions/models/view-unified-change';
import { ViewStatuteParagraph } from '../../../site/motions/models/view-statute-paragraph';
import { Workflow } from '../../../shared/models/motions/workflow';
import { WorkflowState } from '../../../shared/models/motions/workflow-state';
import { User } from 'app/shared/models/users/user';
import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-change-recommendation';
import { ViewMotionAmendedParagraph } from 'app/site/motions/models/view-motion-amended-paragraph';
import { ViewUnifiedChange } from 'app/site/motions/models/view-unified-change';
import { ViewStatuteParagraph } from 'app/site/motions/models/view-statute-paragraph';
import { Workflow } from 'app/shared/models/motions/workflow';
import { WorkflowState } from 'app/shared/models/motions/workflow-state';
import { Tag } from 'app/shared/models/core/tag';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewCategory } from 'app/site/motions/models/view-category';
import { ViewUser } from 'app/site/users/models/view-user';
import { ViewWorkflow } from 'app/site/motions/models/view-workflow';
import { ViewItem } from 'app/site/agenda/models/view-item';
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { ViewTag } from 'app/site/tags/models/view-tag';
/**
* Repository Services for motions (and potentially categories)
@ -64,6 +72,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
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<ViewMotion, Motion>
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<ViewMotion, Motion>
* @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<ViewMotion, Motion>
* @param viewMotion The motion to change the submitters from
* @param submitters The submitters to set
*/
public async setSubmitters(viewMotion: ViewMotion, submitters: User[]): Promise<void> {
public async setSubmitters(viewMotion: ViewMotion, submitters: ViewUser[]): Promise<void> {
const requestData = {
motions: [
{
@ -527,8 +544,8 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
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;
@ -536,7 +553,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
changeReco.rejected = false;
changeReco.motion_id = motionId;
return new ViewChangeReco(changeReco);
return new ViewMotionChangeRecommendation(changeReco);
}
/**

View File

@ -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<ViewStatut
public constructor(
DS: DataStoreService,
mapperService: CollectionStringMapperService,
viewModelStoreService: ViewModelStoreService,
private dataSend: DataSendService
) {
super(DS, mapperService, StatuteParagraph);
super(DS, mapperService, viewModelStoreService, StatuteParagraph);
}
protected createViewModel(statuteParagraph: StatuteParagraph): ViewStatuteParagraph {

View File

@ -10,6 +10,7 @@ import { CollectionStringMapperService } from '../../core-services/collectionStr
import { WorkflowState } from 'app/shared/models/motions/workflow-state';
import { ViewMotion } from 'app/site/motions/models/view-motion';
import { HttpService } from 'app/core/core-services/http.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
/**
* Repository Services for Categories
@ -40,12 +41,13 @@ export class WorkflowRepositoryService extends BaseRepository<ViewWorkflow, Work
* @param httpService HttpService
*/
public constructor(
protected DS: DataStoreService,
DS: DataStoreService,
mapperService: CollectionStringMapperService,
private dataSend: DataSendService,
private httpService: HttpService
private httpService: HttpService,
viewModelStoreService: ViewModelStoreService,
private dataSend: DataSendService
) {
super(DS, mapperService, Workflow);
super(DS, mapperService, viewModelStoreService, Workflow);
}
/**

View File

@ -6,6 +6,7 @@ import { Identifiable } from 'app/shared/models/base/identifiable';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { ViewCountdown } from 'app/site/projector/models/view-countdown';
import { Countdown } from 'app/shared/models/core/countdown';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
@Injectable({
providedIn: 'root'
@ -14,9 +15,10 @@ export class CountdownRepositoryService extends BaseRepository<ViewCountdown, Co
public constructor(
DS: DataStoreService,
mapperService: CollectionStringMapperService,
viewModelStoreService: ViewModelStoreService,
private dataSend: DataSendService
) {
super(DS, mapperService, Countdown);
super(DS, mapperService, viewModelStoreService, Countdown);
}
protected createViewModel(countdown: Countdown): ViewCountdown {

View File

@ -8,6 +8,7 @@ import { Identifiable } from 'app/shared/models/base/identifiable';
import { ViewProjector } from 'app/site/projector/models/view-projector';
import { Projector } from 'app/shared/models/core/projector';
import { HttpService } from 'app/core/core-services/http.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
/**
* Directions for scale and scroll requests.
@ -36,10 +37,11 @@ export class ProjectorRepositoryService extends BaseRepository<ViewProjector, Pr
public constructor(
DS: DataStoreService,
mapperService: CollectionStringMapperService,
viewModelStoreService: ViewModelStoreService,
private dataSend: DataSendService,
private http: HttpService
) {
super(DS, mapperService, Projector);
super(DS, mapperService, viewModelStoreService, Projector);
}
/**

View File

@ -5,13 +5,18 @@ import { Identifiable } from 'app/shared/models/base/identifiable';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { ProjectorMessage } from 'app/shared/models/core/projector-message';
import { ViewProjectorMessage } from 'app/site/projector/models/view-projectormessage';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
@Injectable({
providedIn: 'root'
})
export class ProjectorMessageRepositoryService extends BaseRepository<ViewProjectorMessage, ProjectorMessage> {
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 {

View File

@ -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<ViewTag, Tag> {
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 {

View File

@ -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<ViewGroup, Group> {
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();
}

View File

@ -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<any, PersonalNote> {
export class PersonalNoteRepositoryService extends BaseRepository<ViewPersonalNote, PersonalNote> {
/**
* @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<Identifiable> {
throw new Error('TODO');
}
public async update(personalNote: Partial<PersonalNote>, viewPersonalNote: any): Promise<void> {
public async update(personalNote: Partial<PersonalNote>, viewPersonalNote: ViewPersonalNote): Promise<void> {
throw new Error('TODO');
}
public async delete(viewPersonalNote: any): Promise<void> {
public async delete(viewPersonalNote: ViewPersonalNote): Promise<void> {
throw new Error('TODO');
}
}

View File

@ -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<ViewUser, User> {
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<ViewUser, User> {
}
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<ViewUser, User> {
* @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<ViewUser, User> {
* @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);
}
/**

View File

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

View File

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

View File

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

View File

@ -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';
/**

View File

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

View File

@ -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';
/**

View File

@ -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;
}
/**

View File

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

View File

@ -1,4 +1,4 @@
import { Displayable } from '../models/base/displayable';
import { Displayable } from '../../site/base/displayable';
import { Identifiable } from '../models/base/identifiable';
/**

View File

@ -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';

View File

@ -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';
/**

View File

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

View File

@ -41,7 +41,7 @@ export class Item extends BaseModel<Item> {
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<Item> {
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();
}
}

View File

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

View File

@ -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<Assignment> {
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';
}
}

View File

@ -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<T extends BaseModel<T>> = new (...args: any[]) => T;
@ -10,7 +10,7 @@ export type ModelConstructor<T extends BaseModel<T>> = new (...args: any[]) => T
* When inherit from this class, give the subclass as the type. E.g. `class Motion extends BaseModel<Motion>`
*/
export abstract class BaseModel<T = object> 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<T = object> 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<T = object> 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<T = object> 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

View File

@ -0,0 +1,6 @@
/**
* Every implementing object should have a collection string.
*/
export interface Collection {
readonly collectionString: string;
}

View File

@ -5,16 +5,13 @@ import { BaseModel } from '../base/base-model';
* @ignore
*/
export class ChatMessage extends BaseModel<ChatMessage> {
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);
}
}

View File

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

View File

@ -14,10 +14,6 @@ export class Countdown extends BaseModel<Countdown> {
public running: boolean;
public constructor(input?: any) {
super(Countdown.COLLECTIONSTRING, 'Countdown', input);
}
public getTitle(): string {
return this.description;
super(Countdown.COLLECTIONSTRING, input);
}
}

View File

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

View File

@ -11,10 +11,6 @@ export class ProjectorMessage extends BaseModel<ProjectorMessage> {
public message: string;
public constructor(input?: any) {
super(ProjectorMessage.COLLECTIONSTRING, 'Message', input);
}
public getTitle(): string {
return 'Projectormessage';
super(ProjectorMessage.COLLECTIONSTRING, input);
}
}

View File

@ -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<Projector> {
public static COLLECTIONSTRING = 'core/projector';
public id: number;
public elements: ProjectorElements;
public elements_preview: ProjectorElements;
@ -60,7 +65,7 @@ export class Projector extends BaseModel<Projector> {
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<Projector> {
[[], []] as [T[], T[]]
);
}
public getTitle(): string {
return this.name;
}
}

View File

@ -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<Tag> implements Searchable {
export class Tag extends BaseModel<Tag> {
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);
}
}

View File

@ -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<Mediafile> implements Searchable {
export class Mediafile extends BaseModel<Mediafile> {
public id: number;
public title: string;
public mediafile: File;
@ -17,7 +16,7 @@ export class Mediafile extends BaseModel<Mediafile> 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<Mediafile> 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();
}
}

View File

@ -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<Category> implements Searchable {
export class Category extends BaseModel<Category> {
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);
}
}

View File

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

View File

@ -4,7 +4,9 @@ import { BaseModel } from '../base/base-model';
* Representation of a motion change recommendation.
* @ignore
*/
export class MotionChangeReco extends BaseModel<MotionChangeReco> {
export class MotionChangeRecommendation extends BaseModel<MotionChangeRecommendation> {
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<MotionChangeReco> {
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);
}
}

View File

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

View File

@ -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 <identifier>'.
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 <identifier>' 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);

View File

@ -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<StatuteParagraph> implements Searchable {
export class StatuteParagraph extends BaseModel<StatuteParagraph> {
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);
}
}

View File

@ -6,17 +6,15 @@ import { WorkflowState } from './workflow-state';
* @ignore
*/
export class Workflow extends BaseModel<Workflow> {
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<Workflow> {
});
}
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<Workflow> {
});
}
}
public getTitle(): string {
return this.name;
}
}

View File

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

View File

@ -5,12 +5,13 @@ import { BaseModel } from '../base/base-model';
* @ignore
*/
export class Group extends BaseModel<Group> {
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 = [];

View File

@ -49,12 +49,14 @@ export interface PersonalNoteObject {
* @ignore
*/
export class PersonalNote extends BaseModel<PersonalNote> 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 {

View File

@ -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<User> implements Searchable, DetailNavigable {
export class User extends BaseModel<User> {
public static COLLECTIONSTRING = 'users/user';
public id: number;
@ -34,89 +31,10 @@ export class User extends BaseModel<User> 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];
}
}

View File

@ -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: [
{

View File

@ -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<ViewItem> impleme
matSnackBar: MatSnackBar,
private route: ActivatedRoute,
private router: Router,
private repo: AgendaRepositoryService,
private repo: ItemRepositoryService,
private promptService: PromptService,
private dialog: MatDialog,
private config: ConfigService,

View File

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

View File

@ -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<User[]>;
public users: BehaviorSubject<ViewUser[]>;
/**
* 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);
}
});
}

View File

@ -24,90 +24,89 @@
</div>
</os-head-bar>
<mat-card *ngIf="topic || editTopic"
[ngClass]="editTopic ? 'os-form-card' : 'os-card'"
class="on-transition-fade">
<div *ngIf="!editTopic">
<h1>{{ topic.title }}</h1>
</div>
<div>
<span *ngIf="!editTopic">
<!-- Render topic text as HTML -->
<div [innerHTML]="topic.text"></div>
</span>
</div>
<mat-card *ngIf="topic || editTopic"
[ngClass]="editTopic ? 'os-form-card' : 'os-card'"
class="on-transition-fade">
<div *ngIf="!editTopic">
<h1>{{ topic.title }}</h1>
</div>
<div *ngIf="topic.hasAttachments() && !editTopic">
<h3>
<span translate>Attachments</span>:
<mat-list dense>
<mat-list-item *ngFor="let file of topic.attachments">
<a [routerLink]="file.downloadUrl" target="_blank">{{ file.title }}</a>
</mat-list-item>
</mat-list>
<!-- TODO: Mediafiles and attachments are not fully implemented -->
</h3>
</div>
<form *ngIf="editTopic" [formGroup]="topicForm" (keydown)="onKeyDown($event)" (ngSubmit)="saveTopic()">
<div>
<span *ngIf="!editTopic">
<!-- Render topic text as HTML -->
<div [innerHTML]="topic.text"></div>
</span>
<mat-form-field>
<input
type="text"
matInput
osAutofocus
required
formControlName="title"
placeholder="{{ 'Title' | translate }}"
/>
<mat-error *ngIf="topicForm.invalid" translate>A name is required</mat-error>
</mat-form-field>
</div>
<div *ngIf="topic.hasAttachments() && !editTopic">
<h3>
<span translate>Attachments</span>:
<mat-list dense>
<mat-list-item *ngFor="let file of topic.attachments">
<a [routerLink]="file.getDownloadUrl()" target="_blank">{{ file.title }}</a>
</mat-list-item>
</mat-list>
<!-- TODO: Mediafiles and attachments are not fully implemented -->
</h3>
<!-- The editor -->
<div class="spacer-bottom-20">
<h4 translate>Text</h4>
<editor formControlName="text" [init]="tinyMceSettings"></editor>
</div>
<form *ngIf="editTopic" [formGroup]="topicForm" (keydown)="onKeyDown($event)" (ngSubmit)="saveTopic()">
<!-- Attachments -->
<os-search-value-selector
ngDefaultControl
[form]="topicForm"
[formControl]="topicForm.get('attachments_id')"
[multiple]="true"
listname="{{ 'Attachments' | translate }}"
[InputListValues]="mediafilesObserver"
></os-search-value-selector>
<div *ngIf="newTopic">
<!-- Visibility -->
<div>
<mat-form-field>
<input
type="text"
matInput
osAutofocus
required
formControlName="title"
placeholder="{{ 'Title' | translate }}"
/>
<mat-error *ngIf="topicForm.invalid" translate>A name is required</mat-error>
<mat-select formControlName="agenda_type" placeholder="{{ 'Agenda visibility' | translate }}">
<mat-option *ngFor="let type of itemVisibility" [value]="type.key">
<span>{{ type.name | translate }}</span>
</mat-option>
</mat-select>
</mat-form-field>
</div>
<!-- The editor -->
<div class="spacer-bottom-20">
<h4 translate>Text</h4>
<editor formControlName="text" [init]="tinyMceSettings"></editor>
<!-- Parent item -->
<div>
<os-search-value-selector
ngDefaultControl
[form]="topicForm"
[formControl]="topicForm.get('agenda_parent_id')"
[multiple]="false"
[includeNone]="true"
listname="{{ 'Parent item' | translate }}"
[InputListValues]="itemObserver"
></os-search-value-selector>
</div>
<!-- Attachments -->
<os-search-value-selector
ngDefaultControl
[form]="topicForm"
[formControl]="topicForm.get('attachments_id')"
[multiple]="true"
listname="{{ 'Attachments' | translate }}"
[InputListValues]="mediafilesObserver"
></os-search-value-selector>
<div *ngIf="newTopic">
<!-- Visibility -->
<div>
<mat-form-field>
<mat-select formControlName="agenda_type" placeholder="{{ 'Agenda visibility' | translate }}">
<mat-option *ngFor="let type of itemVisibility" [value]="type.key">
<span>{{ type.name | translate }}</span>
</mat-option>
</mat-select>
</mat-form-field>
</div>
<!-- Parent item -->
<div>
<os-search-value-selector
ngDefaultControl
[form]="topicForm"
[formControl]="topicForm.get('agenda_parent_id')"
[multiple]="false"
[includeNone]="true"
listname="{{ 'Parent item' | translate }}"
[InputListValues]="agendaItemObserver"
></os-search-value-selector>
</div>
</div>
</form>
</mat-card>
</div>
</form>
</mat-card>
<mat-menu #topicExtraMenu="matMenu">
<button mat-menu-item *ngIf="topic" [routerLink]="getSpeakerLink()">

View File

@ -12,10 +12,13 @@ import { TopicRepositoryService } from 'app/core/repositories/agenda/topic-repos
import { ViewTopic } from '../../models/view-topic';
import { OperatorService } from 'app/core/core-services/operator.service';
import { BehaviorSubject } from 'rxjs';
import { DataStoreService } from 'app/core/core-services/data-store.service';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
import { Item, itemVisibilityChoices } from 'app/shared/models/agenda/item';
import { itemVisibilityChoices } from 'app/shared/models/agenda/item';
import { CreateTopic } from '../../models/create-topic';
import { Topic } from 'app/shared/models/topics/topic';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { ViewItem } from '../../models/view-item';
import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service';
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
/**
* Detail page for topics.
@ -49,12 +52,12 @@ export class TopicDetailComponent extends BaseViewComponent {
/**
* Subject for mediafiles
*/
public mediafilesObserver: BehaviorSubject<Mediafile[]>;
public mediafilesObserver: BehaviorSubject<ViewMediafile[]>;
/**
* Subject for agenda items
*/
public agendaItemObserver: BehaviorSubject<Item[]>;
public itemObserver: BehaviorSubject<ViewItem[]>;
/**
* Determine visibility states for the agenda that will be created implicitly
@ -85,22 +88,20 @@ export class TopicDetailComponent extends BaseViewComponent {
private repo: TopicRepositoryService,
private promptService: PromptService,
private operator: OperatorService,
private DS: DataStoreService
private mediafileRepo: MediafileRepositoryService,
private itemRepo: ItemRepositoryService
) {
super(title, translate, matSnackBar);
this.getTopicByUrl();
this.createForm();
this.mediafilesObserver = new BehaviorSubject(this.DS.getAll(Mediafile));
this.agendaItemObserver = new BehaviorSubject(this.DS.getAll(Item));
this.mediafilesObserver = new BehaviorSubject(this.mediafileRepo.getViewModelList());
this.itemObserver = new BehaviorSubject(this.itemRepo.getViewModelList());
this.DS.changeObservable.subscribe(newModel => {
if (newModel instanceof Item) {
this.agendaItemObserver.next(DS.getAll(Item));
} else if (newModel instanceof Mediafile) {
this.mediafilesObserver.next(DS.getAll(Mediafile));
}
});
this.mediafileRepo
.getViewModelListObservable()
.subscribe(mediafiles => this.mediafilesObserver.next(mediafiles));
this.itemRepo.getViewModelListObservable().subscribe(items => this.itemObserver.next(items));
}
/**
@ -170,7 +171,7 @@ export class TopicDetailComponent extends BaseViewComponent {
// creates a new topic
this.newTopic = true;
this.editTopic = true;
this.topic = new ViewTopic();
this.topic = new ViewTopic(new Topic());
} else {
// load existing topic
this.route.params.subscribe(params => {
@ -205,7 +206,7 @@ export class TopicDetailComponent extends BaseViewComponent {
*/
public getSpeakerLink(): string {
if (!this.newTopic && this.topic) {
const item = this.repo.getAgendaItem(this.topic.topic);
const item = this.topic.getAgendaItem();
if (item) {
return `/agenda/${item.id}/speakers`;
}

View File

@ -1,11 +1,11 @@
import { BaseViewModel } from '../../base/base-view-model';
import { Item } from 'app/shared/models/agenda/item';
import { AgendaBaseModel } from 'app/shared/models/base/agenda-base-model';
import { Speaker } from 'app/shared/models/agenda/speaker';
import { BaseAgendaViewModel } from 'app/site/base/base-agenda-view-model';
export class ViewItem extends BaseViewModel {
private _item: Item;
private _contentObject: AgendaBaseModel;
private _contentObject: BaseAgendaViewModel;
/**
* virtual weight defined by the order in the agenda tree, representing a shortcut to sorting by
@ -24,7 +24,7 @@ export class ViewItem extends BaseViewModel {
return this._item;
}
public get contentObject(): AgendaBaseModel {
public get contentObject(): BaseAgendaViewModel {
return this._contentObject;
}
@ -92,8 +92,8 @@ export class ViewItem extends BaseViewModel {
return this.item ? this.item.parent_id : null;
}
public constructor(item: Item, contentObject: AgendaBaseModel) {
super();
public constructor(item: Item, contentObject: BaseAgendaViewModel) {
super('Item');
this._item = item;
this._contentObject = contentObject;
}
@ -113,7 +113,7 @@ export class ViewItem extends BaseViewModel {
* @returns the agenda list title as string
*/
public getListTitle(): string {
const contentObject: AgendaBaseModel = this.contentObject;
const contentObject: BaseAgendaViewModel = this.contentObject;
const numberPrefix = this.itemNumber ? `${this.itemNumber} · ` : '';
if (contentObject) {
@ -123,9 +123,5 @@ export class ViewItem extends BaseViewModel {
}
}
public updateValues(update: Item): void {
if (this.id === update.id) {
this._item = update;
}
}
public updateDependencies(update: BaseViewModel): void {}
}

View File

@ -1,63 +1,62 @@
import { BaseViewModel } from 'app/site/base/base-view-model';
import { Speaker, SpeakerState } from 'app/shared/models/agenda/speaker';
import { User } from 'app/shared/models/users/user';
import { Selectable } from 'app/shared/components/selectable';
import { ViewUser } from 'app/site/users/models/view-user';
/**
* Provides "safe" access to a speaker with all it's components
*/
export class ViewSpeaker extends BaseViewModel implements Selectable {
export class ViewSpeaker extends BaseViewModel {
private _speaker: Speaker;
private _user: User;
private _user: ViewUser | null;
public get speaker(): Speaker {
return this._speaker;
}
public get user(): User {
public get user(): ViewUser {
return this._user;
}
public get id(): number {
return this.speaker ? this.speaker.id : null;
return this.speaker.id;
}
public get weight(): number {
return this.speaker ? this.speaker.weight : null;
return this.speaker.weight;
}
public get marked(): boolean {
return this.speaker ? this.speaker.marked : null;
return this.speaker.marked;
}
/**
* @returns an ISO datetime string or null
*/
public get begin_time(): string {
return this.speaker ? this.speaker.begin_time : null;
return this.speaker.begin_time;
}
/**
* @returns an ISO datetime string or null
*/
public get end_time(): string {
return this.speaker ? this.speaker.end_time : null;
return this.speaker.end_time;
}
public get state(): SpeakerState {
return this.speaker ? this.speaker.state : null;
return this.speaker.state;
}
public get name(): string {
return this.user.full_name;
return this.user ? this.user.full_name : '';
}
public get gender(): string {
return this.user.gender || '';
return this.user ? this.user.gender : '';
}
public constructor(speaker?: Speaker, user?: User) {
super();
public constructor(speaker: Speaker, user?: ViewUser) {
super('Speaker');
this._speaker = speaker;
this._user = user;
}
@ -70,9 +69,5 @@ export class ViewSpeaker extends BaseViewModel implements Selectable {
* Speaker is not a base model,
* @param update the incoming update
*/
public updateValues(update: Speaker): void {
if (this.id === update.id) {
this._speaker = update;
}
}
public updateDependencies(update: BaseViewModel): void {}
}

View File

@ -1,71 +1,112 @@
import { BaseViewModel } from '../../base/base-view-model';
import { Topic } from 'app/shared/models/topics/topic';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
import { Item } from 'app/shared/models/agenda/item';
import { BaseModel } from 'app/shared/models/base/base-model';
import { BaseAgendaViewModel } from 'app/site/base/base-agenda-view-model';
import { SearchRepresentation } from 'app/core/ui-services/search.service';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { ViewItem } from './view-item';
import { BaseViewModel } from 'app/site/base/base-view-model';
/**
* Provides "safe" access to topic with all it's components
* @ignore
*/
export class ViewTopic extends BaseViewModel {
export class ViewTopic extends BaseAgendaViewModel {
protected _topic: Topic;
private _attachments: Mediafile[];
private _agenda_item: Item;
private _attachments: ViewMediafile[];
private _agendaItem: ViewItem;
public get topic(): Topic {
return this._topic;
}
public get attachments(): Mediafile[] {
public get attachments(): ViewMediafile[] {
return this._attachments;
}
public get agenda_item(): Item {
return this._agenda_item;
public get agendaItem(): ViewItem {
return this._agendaItem;
}
public get id(): number {
return this.topic ? this.topic.id : null;
return this.topic.id;
}
public get agenda_item_id(): number {
return this.topic ? this.topic.agenda_item_id : null;
return this.topic.agenda_item_id;
}
public get attachments_id(): number[] {
return this.topic ? this.topic.attachments_id : null;
return this.topic.attachments_id;
}
public get title(): string {
return this.topic ? this.topic.title : null;
return this.topic.title;
}
public get text(): string {
return this.topic ? this.topic.text : null;
return this.topic.text;
}
public constructor(topic?: Topic, attachments?: Mediafile[], item?: Item) {
super();
public constructor(topic: Topic, attachments?: ViewMediafile[], item?: ViewItem) {
super('Topic');
this._topic = topic;
this._attachments = attachments;
this._agenda_item = item;
this._agendaItem = item;
}
public getTitle(): string {
return this.title;
}
public getAgendaItem(): ViewItem {
return this.agendaItem;
}
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;
}
public getSlide(): ProjectorElementBuildDeskriptor {
throw new Error('TODO');
}
public hasAttachments(): boolean {
return this.attachments && this.attachments.length > 0;
}
public updateValues(update: BaseModel): void {
if (update instanceof Mediafile) {
if (this.topic && this.attachments_id && this.attachments_id.includes(update.id)) {
const attachmentIndex = this.attachments.findIndex(mediafile => mediafile.id === update.id);
this.attachments[attachmentIndex] = update as Mediafile;
public updateDependencies(update: BaseViewModel): void {
if (update instanceof ViewMediafile && this.attachments_id.includes(update.id)) {
const attachmentIndex = this.attachments.findIndex(mediafile => mediafile.id === update.id);
if (attachmentIndex < 0) {
this.attachments.push(update);
} else {
this.attachments[attachmentIndex] = update;
}
}
if (update instanceof ViewItem && this.agenda_item_id === update.id) {
this._agendaItem = update;
}
}
}

View File

@ -4,7 +4,7 @@ import { BaseFilterListService, OsFilter, OsFilterOption } from 'app/core/ui-ser
import { Item, itemVisibilityChoices } from 'app/shared/models/agenda/item';
import { ViewItem } from '../models/view-item';
import { StorageService } from 'app/core/core-services/storage.service';
import { AgendaRepositoryService } from 'app/core/repositories/agenda/agenda-repository.service';
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
@Injectable({
providedIn: 'root'
@ -19,7 +19,7 @@ export class AgendaFilterListService extends BaseFilterListService<Item, ViewIte
* @param store
* @param repo
*/
public constructor(store: StorageService, repo: AgendaRepositoryService) {
public constructor(store: StorageService, repo: ItemRepositoryService) {
super(store, repo);
this.filterOptions = [
{

View File

@ -1,6 +1,7 @@
import { AppConfig } from '../../core/app-config';
import { Assignment } from '../../shared/models/assignments/assignment';
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
import { ViewAssignment } from './models/view-assignment';
export const AssignmentsAppConfig: AppConfig = {
name: 'assignments',
@ -8,6 +9,7 @@ export const AssignmentsAppConfig: AppConfig = {
{
collectionString: 'assignments/assignment',
model: Assignment,
viewModel: ViewAssignment,
searchOrder: 3,
repository: AssignmentRepositoryService
}

View File

@ -1,14 +1,17 @@
import { BaseViewModel } from '../../base/base-view-model';
import { Assignment } from 'app/shared/models/assignments/assignment';
import { Tag } from 'app/shared/models/core/tag';
import { User } from 'app/shared/models/users/user';
import { Item } from 'app/shared/models/agenda/item';
import { BaseAgendaViewModel } from 'app/site/base/base-agenda-view-model';
import { SearchRepresentation } from 'app/core/ui-services/search.service';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { ViewUser } from 'app/site/users/models/view-user';
import { ViewItem } from 'app/site/agenda/models/view-item';
import { ViewTag } from 'app/site/tags/models/view-tag';
import { BaseViewModel } from 'app/site/base/base-view-model';
export class ViewAssignment extends BaseViewModel {
export class ViewAssignment extends BaseAgendaViewModel {
private _assignment: Assignment;
private _relatedUser: User[];
private _agendaItem: Item;
private _tags: Tag[];
private _relatedUser: ViewUser[];
private _agendaItem: ViewItem;
private _tags: ViewTag[];
public get id(): number {
return this._assignment ? this._assignment.id : null;
@ -18,15 +21,15 @@ export class ViewAssignment extends BaseViewModel {
return this._assignment;
}
public get candidates(): User[] {
public get candidates(): ViewUser[] {
return this._relatedUser;
}
public get agendaItem(): Item {
public get agendaItem(): ViewItem {
return this._agendaItem;
}
public get tags(): Tag[] {
public get tags(): ViewTag[] {
return this._tags;
}
@ -41,17 +44,35 @@ export class ViewAssignment extends BaseViewModel {
return this.candidates ? this.candidates.length : 0;
}
public constructor(assignment: Assignment, relatedUser: User[], agendaItem?: Item, tags?: Tag[]) {
super();
public constructor(assignment: Assignment, relatedUser?: ViewUser[], agendaItem?: ViewItem, tags?: ViewTag[]) {
super('Election');
this._assignment = assignment;
this._relatedUser = relatedUser;
this._agendaItem = agendaItem;
this._tags = tags;
}
public updateValues(): void {}
public updateDependencies(update: BaseViewModel): void {
console.log('TODO: assignment updateDependencies');
}
public getAgendaItem(): ViewItem {
return this.agendaItem;
}
public getTitle(): string {
return this.assignment ? this.assignment.title : null;
return this.assignment.title;
}
public formatForSearch(): SearchRepresentation {
throw new Error('TODO');
}
public getDetailStateURL(): string {
return 'TODO';
}
public getSlide(): ProjectorElementBuildDeskriptor {
throw new Error('TODO');
}
}

View File

@ -1,4 +1,5 @@
import { DetailNavigable } from './detail-navigable';
import { DetailNavigable } from '../../shared/models/base/detail-navigable';
import { ViewItem } from '../agenda/models/view-item';
/**
* An Interface for all extra information needed for content objects of items.
@ -13,4 +14,14 @@ export interface AgendaInformation extends DetailNavigable {
* Should return the title for the list of speakers view.
*/
getAgendaTitleWithType(): string;
/**
* An (optional) descriptive text to be exported in the CSV.
*/
getCSVExportText(): string;
/**
* Get access to the agenda item
*/
getAgendaItem(): ViewItem;
}

View File

@ -1,22 +1,13 @@
import { AgendaInformation } from './agenda-information';
import { Searchable } from './searchable';
import { AgendaInformation } from 'app/site/base/agenda-information';
import { BaseProjectableViewModel } from './base-projectable-view-model';
import { SearchRepresentation } from 'app/core/ui-services/search.service';
import { BaseModel } from './base-model';
import { ViewItem } from '../agenda/models/view-item';
/**
* A base model for models, that can be content objects in the agenda. Provides title and navigation
* information for the agenda.
* Base view class for projectable models.
*/
export abstract class AgendaBaseModel extends BaseModel<AgendaBaseModel> implements AgendaInformation, Searchable {
/**
* A model that can be a content object of an agenda item.
* @param collectionString
* @param verboseName
* @param input
*/
protected constructor(collectionString: string, verboseName: string, input?: any) {
super(collectionString, verboseName, input);
}
export abstract class BaseAgendaViewModel extends BaseProjectableViewModel implements AgendaInformation {
public abstract getAgendaItem(): ViewItem;
/**
* @returns the agenda title
@ -41,14 +32,10 @@ export abstract class AgendaBaseModel extends BaseModel<AgendaBaseModel> impleme
return '';
}
public abstract getDetailStateURL(): string;
/**
* Should return a string representation of the object, so there can be searched for.
*/
public abstract formatForSearch(): SearchRepresentation;
/**
* Should return the URL to the detail view. Used for the agenda, that the
* user can navigate to the content object.
*/
public abstract getDetailStateURL(): string;
}

View File

@ -4,6 +4,10 @@ import { BaseViewModel } from './base-view-model';
/**
* Base view class for projectable models.
*/
export abstract class BaseProjectableModel extends BaseViewModel implements Projectable {
export abstract class BaseProjectableViewModel extends BaseViewModel implements Projectable {
public constructor(verboseName: string) {
super(verboseName);
}
public abstract getSlide(): ProjectorElementBuildDeskriptor;
}

View File

@ -1,6 +1,7 @@
import { Displayable } from '../../shared/models/base/displayable';
import { Displayable } from './displayable';
import { Identifiable } from '../../shared/models/base/identifiable';
import { Deserializable } from 'app/shared/models/base/deserializable';
export type ViewModelConstructor<T extends BaseViewModel> = new (...args: any[]) => T;
/**
* Base class for view models. alls view models should have titles.
@ -11,7 +12,32 @@ export abstract class BaseViewModel implements Displayable, Identifiable {
*/
public abstract id: number;
public abstract updateValues(update: Deserializable): void;
/**
* Children should also have a verbose name for generic display purposes
*/
protected _verboseName: string;
public constructor(verboseName: string) {
this._verboseName = verboseName;
}
/**
* 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;
}
}
public abstract updateDependencies(update: BaseViewModel): void;
public abstract getTitle(): string;

View File

@ -1,4 +1,4 @@
import { Displayable } from 'app/shared/models/base/displayable';
import { Displayable } from 'app/site/base/displayable';
import { IdentifiableProjectorElement } from 'app/shared/models/core/projector';
import { SlideOptions } from './slide-options';

View File

@ -1,5 +1,5 @@
import { DetailNavigable } from './detail-navigable';
import { SearchRepresentation } from 'app/core/ui-services/search.service';
import { DetailNavigable } from 'app/shared/models/base/detail-navigable';
/**
* Asserts, if the given object is searchable.

View File

@ -1,10 +1,18 @@
import { AppConfig } from '../../core/app-config';
import { ChatMessage } from '../../shared/models/core/chat-message';
import { ChatMessageRepositoryService } from 'app/core/repositories/common/chatmessage-repository.service';
import { ViewChatMessage } from './models/view-chatmessage';
export const CommonAppConfig: AppConfig = {
name: 'common',
models: [{ collectionString: 'core/chat-message', model: ChatMessage, repository: ChatMessageRepositoryService }],
models: [
{
collectionString: 'core/chat-message',
model: ChatMessage,
viewModel: ViewChatMessage,
repository: ChatMessageRepositoryService
}
],
mainMenuEntries: [
{
route: '/',

View File

@ -1,9 +1,10 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { BaseComponent } from 'app/base.component';
import { TranslateService } from '@ngx-translate/core'; // showcase
import { BaseComponent } from 'app/base.component';
// for testing the DS and BaseModel
import { Config } from 'app/shared/models/core/config';
import { DataStoreService } from 'app/core/core-services/data-store.service';

View File

@ -2,30 +2,28 @@ import { ChatMessage } from 'app/shared/models/core/chat-message';
import { BaseViewModel } from 'app/site/base/base-view-model';
export class ViewChatMessage extends BaseViewModel {
private _message: ChatMessage;
private _chatMessage: ChatMessage;
public get chatmessage(): ChatMessage {
return this._message ? this._message : null;
return this._chatMessage;
}
public get id(): number {
return this.chatmessage ? this.chatmessage.id : null;
return this.chatmessage.id;
}
public get message(): string {
return this.chatmessage ? this.chatmessage.message : null;
return this.chatmessage.message;
}
public constructor(message?: ChatMessage) {
super();
this._message = message;
super('Chatmessage');
this._chatMessage = message;
}
public getTitle(): string {
return 'Chatmessage';
}
public updateValues(message: ChatMessage): void {
console.log('Update message TODO with vals:', message);
}
public updateDependencies(message: BaseViewModel): void {}
}

View File

@ -1,15 +1,15 @@
import { Component, OnInit, Input, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
import { Title } from '@angular/platform-browser';
import { distinctUntilChanged } from 'rxjs/operators';
import { distinctUntilChanged } from 'rxjs/operators';
import { DateTimeAdapter } from 'ng-pick-datetime';
import { TranslateService } from '@ngx-translate/core';
import { BaseComponent } from '../../../../base.component';
import { ConfigRepositoryService } from '../../services/config-repository.service';
import { BaseComponent } from 'app/base.component';
import { ParentErrorStateMatcher } from 'app/shared/parent-error-state-matcher';
import { ViewConfig } from '../../models/view-config';
import { ConfigRepositoryService } from 'app/core/repositories/config/config-repository.service';
/**
* List view for the categories.

View File

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { ConfigRepositoryService, ConfigGroup } from '../../services/config-repository.service';
import { ConfigRepositoryService, ConfigGroup } from '../../../../core/repositories/config/config-repository.service';
import { BaseComponent } from '../../../../base.component';
/**

View File

@ -1,10 +1,13 @@
import { AppConfig } from '../../core/app-config';
import { Config } from '../../shared/models/core/config';
import { ConfigRepositoryService } from './services/config-repository.service';
import { ConfigRepositoryService } from '../../core/repositories/config/config-repository.service';
import { ViewConfig } from './models/view-config';
export const ConfigAppConfig: AppConfig = {
name: 'settings',
models: [{ collectionString: 'core/config', model: Config, repository: ConfigRepositoryService }],
models: [
{ collectionString: 'core/config', model: Config, viewModel: ViewConfig, repository: ConfigRepositoryService }
],
mainMenuEntries: [
{
route: '/settings',

View File

@ -94,7 +94,7 @@ export class ViewConfig extends BaseViewModel {
}
public constructor(config: Config) {
super();
super('Config');
this._config = config;
}
@ -102,9 +102,7 @@ export class ViewConfig extends BaseViewModel {
return this.label;
}
public updateValues(update: Config): void {
this._config = update;
}
public updateDependencies(update: BaseViewModel): void {}
/**
* Returns the time this config field needs to debounce before sending a request to the server.

View File

@ -2,16 +2,15 @@ import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material';
import { Router } from '@angular/router';
import { Subject } from 'rxjs';
import { Subject } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { ListViewBaseComponent } from 'app/site/base/list-view-base';
import { HistoryRepositoryService } from 'app/core/repositories/history/history-repository.service';
import { DataStoreService } from 'app/core/core-services/data-store.service';
import { isDetailNavigable } from 'app/shared/models/base/detail-navigable';
import { ViewHistory } from '../../models/view-history';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
/**
* A list view for the history.
@ -42,7 +41,7 @@ export class HistoryListComponent extends ListViewBaseComponent<ViewHistory> imp
translate: TranslateService,
matSnackBar: MatSnackBar,
private repo: HistoryRepositoryService,
private DS: DataStoreService,
private viewModelStore: ViewModelStoreService,
private router: Router
) {
super(titleService, translate, matSnackBar);
@ -89,7 +88,7 @@ export class HistoryListComponent extends ListViewBaseComponent<ViewHistory> imp
* @returns the title of an old element or null if it could not be found
*/
public getElementInfo(history: ViewHistory): string {
const oldElementTitle = this.repo.getOldModelInfo(history.getCollectionString(), history.getModelID());
const oldElementTitle = this.repo.getOldModelInfo(history.getCollectionString(), history.getModelId());
if (oldElementTitle) {
return oldElementTitle;
@ -104,22 +103,19 @@ export class HistoryListComponent extends ListViewBaseComponent<ViewHistory> imp
*
* @param history Represents the selected element
*/
public onClickRow(history: ViewHistory): void {
this.repo.browseHistory(history).then(() => {
const element = this.DS.get(history.getCollectionString(), history.getModelID());
let message = this.translate.instant('Temporarily reset OpenSlides to the state from ');
message += history.getLocaleString('DE-de') + '. ';
public async onClickRow(history: ViewHistory): Promise<void> {
await this.repo.browseHistory(history);
const element = this.viewModelStore.get(history.getCollectionString(), history.getModelId());
let message = this.translate.instant('Temporarily reset OpenSlides to the state from ');
message += history.getLocaleString('DE-de') + '. ';
if (isDetailNavigable(element)) {
message += this.translate.instant(
'You will be redirected to the detail state of the last changed item.'
);
this.raiseError(message);
this.router.navigate([element.getDetailStateURL()]);
} else {
this.raiseError(message);
}
});
if (isDetailNavigable(element)) {
message += this.translate.instant('You will be redirected to the detail state of the last changed item.');
this.raiseError(message);
this.router.navigate([element.getDetailStateURL()]);
} else {
this.raiseError(message);
}
}
/**

View File

@ -1,6 +1,7 @@
import { AppConfig } from '../../core/app-config';
import { History } from 'app/shared/models/core/history';
import { HistoryRepositoryService } from 'app/core/repositories/history/history-repository.service';
import { ViewHistory } from './models/view-history';
/**
* Config object for history.
@ -8,7 +9,14 @@ import { HistoryRepositoryService } from 'app/core/repositories/history/history-
*/
export const HistoryAppConfig: AppConfig = {
name: 'history',
models: [{ collectionString: 'core/history', model: History, repository: HistoryRepositoryService }],
models: [
{
collectionString: 'core/history',
model: History,
viewModel: ViewHistory,
repository: HistoryRepositoryService
}
],
mainMenuEntries: [
{
route: '/history',

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