document, restructure, add relations
- models get other models from DataStore (Relations) - documentation using Compodoc - rename and restructure - http-interceptor makes all http-objections obsolete - created 'Deserializable model' interface for better mapping of JSON objects - Supports multiple nested objects - No foreign dependancies, no magic - Simple yet efficient deserialize function - arrays of nested objects - created more classes for better OOP AOP
This commit is contained in:
parent
30ac9c8e36
commit
6b09427565
1
.gitignore
vendored
1
.gitignore
vendored
@ -43,6 +43,7 @@ openslides_*
|
||||
client/dist
|
||||
client/tmp
|
||||
client/out-tsc
|
||||
client/documentation
|
||||
|
||||
# dependencies
|
||||
client/node_modules
|
||||
|
@ -1,12 +1,46 @@
|
||||
# OpenSlides 3 Client
|
||||
|
||||
Prototype application for OpenSlides 3.0 (Client)
|
||||
Prototype application for OpenSlides 3.0 (Client).
|
||||
Currently under constant heavy maintenance.
|
||||
|
||||
## Development server
|
||||
## Development Info
|
||||
|
||||
Run `npm start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
As an Angular project, Angular CLI is highly recommended to create components and services.
|
||||
See https://angular.io/guide/quickstart for details.
|
||||
|
||||
A running OpenSlides (2.2) instance is expected on port 8000.
|
||||
### Contribution Info
|
||||
|
||||
Please respect the code-style defined in `.editorconf` and `.pretierrc`.
|
||||
|
||||
Code alignment should be automatically corrected by the pre-commit hooks.
|
||||
Adjust your editor to the `.editorconfig` to avoid surprises.
|
||||
See https://editorconfig.org/ for details.
|
||||
|
||||
### Pre-Commit Hooks
|
||||
|
||||
Before commiting, new code will automatically be aligned to the definitions set in the
|
||||
`.prettierrc`.
|
||||
Furthermore, new code has to pass linting.
|
||||
|
||||
Our pre-commit hooks are:
|
||||
`pretty-quick --staged` and `lint`
|
||||
See `package.json` for details.
|
||||
|
||||
### Documentation Info
|
||||
|
||||
The documentation can be generated by running `npm run compodoc`.
|
||||
A new web server will be started on http://localhost:8080
|
||||
Once running, the documentation will be updated automatically.
|
||||
|
||||
Please document new code using JSDoc tags.
|
||||
See https://compodoc.app/guides/jsdoc-tags.html for details.
|
||||
|
||||
### Development server
|
||||
|
||||
Run `npm start` for a development server. Navigate to `http://localhost:4200/`.
|
||||
The app will automatically reload if you change any of the source files.
|
||||
|
||||
A running OpenSlides (2.2 or higher) instance is expected on port 8000.
|
||||
|
||||
Start OpenSlides as usual using
|
||||
`python manage.py start --no-browser --host 0.0.0.0`
|
||||
|
949
client/package-lock.json
generated
949
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -3,11 +3,12 @@
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --proxy-config proxy.conf.json",
|
||||
"start": "ng serve --proxy-config proxy.conf.json --host=0.0.0.0",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e",
|
||||
"compodoc": "./node_modules/.bin/compodoc -p src/tsconfig.app.json -s -w",
|
||||
"extract": "ngx-translate-extract -i ./src -o ./src/assets/i18n/{en,de,fr}.json --clean --sort --format-indentation ' ' --format namespaced-json",
|
||||
"format:fix": "pretty-quick --staged",
|
||||
"precommit": "run-s format:fix lint"
|
||||
@ -40,6 +41,7 @@
|
||||
"@angular/compiler-cli": "^6.0.6",
|
||||
"@angular/language-service": "^6.0.6",
|
||||
"@biesbjerg/ngx-translate-extract": "^2.3.4",
|
||||
"@compodoc/compodoc": "^1.1.3",
|
||||
"@types/jasmine": "~2.8.6",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/node": "~8.9.4",
|
||||
|
@ -1,16 +1,23 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { AutoupdateService } from 'app/core/services/autoupdate.service';
|
||||
// import { AuthService } from 'app/core/services/auth.service';
|
||||
import { OperatorService } from 'app/core/services/operator.service';
|
||||
|
||||
/**
|
||||
* Angular's global App Component
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
// export class AppComponent implements OnInit {
|
||||
export class AppComponent {
|
||||
/**
|
||||
* Initialises the operator, the auto update (and therefore a websocket) feature and the translation unit.
|
||||
* @param operator
|
||||
* @param autoupdate
|
||||
* @param translate
|
||||
*/
|
||||
constructor(
|
||||
private operator: OperatorService,
|
||||
private autoupdate: AutoupdateService,
|
||||
|
@ -3,7 +3,7 @@ import { BrowserModule, Title } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClientModule, HttpClient, HttpClientXsrfModule } from '@angular/common/http';
|
||||
import { HttpClientModule, HttpClient, HttpClientXsrfModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
|
||||
// MaterialUI modules
|
||||
import {
|
||||
@ -34,15 +34,26 @@ import { MotionsComponent } from './site/motions/motions.component';
|
||||
import { AgendaComponent } from './site/agenda/agenda.component';
|
||||
import { SiteComponent } from './site/site.component';
|
||||
import { StartComponent } from './site/start/start.component';
|
||||
import { WebsocketService } from './core/services/websocket.service';
|
||||
import { AddHeaderInterceptor } from './core/http-interceptor';
|
||||
import { ProjectorContainerComponent } from './projector-container/projector-container.component';
|
||||
import { AlertComponent } from './core/directives/alert/alert.component';
|
||||
|
||||
//translation module. TODO: Potetially a SharedModule and own files
|
||||
// Root Services
|
||||
import { AuthGuard } from './core/services/auth-guard.service';
|
||||
import { AuthService } from './core/services/auth.service';
|
||||
import { AutoupdateService } from './core/services/autoupdate.service';
|
||||
import { DataStoreService } from './core/services/dataStore.service';
|
||||
import { OperatorService } from './core/services/operator.service';
|
||||
import { WebsocketService } from './core/services/websocket.service';
|
||||
|
||||
// translation module.
|
||||
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
||||
import { PruningTranslationLoader } from './core/pruning-loader';
|
||||
import { OsPermsDirective } from './core/directives/os-perms.directive';
|
||||
|
||||
/**
|
||||
* For the translation module. Loads a Custom 'translation loader' and provides it as loader.
|
||||
* @param http Just the HttpClient to load stuff
|
||||
*/
|
||||
export function HttpLoaderFactory(http: HttpClient) {
|
||||
return new PruningTranslationLoader(http);
|
||||
}
|
||||
@ -61,7 +72,6 @@ library.add(fas);
|
||||
SiteComponent,
|
||||
StartComponent,
|
||||
ProjectorContainerComponent,
|
||||
AlertComponent,
|
||||
OsPermsDirective
|
||||
],
|
||||
imports: [
|
||||
@ -94,7 +104,20 @@ library.add(fas);
|
||||
}),
|
||||
AppRoutingModule
|
||||
],
|
||||
providers: [Title, WebsocketService],
|
||||
providers: [
|
||||
Title,
|
||||
AuthGuard,
|
||||
AuthService,
|
||||
AutoupdateService,
|
||||
DataStoreService,
|
||||
OperatorService,
|
||||
WebsocketService,
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: AddHeaderInterceptor,
|
||||
multi: true
|
||||
}
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule {}
|
||||
|
@ -1,28 +1,35 @@
|
||||
import { Injector } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { DataStoreService } from 'app/core/services/DS.service';
|
||||
import { OpenSlidesComponent } from './openslides.component';
|
||||
|
||||
// provides functions that might be used by a lot of components
|
||||
export abstract class BaseComponent {
|
||||
protected injector: Injector;
|
||||
protected dataStore: DataStoreService;
|
||||
/**
|
||||
* Provides functionalities that will be used by most components
|
||||
* currently able to set the title with the suffix ' - OpenSlides 3'
|
||||
*
|
||||
* A BaseComponent is an OpenSlides Component.
|
||||
* Components in the 'Side'- or 'projector' Folder are BaseComponents
|
||||
*/
|
||||
export abstract class BaseComponent extends OpenSlidesComponent {
|
||||
/**
|
||||
* To manipulate the browser title bar, adds the Suffix "OpenSlides 3"
|
||||
*
|
||||
* Might be a config variable later at some point
|
||||
*/
|
||||
private titleSuffix = ' - OpenSlides 3';
|
||||
|
||||
/**
|
||||
* Child constructor that implements the titleServices and calls Super from OpenSlidesComponent
|
||||
*/
|
||||
constructor(protected titleService?: Title) {
|
||||
// throws a warning even tho it is the new syntax. Ignored for now.
|
||||
this.injector = Injector.create([{ provide: DataStoreService, useClass: DataStoreService, deps: [] }]);
|
||||
super();
|
||||
}
|
||||
|
||||
setTitle(prefix: string) {
|
||||
/**
|
||||
* Set the title in web browser using angulars TitleService
|
||||
* @param prefix The title prefix. Should be translated here.
|
||||
* TODO Might translate the prefix here?
|
||||
*/
|
||||
setTitle(prefix: string): void {
|
||||
this.titleService.setTitle(prefix + this.titleSuffix);
|
||||
}
|
||||
|
||||
// static injection of DataStore (ds) in all child instancces of BaseComponent
|
||||
// use this.DS[...]
|
||||
get DS(): DataStoreService {
|
||||
if (this.dataStore == null) {
|
||||
this.dataStore = this.injector.get(DataStoreService);
|
||||
}
|
||||
return this.dataStore;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +0,0 @@
|
||||
<div *ngIf="alert" [ngClass]="cssClass(alert)" class="alert-dismissable">
|
||||
{{alert.message}}
|
||||
<a class="close" (click)="removeAlert(alert)">×</a>
|
||||
</div>
|
@ -1,24 +0,0 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AlertComponent } from './alert.component';
|
||||
|
||||
describe('AlertComponent', () => {
|
||||
let component: AlertComponent;
|
||||
let fixture: ComponentFixture<AlertComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AlertComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AlertComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,38 +0,0 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
|
||||
import { Alert, AlertType } from 'app/core/models/alert';
|
||||
|
||||
/**TODO Drafted for now. Since the UI is not done yet, this might be replaced or disappear entirely.
|
||||
* Furtermore, Material UI does not support these kinds of alerts
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'app-alert',
|
||||
templateUrl: './alert.component.html',
|
||||
styleUrls: ['./alert.component.css']
|
||||
})
|
||||
export class AlertComponent implements OnInit {
|
||||
@Input() alert: Alert;
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
removeAlert(alert: Alert) {
|
||||
this.alert = undefined;
|
||||
}
|
||||
|
||||
cssClass(alert: Alert) {
|
||||
// return css class based on alert type
|
||||
switch (alert.type) {
|
||||
case AlertType.Success:
|
||||
return 'alert alert-success';
|
||||
case AlertType.Error:
|
||||
return 'alert alert-danger';
|
||||
case AlertType.Info:
|
||||
return 'alert alert-info';
|
||||
case AlertType.Warning:
|
||||
return 'alert alert-warning';
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +1,40 @@
|
||||
import { Directive, Input, ElementRef, TemplateRef, ViewContainerRef, OnInit } from '@angular/core';
|
||||
|
||||
import { OperatorService } from 'app/core/services/operator.service';
|
||||
import { BaseComponent } from 'app/base.component';
|
||||
import { OpenSlidesComponent } from '../../openslides.component';
|
||||
import { Group } from 'app/core/models/users/group';
|
||||
|
||||
/**
|
||||
* Directive to check if the {@link OperatorService} has the correct permissions to access certain functions
|
||||
*
|
||||
* Successor of os-perms in OpenSlides 2.2
|
||||
* @example <div *appOsPerms=".." ..> ... < /div>
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[appOsPerms]'
|
||||
})
|
||||
export class OsPermsDirective extends BaseComponent {
|
||||
export class OsPermsDirective extends OpenSlidesComponent {
|
||||
/**
|
||||
* Holds the {@link OperatorService} permissions
|
||||
*/
|
||||
private userPermissions: string[];
|
||||
|
||||
/**
|
||||
* Holds the required permissions the access a feature
|
||||
*/
|
||||
private permissions;
|
||||
|
||||
/**
|
||||
* Constructs the direcctive once. Observes the operator for it's groups so the directvice can perform changes
|
||||
* dynamically
|
||||
*
|
||||
* @param template inner part of the HTML container
|
||||
* @param viewContainer outer part of the HTML container (for example a `<div>`)
|
||||
* @param operator OperatorService
|
||||
*/
|
||||
constructor(
|
||||
private element: ElementRef,
|
||||
private template: TemplateRef<any>,
|
||||
private viewContainer: ViewContainerRef, //TODO private operator. OperatorService
|
||||
private viewContainer: ViewContainerRef,
|
||||
private operator: OperatorService
|
||||
) {
|
||||
super();
|
||||
@ -30,6 +51,10 @@ export class OsPermsDirective extends BaseComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Comes directly from the view.
|
||||
* The value defines the requires permissions.
|
||||
*/
|
||||
@Input()
|
||||
set appOsPerms(value) {
|
||||
this.permissions = value;
|
||||
@ -37,6 +62,10 @@ export class OsPermsDirective extends BaseComponent {
|
||||
this.updateView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the local `userPermissions[]` by the permissions found in the operators groups
|
||||
* Will just set, but not remove them.
|
||||
*/
|
||||
private readUserPermissions(): void {
|
||||
const opGroups = this.operator.getGroups();
|
||||
console.log('operator Groups: ', opGroups);
|
||||
@ -45,7 +74,9 @@ export class OsPermsDirective extends BaseComponent {
|
||||
});
|
||||
}
|
||||
|
||||
// hides or shows a contrainer
|
||||
/**
|
||||
* Shows or hides certain content in the view.
|
||||
*/
|
||||
private updateView(): void {
|
||||
if (this.checkPermissions()) {
|
||||
// will just render the page normally
|
||||
@ -56,7 +87,10 @@ export class OsPermsDirective extends BaseComponent {
|
||||
}
|
||||
}
|
||||
|
||||
// checks for the required permission
|
||||
/**
|
||||
* Compare the required permissions with the users permissions.
|
||||
* Returns true if the users permissions fit.
|
||||
*/
|
||||
private checkPermissions(): boolean {
|
||||
let isPermitted = false;
|
||||
if (this.userPermissions && this.permissions) {
|
||||
|
@ -1,4 +0,0 @@
|
||||
<div *ngFor="let toast of toasts" class="{{ cssClass(toast) }} alert-dismissable">
|
||||
{{toast.message}}
|
||||
<a class="close" (click)="removeAlert(toast)">×</a>
|
||||
</div>
|
@ -1,24 +0,0 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ToastComponent } from './toast.component';
|
||||
|
||||
describe('ToastComponent', () => {
|
||||
let component: ToastComponent;
|
||||
let fixture: ComponentFixture<ToastComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ToastComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ToastComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,54 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { Alert, AlertType } from 'app/core/models/alert';
|
||||
import { ToastService } from 'app/core/services/toast.service';
|
||||
|
||||
/**TODO Drafted for now. Since the UI is not done yet, this might be replaced or disappear entirely.
|
||||
* Furtermore, Material UI does not support these kinds of alerts
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'app-toast',
|
||||
templateUrl: './toast.component.html',
|
||||
styleUrls: ['./toast.component.css']
|
||||
})
|
||||
export class ToastComponent implements OnInit {
|
||||
toasts: Alert[] = [];
|
||||
|
||||
constructor(private toastService: ToastService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.toastService.getToast().subscribe((alert: Alert) => {
|
||||
if (!alert) {
|
||||
// clear alerts when an empty alert is received
|
||||
this.toasts = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// add alert to array
|
||||
this.toasts.push(alert);
|
||||
});
|
||||
}
|
||||
|
||||
removeAlert(alert: Alert) {
|
||||
this.toasts = this.toasts.filter(x => x !== alert);
|
||||
}
|
||||
|
||||
cssClass(alert: Alert) {
|
||||
if (!alert) {
|
||||
return;
|
||||
}
|
||||
|
||||
// return css class based on alert type
|
||||
switch (alert.type) {
|
||||
case AlertType.Success:
|
||||
return 'alert alert-success';
|
||||
case AlertType.Error:
|
||||
return 'alert alert-danger';
|
||||
case AlertType.Info:
|
||||
return 'alert alert-info';
|
||||
case AlertType.Warning:
|
||||
return 'alert alert-warning';
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,11 @@
|
||||
/**
|
||||
* custom exception that indicated that a collectionString is invalid.
|
||||
*/
|
||||
export class ImproperlyConfiguredError extends Error {
|
||||
/**
|
||||
* Default Constructor for Errors
|
||||
* @param m The Error Message
|
||||
*/
|
||||
constructor(m: string) {
|
||||
super(m);
|
||||
}
|
||||
|
24
client/src/app/core/http-interceptor.ts
Normal file
24
client/src/app/core/http-interceptor.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Interceptor class for HTTP requests. Replaces all 'httpOptions' in all http.get or http.post requests.
|
||||
*
|
||||
* Should not need further adjustment.
|
||||
*/
|
||||
export class AddHeaderInterceptor implements HttpInterceptor {
|
||||
/**
|
||||
* Normal HttpInterceptor usage
|
||||
*
|
||||
* @param req Will clone the request and intercept it with our desired headers
|
||||
* @param next HttpHandler will catch the response and forwards it to the original instance
|
||||
*/
|
||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
const clonedRequest = req.clone({
|
||||
withCredentials: true,
|
||||
headers: req.headers.set('Content-Type', 'application/json')
|
||||
});
|
||||
|
||||
return next.handle(clonedRequest);
|
||||
}
|
||||
}
|
29
client/src/app/core/models/agenda/content-object.ts
Normal file
29
client/src/app/core/models/agenda/content-object.ts
Normal file
@ -0,0 +1,29 @@
|
||||
// import { Serializable } from 'app/core/models/serializable';
|
||||
import { Deserializable } from 'app/core/models/deserializable.model';
|
||||
|
||||
/**
|
||||
* Representation of the content object in agenda item
|
||||
* @ignore
|
||||
*/
|
||||
export class ContentObject implements Deserializable {
|
||||
/**
|
||||
* Is the same with dataStores collectionString
|
||||
*/
|
||||
collection: string;
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* Needs to be completely optional because agenda has (yet) the optional parameter 'speaker'
|
||||
* @param collection
|
||||
* @param id
|
||||
*/
|
||||
constructor(collection?: string, id?: number) {
|
||||
this.collection = collection;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
deserialize(input: any): this {
|
||||
Object.assign(this, input);
|
||||
return this;
|
||||
}
|
||||
}
|
@ -1,54 +1,84 @@
|
||||
import { BaseModel } from 'app/core/models/baseModel';
|
||||
import { BaseModel } from 'app/core/models/base-model';
|
||||
import { Speaker } from './speaker';
|
||||
import { ContentObject } from './content-object';
|
||||
|
||||
/**
|
||||
* Representations of agenda Item
|
||||
* @ignore
|
||||
*/
|
||||
export class Item extends BaseModel {
|
||||
static collectionString = 'agenda/item';
|
||||
protected _collectionString: string;
|
||||
id: number;
|
||||
closed: boolean;
|
||||
comment: string;
|
||||
content_object: Object;
|
||||
duration: number; //time?
|
||||
is_hidden: boolean;
|
||||
item_number: string;
|
||||
list_view_title: string;
|
||||
parent_id: number;
|
||||
speaker_list_closed: boolean;
|
||||
speakers: BaseModel[]; //we should not know users just yet
|
||||
title: string;
|
||||
list_view_title: string;
|
||||
comment: string;
|
||||
closed: boolean;
|
||||
type: number;
|
||||
is_hidden: boolean;
|
||||
duration: number;
|
||||
speakers: Speaker[];
|
||||
speaker_list_closed: boolean;
|
||||
content_object: ContentObject;
|
||||
weight: number;
|
||||
parent_id: number;
|
||||
|
||||
constructor(
|
||||
id: number,
|
||||
closed?: boolean,
|
||||
comment?: string,
|
||||
content_object?: Object,
|
||||
duration?: number,
|
||||
is_hidden?: boolean,
|
||||
id?: number,
|
||||
item_number?: string,
|
||||
list_view_title?: string,
|
||||
parent_id?: number,
|
||||
speaker_list_closed?: boolean,
|
||||
speakers?: BaseModel[],
|
||||
title?: string,
|
||||
list_view_title?: string,
|
||||
comment?: string,
|
||||
closed?: boolean,
|
||||
type?: number,
|
||||
weight?: number
|
||||
is_hidden?: boolean,
|
||||
duration?: number,
|
||||
speakers?: Speaker[],
|
||||
speaker_list_closed?: boolean,
|
||||
content_object?: ContentObject,
|
||||
weight?: number,
|
||||
parent_id?: number
|
||||
) {
|
||||
super(id);
|
||||
this.comment = comment;
|
||||
this.content_object = content_object;
|
||||
this.duration = duration;
|
||||
this.is_hidden = is_hidden;
|
||||
super();
|
||||
this._collectionString = 'agenda/item';
|
||||
this.id = id;
|
||||
this.item_number = item_number;
|
||||
this.list_view_title = list_view_title;
|
||||
this.parent_id = parent_id;
|
||||
this.speaker_list_closed = speaker_list_closed;
|
||||
this.speakers = speakers;
|
||||
this.title = title;
|
||||
this.list_view_title = list_view_title;
|
||||
this.comment = comment;
|
||||
this.closed = closed;
|
||||
this.type = type;
|
||||
this.is_hidden = is_hidden;
|
||||
this.duration = duration;
|
||||
this.speakers = speakers;
|
||||
this.speaker_list_closed = speaker_list_closed;
|
||||
this.content_object = content_object;
|
||||
this.weight = weight;
|
||||
this.parent_id = parent_id;
|
||||
}
|
||||
|
||||
public getCollectionString(): string {
|
||||
return Item.collectionString;
|
||||
getSpeakersAsUser(): BaseModel | BaseModel[] {
|
||||
const speakerIds = [];
|
||||
this.speakers.forEach(speaker => {
|
||||
speakerIds.push(speaker.user_id);
|
||||
});
|
||||
return this.DS.get('users/user', ...speakerIds);
|
||||
}
|
||||
|
||||
getContentObject(): BaseModel | BaseModel[] {
|
||||
return this.DS.get(this.content_object.collection, this.content_object.id);
|
||||
}
|
||||
|
||||
deserialize(input: any): this {
|
||||
Object.assign(this, input);
|
||||
this.content_object = new ContentObject().deserialize(input.content_object);
|
||||
|
||||
if (input.speakers instanceof Array) {
|
||||
this.speakers = [];
|
||||
input.speakers.forEach(speakerData => {
|
||||
this.speakers.push(new Speaker().deserialize(speakerData));
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
50
client/src/app/core/models/agenda/speaker.ts
Normal file
50
client/src/app/core/models/agenda/speaker.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { Deserializable } from 'app/core/models/deserializable.model';
|
||||
|
||||
/**
|
||||
* Representation of a speaker in an agenda item
|
||||
*
|
||||
* Part of the 'speakers' list.
|
||||
* @ignore
|
||||
*/
|
||||
export class Speaker implements Deserializable {
|
||||
id: number;
|
||||
user_id: number;
|
||||
begin_time: string; //TODO this is a time object
|
||||
end_time: string; // TODO this is a time object
|
||||
weight: number;
|
||||
marked: boolean;
|
||||
item_id: number;
|
||||
|
||||
/**
|
||||
* Needs to be completely optional because agenda has (yet) the optional parameter 'speaker'
|
||||
* @param id
|
||||
* @param user_id
|
||||
* @param begin_time
|
||||
* @param end_time
|
||||
* @param weight
|
||||
* @param marked
|
||||
* @param item_id
|
||||
*/
|
||||
constructor(
|
||||
id?: number,
|
||||
user_id?: number,
|
||||
begin_time?: string,
|
||||
end_time?: string,
|
||||
weight?: number,
|
||||
marked?: boolean,
|
||||
item_id?: number
|
||||
) {
|
||||
this.id = id;
|
||||
this.user_id = user_id;
|
||||
this.begin_time = begin_time;
|
||||
this.end_time = end_time;
|
||||
this.weight = weight;
|
||||
this.marked = marked;
|
||||
this.item_id = item_id;
|
||||
}
|
||||
|
||||
deserialize(input: any): this {
|
||||
Object.assign(this, input);
|
||||
return this;
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
export class Alert {
|
||||
type: AlertType;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export enum AlertType {
|
||||
Success,
|
||||
Error,
|
||||
Info,
|
||||
Warning
|
||||
}
|
34
client/src/app/core/models/assignments/assignment-user.ts
Normal file
34
client/src/app/core/models/assignments/assignment-user.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { Deserializable } from '../deserializable.model';
|
||||
|
||||
/**
|
||||
* Content of the 'assignment_related_users' property
|
||||
* @ignore
|
||||
*/
|
||||
export class AssignmentUser implements Deserializable {
|
||||
id: number;
|
||||
user_id: number;
|
||||
elected: boolean;
|
||||
assignment_id: number;
|
||||
weight: number;
|
||||
|
||||
/**
|
||||
* Needs to be completely optional because assignment has (yet) the optional parameter 'assignment_related_users'
|
||||
* @param id
|
||||
* @param user_id
|
||||
* @param elected
|
||||
* @param assignment_id
|
||||
* @param weight
|
||||
*/
|
||||
constructor(id?: number, user_id?: number, elected?: boolean, assignment_id?: number, weight?: number) {
|
||||
this.id = id;
|
||||
this.user_id = user_id;
|
||||
this.elected = elected;
|
||||
this.assignment_id = assignment_id;
|
||||
this.weight = weight;
|
||||
}
|
||||
|
||||
deserialize(input: any): this {
|
||||
Object.assign(this, input);
|
||||
return this;
|
||||
}
|
||||
}
|
@ -1,41 +1,78 @@
|
||||
import { BaseModel } from 'app/core/models/baseModel';
|
||||
import { BaseModel } from '../base-model';
|
||||
import { AssignmentUser } from './assignment-user';
|
||||
import { Poll } from './poll';
|
||||
|
||||
/**
|
||||
* Representation of an assignment.
|
||||
* @ignore
|
||||
*/
|
||||
export class Assignment extends BaseModel {
|
||||
static collectionString = 'assignments/assignment';
|
||||
protected _collectionString: string;
|
||||
id: number;
|
||||
agenda_item_id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
open_posts: number;
|
||||
phase: number;
|
||||
assignment_related_users: AssignmentUser[];
|
||||
poll_description_default: number;
|
||||
polls: Object[];
|
||||
polls: Poll[];
|
||||
agenda_item_id: number;
|
||||
tags_id: number[];
|
||||
title: string;
|
||||
|
||||
constructor(
|
||||
id: number,
|
||||
agenda_item_id?: number,
|
||||
id?: number,
|
||||
title?: string,
|
||||
description?: string,
|
||||
open_posts?: number,
|
||||
phase?: number,
|
||||
assignment_related_users?: AssignmentUser[],
|
||||
poll_description_default?: number,
|
||||
polls?: Object[],
|
||||
tags_id?: number[],
|
||||
title?: string
|
||||
polls?: Poll[],
|
||||
agenda_item_id?: number,
|
||||
tags_id?: number[]
|
||||
) {
|
||||
super(id);
|
||||
super();
|
||||
this._collectionString = 'assignments/assignment';
|
||||
this.id = id;
|
||||
this.agenda_item_id = agenda_item_id;
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.open_posts = open_posts;
|
||||
this.phase = phase;
|
||||
this.assignment_related_users = assignment_related_users || []; //TODO Array
|
||||
this.poll_description_default = poll_description_default;
|
||||
this.polls = polls;
|
||||
this.polls = polls || Array(); // TODO Array
|
||||
this.agenda_item_id = agenda_item_id;
|
||||
this.tags_id = tags_id;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public getCollectionString(): string {
|
||||
return Assignment.collectionString;
|
||||
getAssignmentReleatedUsers(): BaseModel | BaseModel[] {
|
||||
const userIds = [];
|
||||
this.assignment_related_users.forEach(user => {
|
||||
userIds.push(user.user_id);
|
||||
});
|
||||
return this.DS.get('users/user', ...userIds);
|
||||
}
|
||||
|
||||
getTags(): BaseModel | BaseModel[] {
|
||||
return this.DS.get('core/tag', ...this.tags_id);
|
||||
}
|
||||
|
||||
deserialize(input: any): this {
|
||||
Object.assign(this, input);
|
||||
|
||||
if (input.assignment_related_users instanceof Array) {
|
||||
this.assignment_related_users = [];
|
||||
input.assignment_related_users.forEach(assignmentUserData => {
|
||||
this.assignment_related_users.push(new AssignmentUser().deserialize(assignmentUserData));
|
||||
});
|
||||
}
|
||||
|
||||
if (input.polls instanceof Array) {
|
||||
this.polls = [];
|
||||
input.polls.forEach(pollData => {
|
||||
this.polls.push(new Poll().deserialize(pollData));
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
46
client/src/app/core/models/assignments/poll-option.ts
Normal file
46
client/src/app/core/models/assignments/poll-option.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Deserializable } from '../deserializable.model';
|
||||
|
||||
/**
|
||||
* Representation of a poll option
|
||||
*
|
||||
* part of the 'polls-options'-array in poll
|
||||
* @ignore
|
||||
*/
|
||||
export class PollOption implements Deserializable {
|
||||
id: number;
|
||||
candidate_id: number;
|
||||
is_elected: boolean;
|
||||
votes: number[];
|
||||
poll_id: number;
|
||||
weight: number;
|
||||
|
||||
/**
|
||||
* Needs to be completely optional because poll has (yet) the optional parameter 'poll-options'
|
||||
* @param id
|
||||
* @param candidate_id
|
||||
* @param is_elected
|
||||
* @param votes
|
||||
* @param poll_id
|
||||
* @param weight
|
||||
*/
|
||||
constructor(
|
||||
id?: number,
|
||||
candidate_id?: number,
|
||||
is_elected?: boolean,
|
||||
votes?: number[],
|
||||
poll_id?: number,
|
||||
weight?: number
|
||||
) {
|
||||
this.id = id;
|
||||
this.candidate_id = candidate_id;
|
||||
this.is_elected = is_elected;
|
||||
this.votes = votes;
|
||||
this.poll_id = poll_id;
|
||||
this.weight = weight;
|
||||
}
|
||||
|
||||
deserialize(input: any): this {
|
||||
Object.assign(this, input);
|
||||
return this;
|
||||
}
|
||||
}
|
68
client/src/app/core/models/assignments/poll.ts
Normal file
68
client/src/app/core/models/assignments/poll.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { PollOption } from './poll-option';
|
||||
import { Deserializable } from '../deserializable.model';
|
||||
|
||||
/**
|
||||
* Content of the 'polls' property of assignments
|
||||
* @ignore
|
||||
*/
|
||||
export class Poll implements Deserializable {
|
||||
id: number;
|
||||
pollmethod: string;
|
||||
description: string;
|
||||
published: boolean;
|
||||
options: PollOption[];
|
||||
votesvalid: number;
|
||||
votesinvalid: number;
|
||||
votescast: number;
|
||||
has_votes: boolean;
|
||||
assignment_id: number;
|
||||
|
||||
/**
|
||||
* Needs to be completely optional because assignment has (yet) the optional parameter 'polls'
|
||||
* @param id
|
||||
* @param pollmethod
|
||||
* @param description
|
||||
* @param published
|
||||
* @param options
|
||||
* @param votesvalid
|
||||
* @param votesinvalid
|
||||
* @param votescast
|
||||
* @param has_votes
|
||||
* @param assignment_id
|
||||
*/
|
||||
constructor(
|
||||
id?: number,
|
||||
pollmethod?: string,
|
||||
description?: string,
|
||||
published?: boolean,
|
||||
options?: PollOption[],
|
||||
votesvalid?: number,
|
||||
votesinvalid?: number,
|
||||
votescast?: number,
|
||||
has_votes?: boolean,
|
||||
assignment_id?: number
|
||||
) {
|
||||
this.id = id;
|
||||
this.pollmethod = pollmethod;
|
||||
this.description = description;
|
||||
this.published = published;
|
||||
this.options = options || Array(new PollOption()); //TODO Array
|
||||
this.votesvalid = votesvalid;
|
||||
this.votesinvalid = votesinvalid;
|
||||
this.votescast = votescast;
|
||||
this.has_votes = has_votes;
|
||||
this.assignment_id = assignment_id;
|
||||
}
|
||||
|
||||
deserialize(input: any): this {
|
||||
Object.assign(this, input);
|
||||
|
||||
if (input.options instanceof Array) {
|
||||
this.options = [];
|
||||
input.options.forEach(pollOptionData => {
|
||||
this.options.push(new PollOption().deserialize(pollOptionData));
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
49
client/src/app/core/models/base-model.ts
Normal file
49
client/src/app/core/models/base-model.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { OpenSlidesComponent } from 'app/openslides.component';
|
||||
|
||||
/**
|
||||
* Define that an ID might be a number or a string.
|
||||
*/
|
||||
export type ModelId = number | string;
|
||||
|
||||
/**
|
||||
* Abstract parent class to set rules and functions for all models.
|
||||
*/
|
||||
export abstract class BaseModel extends OpenSlidesComponent {
|
||||
/**
|
||||
* force children of BaseModel to have a collectionString.
|
||||
*
|
||||
* Has a getter but no setter.
|
||||
*/
|
||||
protected abstract _collectionString: string;
|
||||
|
||||
/**
|
||||
* force children of BaseModel to have an `id`
|
||||
*/
|
||||
abstract id: ModelId;
|
||||
|
||||
/**
|
||||
* constructor that calls super from parent class
|
||||
*/
|
||||
protected constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the collectionString.
|
||||
*
|
||||
* The server and the dataStore use it to identify the collection.
|
||||
*/
|
||||
get collectionString(): string {
|
||||
return this._collectionString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Most simple and most commonly used deserialize function.
|
||||
* Inherited to children, can be overwritten for special use cases
|
||||
* @param input JSON data for deserialization.
|
||||
*/
|
||||
deserialize(input: any): this {
|
||||
Object.assign(this, input);
|
||||
return this;
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
// import { DS } from 'app/core/services/DS.service';
|
||||
import { ImproperlyConfiguredError } from 'app/core/exceptions';
|
||||
|
||||
const INVALID_COLLECTION_STRING = 'invalid-collection-string';
|
||||
|
||||
export type ModelId = number | string;
|
||||
|
||||
export abstract class BaseModel {
|
||||
static collectionString = INVALID_COLLECTION_STRING;
|
||||
id: ModelId;
|
||||
|
||||
constructor(id: ModelId) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
// convert an serialized version of the model to an instance of the class
|
||||
// jsonString is usually the server respince
|
||||
// T is the target model, User, Motion, Whatever
|
||||
// demands full functionening Models with constructors
|
||||
static fromJSON(jsonString: {}, Type): BaseModel {
|
||||
// create an instance of the User class
|
||||
const model = Object.create(Type.prototype);
|
||||
// copy all the fields from the json object
|
||||
return Object.assign(model, jsonString);
|
||||
}
|
||||
|
||||
public getCollectionString(): string {
|
||||
return BaseModel.collectionString;
|
||||
}
|
||||
|
||||
//TODO document this function.
|
||||
// public getCheckedCollectionString(): string {
|
||||
// if (this.collectionString === INVALID_COLLECTION_STRING) {
|
||||
// throw new ImproperlyConfiguredError(
|
||||
// 'Invalid collection string: Please override the static getCollectionString method!'
|
||||
// );
|
||||
// }
|
||||
// return collectionString;
|
||||
// }
|
||||
}
|
@ -1,19 +1,26 @@
|
||||
import { BaseModel } from 'app/core/models/baseModel';
|
||||
import { BaseModel } from '../base-model';
|
||||
|
||||
/**
|
||||
* Representation of chat messages.
|
||||
* @ignore
|
||||
*/
|
||||
export class ChatMessage extends BaseModel {
|
||||
static collectionString = 'core/chat-message';
|
||||
protected _collectionString: string;
|
||||
id: number;
|
||||
message: string;
|
||||
timestamp: string; // TODO: Type for timestamp
|
||||
user_id: number;
|
||||
|
||||
constructor(id: number, message?: string, timestamp?: string, user_id?: number) {
|
||||
super(id);
|
||||
constructor(id?: number, message?: string, timestamp?: string, user_id?: number) {
|
||||
super();
|
||||
this._collectionString = 'core/chat-message';
|
||||
this.id = id;
|
||||
this.message = message;
|
||||
this.timestamp = timestamp;
|
||||
this.user_id = user_id;
|
||||
}
|
||||
|
||||
public getCollectionString(): string {
|
||||
return ChatMessage.collectionString;
|
||||
getUser(): BaseModel | BaseModel[] {
|
||||
return this.DS.get('users/user', this.user_id);
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,20 @@
|
||||
import { BaseModel } from 'app/core/models/baseModel';
|
||||
import { BaseModel } from '../base-model';
|
||||
|
||||
/**
|
||||
* Representation of a config variable
|
||||
* @ignore
|
||||
*/
|
||||
export class Config extends BaseModel {
|
||||
static collectionString = 'core/config';
|
||||
protected _collectionString: string;
|
||||
id: number;
|
||||
key: string;
|
||||
value: Object;
|
||||
|
||||
constructor(id: number, key?: string, value?: Object) {
|
||||
super(id);
|
||||
constructor(id?: number, key?: string, value?: Object) {
|
||||
super();
|
||||
this._collectionString = 'core/config';
|
||||
this.id = id;
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public getCollectionString(): string {
|
||||
return Config.collectionString;
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,24 @@
|
||||
import { BaseModel } from 'app/core/models/baseModel';
|
||||
import { BaseModel } from 'app/core/models/base-model';
|
||||
|
||||
/**
|
||||
* Representation of a countdown
|
||||
* @ignore
|
||||
*/
|
||||
export class Countdown extends BaseModel {
|
||||
static collectionString = 'core/countdown';
|
||||
protected _collectionString: string;
|
||||
id: number;
|
||||
countdown_time: number;
|
||||
default_time: number;
|
||||
description: string;
|
||||
default_time: number;
|
||||
countdown_time: number;
|
||||
running: boolean;
|
||||
|
||||
constructor(id: number, countdown_time?: number, default_time?: number, description?: string) {
|
||||
super(id);
|
||||
constructor(id?: number, countdown_time?: number, default_time?: number, description?: string, running?: boolean) {
|
||||
super();
|
||||
this._collectionString = 'core/countdown';
|
||||
this.id = id;
|
||||
this.countdown_time = countdown_time;
|
||||
this.default_time = default_time;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public getCollectionString(): string {
|
||||
return Countdown.collectionString;
|
||||
this.default_time = default_time;
|
||||
this.countdown_time = countdown_time;
|
||||
this.running = running;
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,18 @@
|
||||
import { BaseModel } from 'app/core/models/baseModel';
|
||||
import { BaseModel } from 'app/core/models/base-model';
|
||||
|
||||
/**
|
||||
* Representation of a projector message.
|
||||
* @ignore
|
||||
*/
|
||||
export class ProjectorMessage extends BaseModel {
|
||||
static collectionString = 'core/projector-message';
|
||||
protected _collectionString: string;
|
||||
id: number;
|
||||
message: string;
|
||||
|
||||
constructor(id: number, message?: string) {
|
||||
super(id);
|
||||
constructor(id?: number, message?: string) {
|
||||
super();
|
||||
this._collectionString = 'core/projector-message';
|
||||
this.id = id;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public getCollectionString(): string {
|
||||
return ProjectorMessage.collectionString;
|
||||
}
|
||||
}
|
||||
|
@ -1,40 +1,42 @@
|
||||
import { BaseModel } from 'app/core/models/baseModel';
|
||||
import { BaseModel } from 'app/core/models/base-model';
|
||||
|
||||
/**
|
||||
* Representation of a projector. Has the nested property "projectiondefaults"
|
||||
* @ignore
|
||||
*/
|
||||
export class Projector extends BaseModel {
|
||||
static collectionString = 'core/projector';
|
||||
protected _collectionString: string;
|
||||
id: number;
|
||||
blank: boolean;
|
||||
elements: Object;
|
||||
height: number;
|
||||
name: string;
|
||||
projectiondefaults: BaseModel[];
|
||||
scale: number;
|
||||
scroll: number;
|
||||
name: string;
|
||||
blank: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
projectiondefaults: Object[];
|
||||
|
||||
constructor(
|
||||
id: number,
|
||||
blank?: boolean,
|
||||
id?: number,
|
||||
elements?: Object,
|
||||
height?: number,
|
||||
name?: string,
|
||||
projectiondefaults?: BaseModel[],
|
||||
scale?: number,
|
||||
scroll?: number,
|
||||
width?: number
|
||||
name?: string,
|
||||
blank?: boolean,
|
||||
width?: number,
|
||||
height?: number,
|
||||
projectiondefaults?: Object[]
|
||||
) {
|
||||
super(id);
|
||||
this.blank = blank;
|
||||
super();
|
||||
this._collectionString = 'core/projector';
|
||||
this.id = id;
|
||||
this.elements = elements;
|
||||
this.height = height;
|
||||
this.name = name;
|
||||
this.projectiondefaults = projectiondefaults;
|
||||
this.scale = scale;
|
||||
this.scroll = scroll;
|
||||
this.name = name;
|
||||
this.blank = blank;
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public getCollectionString(): string {
|
||||
return Projector.collectionString;
|
||||
this.height = height;
|
||||
this.projectiondefaults = projectiondefaults;
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,18 @@
|
||||
import { BaseModel } from 'app/core/models/baseModel';
|
||||
import { BaseModel } from 'app/core/models/base-model';
|
||||
|
||||
/**
|
||||
* Representation of a tag.
|
||||
* @ignore
|
||||
*/
|
||||
export class Tag extends BaseModel {
|
||||
static collectionString = 'core/tag';
|
||||
protected _collectionString: string;
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
constructor(id: number, name?: string) {
|
||||
super(id);
|
||||
constructor(id?: number, name?: string) {
|
||||
super();
|
||||
this._collectionString = 'core/tag';
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public getCollectionString(): string {
|
||||
return Tag.collectionString;
|
||||
}
|
||||
}
|
||||
|
13
client/src/app/core/models/deserializable.model.ts
Normal file
13
client/src/app/core/models/deserializable.model.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Interface tells models to offer a 'deserialize' function
|
||||
*
|
||||
* Also nested objects and arrays have have to be handled.
|
||||
* @example const myUser = new User().deserialize(jsonData)
|
||||
*/
|
||||
export interface Deserializable {
|
||||
/**
|
||||
* should be used to assign JSON values to the object itself.
|
||||
* @param input
|
||||
*/
|
||||
deserialize(input: any): this;
|
||||
}
|
25
client/src/app/core/models/mediafiles/file.ts
Normal file
25
client/src/app/core/models/mediafiles/file.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Deserializable } from '../deserializable.model';
|
||||
|
||||
/**
|
||||
* The name and the type of a mediaFile.
|
||||
* @ignore
|
||||
*/
|
||||
export class File implements Deserializable {
|
||||
name: string;
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Needs to be fully optional, because the 'mediafile'-property in the mediaFile class is optional as well
|
||||
* @param name The name of the file
|
||||
* @param type The tape (jpg, png, pdf)
|
||||
*/
|
||||
constructor(name?: string, type?: string) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
deserialize(input: any): this {
|
||||
Object.assign(this, input);
|
||||
return this;
|
||||
}
|
||||
}
|
@ -1,37 +1,50 @@
|
||||
import { BaseModel } from 'app/core/models/baseModel';
|
||||
import { BaseModel } from 'app/core/models/base-model';
|
||||
import { File } from './file';
|
||||
|
||||
/**
|
||||
* Representation of MediaFile. Has the nested property "File"
|
||||
* @ignore
|
||||
*/
|
||||
export class Mediafile extends BaseModel {
|
||||
static collectionString = 'mediafiles/mediafile';
|
||||
protected _collectionString: string;
|
||||
id: number;
|
||||
title: string;
|
||||
mediafile: File;
|
||||
media_url_prefix: string;
|
||||
uploader_id: number;
|
||||
filesize: string;
|
||||
hidden: boolean;
|
||||
media_url_prefix: string;
|
||||
mediafile: Object;
|
||||
timestamp: string;
|
||||
title: string;
|
||||
uploader_id: number;
|
||||
|
||||
constructor(
|
||||
id: number,
|
||||
id?: number,
|
||||
title?: string,
|
||||
mediafile?: File,
|
||||
media_url_prefix?: string,
|
||||
uploader_id?: number,
|
||||
filesize?: string,
|
||||
hidden?: boolean,
|
||||
media_url_prefix?: string,
|
||||
mediafile?: Object,
|
||||
timestamp?: string,
|
||||
title?: string,
|
||||
uploader_id?: number
|
||||
timestamp?: string
|
||||
) {
|
||||
super(id);
|
||||
super();
|
||||
this._collectionString = 'mediafiles/mediafile';
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.mediafile = mediafile;
|
||||
this.media_url_prefix = media_url_prefix;
|
||||
this.uploader_id = uploader_id;
|
||||
this.filesize = filesize;
|
||||
this.hidden = hidden;
|
||||
this.media_url_prefix = media_url_prefix;
|
||||
this.mediafile = mediafile;
|
||||
this.timestamp = timestamp;
|
||||
this.title = title;
|
||||
this.uploader_id = uploader_id;
|
||||
}
|
||||
|
||||
public getCollectionString(): string {
|
||||
return Mediafile.collectionString;
|
||||
deserialize(input: any): this {
|
||||
Object.assign(this, input);
|
||||
this.mediafile = new File().deserialize(input.mediafile);
|
||||
return this;
|
||||
}
|
||||
|
||||
getUploader(): BaseModel | BaseModel[] {
|
||||
return this.DS.get('users/user', this.uploader_id);
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,20 @@
|
||||
import { BaseModel } from 'app/core/models/baseModel';
|
||||
import { BaseModel } from 'app/core/models/base-model';
|
||||
|
||||
/**
|
||||
* Representation of a motion category. Has the nested property "File"
|
||||
* @ignore
|
||||
*/
|
||||
export class Category extends BaseModel {
|
||||
static collectionString = 'motions/category';
|
||||
protected _collectionString: string;
|
||||
id: number;
|
||||
name: string;
|
||||
prefix: string;
|
||||
|
||||
constructor(id: number, name?: string, prefix?: string) {
|
||||
super(id);
|
||||
constructor(id?: number, name?: string, prefix?: string) {
|
||||
super();
|
||||
this._collectionString = 'motions/category';
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
public getCollectionString(): string {
|
||||
return Category.collectionString;
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,24 @@
|
||||
import { BaseModel } from 'app/core/models/baseModel';
|
||||
import { BaseModel } from 'app/core/models/base-model';
|
||||
|
||||
/**
|
||||
* Representation of a motion block.
|
||||
* @ignore
|
||||
*/
|
||||
export class MotionBlock extends BaseModel {
|
||||
static collectionString = 'motions/motion-block';
|
||||
protected _collectionString: string;
|
||||
id: number;
|
||||
agenda_item_id: number;
|
||||
title: string;
|
||||
agenda_item_id: number;
|
||||
|
||||
constructor(id: number, agenda_item_id?: number, title?: string) {
|
||||
super(id);
|
||||
this.agenda_item_id = agenda_item_id;
|
||||
constructor(id?: number, title?: string, agenda_item_id?: number) {
|
||||
super();
|
||||
this._collectionString = 'motions/motion-block';
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.agenda_item_id = agenda_item_id;
|
||||
}
|
||||
|
||||
public getCollectionString(): string {
|
||||
return MotionBlock.collectionString;
|
||||
getAgenda(): BaseModel | BaseModel[] {
|
||||
return this.DS.get('agenda/item', this.agenda_item_id);
|
||||
}
|
||||
}
|
||||
|
@ -1,40 +1,42 @@
|
||||
import { BaseModel } from 'app/core/models/baseModel';
|
||||
import { BaseModel } from 'app/core/models/base-model';
|
||||
|
||||
/**
|
||||
* Representation of a motion change recommendation.
|
||||
* @ignore
|
||||
*/
|
||||
export class MotionChangeReco extends BaseModel {
|
||||
static collectionString = 'motions/motion-change-recommendation';
|
||||
protected _collectionString: string;
|
||||
id: number;
|
||||
creation_time: string;
|
||||
motion_version_id: number;
|
||||
rejected: boolean;
|
||||
type: number;
|
||||
other_description: string;
|
||||
line_from: number;
|
||||
line_to: number;
|
||||
motion_version_id: number;
|
||||
other_description: string;
|
||||
rejected: boolean;
|
||||
text: string;
|
||||
type: number;
|
||||
creation_time: string;
|
||||
|
||||
constructor(
|
||||
id: number,
|
||||
creation_time?: string,
|
||||
id?: number,
|
||||
motion_version_id?: number,
|
||||
rejected?: boolean,
|
||||
type?: number,
|
||||
other_description?: string,
|
||||
line_from?: number,
|
||||
line_to?: number,
|
||||
motion_version_id?: number,
|
||||
other_description?: string,
|
||||
rejected?: boolean,
|
||||
text?: string,
|
||||
type?: number
|
||||
creation_time?: string
|
||||
) {
|
||||
super(id);
|
||||
this.creation_time = creation_time;
|
||||
super();
|
||||
this._collectionString = 'motions/motion-change-recommendation';
|
||||
this.id = id;
|
||||
this.motion_version_id = motion_version_id;
|
||||
this.rejected = rejected;
|
||||
this.type = type;
|
||||
this.other_description = other_description;
|
||||
this.line_from = line_from;
|
||||
this.line_to = line_to;
|
||||
this.motion_version_id = motion_version_id;
|
||||
this.other_description = other_description;
|
||||
this.rejected = rejected;
|
||||
this.text = text;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public getCollectionString(): string {
|
||||
return MotionChangeReco.collectionString;
|
||||
this.creation_time = creation_time;
|
||||
}
|
||||
}
|
||||
|
@ -1,70 +1,75 @@
|
||||
import { BaseModel } from 'app/core/models/baseModel';
|
||||
import { BaseModel } from 'app/core/models/base-model';
|
||||
|
||||
/**
|
||||
* Representation of Motion.
|
||||
*
|
||||
* Untouched for now because of heavy maintainance on server side
|
||||
*
|
||||
* @ignore
|
||||
*/
|
||||
export class Motion extends BaseModel {
|
||||
static collectionString = 'motions/motion';
|
||||
protected _collectionString: string;
|
||||
id: number;
|
||||
active_version: number;
|
||||
agenda_item_id: number;
|
||||
attachments_id: number[];
|
||||
category_id: number;
|
||||
comments: Object;
|
||||
identifier: string;
|
||||
log_messages: Object[];
|
||||
versions: Object[];
|
||||
active_version: number;
|
||||
parent_id: number;
|
||||
category_id: number;
|
||||
motion_block_id: number;
|
||||
origin: string;
|
||||
parent_id: number;
|
||||
polls: BaseModel[];
|
||||
recommendation_id: number;
|
||||
state_id: number;
|
||||
state_required_permission_to_see: string;
|
||||
submitters: Object[];
|
||||
supporters_id: number[];
|
||||
comments: Object;
|
||||
state_id: number;
|
||||
state_required_permission_to_see: string;
|
||||
recommendation_id: number;
|
||||
tags_id: number[];
|
||||
versions: Object[];
|
||||
attachments_id: number[];
|
||||
polls: BaseModel[];
|
||||
agenda_item_id: number;
|
||||
log_messages: Object[];
|
||||
|
||||
constructor(
|
||||
id: number,
|
||||
active_version?: number,
|
||||
agenda_item_id?: number,
|
||||
attachments_id?: number[],
|
||||
category_id?: number,
|
||||
comments?: Object,
|
||||
id?: number,
|
||||
identifier?: string,
|
||||
log_messages?: Object[],
|
||||
versions?: Object[],
|
||||
active_version?: number,
|
||||
parent_id?: number,
|
||||
category_id?: number,
|
||||
motion_block_id?: number,
|
||||
origin?: string,
|
||||
parent_id?: number,
|
||||
polls?: BaseModel[],
|
||||
recommendation_id?: number,
|
||||
state_id?: number,
|
||||
state_required_permission_to_see?: string,
|
||||
submitters?: Object[],
|
||||
supporters_id?: number[],
|
||||
comments?: Object,
|
||||
state_id?: number,
|
||||
state_required_permission_to_see?: string,
|
||||
recommendation_id?: number,
|
||||
tags_id?: number[],
|
||||
versions?: Object[]
|
||||
attachments_id?: number[],
|
||||
polls?: BaseModel[],
|
||||
agenda_item_id?: number,
|
||||
log_messages?: Object[]
|
||||
) {
|
||||
super(id);
|
||||
this.active_version = active_version;
|
||||
this.agenda_item_id = agenda_item_id;
|
||||
this.attachments_id = attachments_id;
|
||||
this.category_id = category_id;
|
||||
this.comments = comments;
|
||||
super();
|
||||
this._collectionString = 'motions/motion';
|
||||
this.id = id;
|
||||
this.identifier = identifier;
|
||||
this.log_messages = log_messages;
|
||||
this.versions = versions;
|
||||
this.active_version = active_version;
|
||||
this.parent_id = parent_id;
|
||||
this.category_id = category_id;
|
||||
this.motion_block_id = motion_block_id;
|
||||
this.origin = origin;
|
||||
this.parent_id = parent_id;
|
||||
this.polls = polls;
|
||||
this.recommendation_id = recommendation_id;
|
||||
this.state_id = state_id;
|
||||
this.state_required_permission_to_see = state_required_permission_to_see;
|
||||
this.submitters = submitters;
|
||||
this.supporters_id = supporters_id;
|
||||
this.comments = comments;
|
||||
this.state_id = state_id;
|
||||
this.state_required_permission_to_see = state_required_permission_to_see;
|
||||
this.recommendation_id = recommendation_id;
|
||||
this.tags_id = tags_id;
|
||||
this.versions = versions;
|
||||
}
|
||||
|
||||
public getCollectionString(): string {
|
||||
return Motion.collectionString;
|
||||
this.attachments_id = attachments_id;
|
||||
this.polls = polls;
|
||||
this.agenda_item_id = agenda_item_id;
|
||||
this.log_messages = log_messages;
|
||||
}
|
||||
}
|
||||
|
86
client/src/app/core/models/motions/workflow-state.ts
Normal file
86
client/src/app/core/models/motions/workflow-state.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { Deserializable } from '../deserializable.model';
|
||||
|
||||
/**
|
||||
* Representation of a workflow state
|
||||
*
|
||||
* Part of the 'states'-array in motion/workflow
|
||||
* @ignore
|
||||
*/
|
||||
export class WorkflowState implements Deserializable {
|
||||
id: number;
|
||||
name: string;
|
||||
action_word: string;
|
||||
recommendation_label: string;
|
||||
css_class: string;
|
||||
required_permission_to_see: string;
|
||||
allow_support: boolean;
|
||||
allow_create_poll: boolean;
|
||||
allow_submitter_edit: boolean;
|
||||
versioning: boolean;
|
||||
leave_old_version_active: boolean;
|
||||
dont_set_identifier: boolean;
|
||||
show_state_extension_field: boolean;
|
||||
show_recommendation_extension_field: boolean;
|
||||
next_states_id: number[];
|
||||
workflow_id: number;
|
||||
|
||||
/**
|
||||
* Needs to be completely optional because Workflow has (yet) the optional parameter 'states'
|
||||
* @param id
|
||||
* @param name
|
||||
* @param action_word
|
||||
* @param recommendation_label
|
||||
* @param css_class
|
||||
* @param required_permission_to_see
|
||||
* @param allow_support
|
||||
* @param allow_create_poll
|
||||
* @param allow_submitter_edit
|
||||
* @param versioning
|
||||
* @param leave_old_version_active
|
||||
* @param dont_set_identifier
|
||||
* @param show_state_extension_field
|
||||
* @param show_recommendation_extension_field
|
||||
* @param next_states_id
|
||||
* @param workflow_id
|
||||
*/
|
||||
constructor(
|
||||
id?: number,
|
||||
name?: string,
|
||||
action_word?: string,
|
||||
recommendation_label?: string,
|
||||
css_class?: string,
|
||||
required_permission_to_see?: string,
|
||||
allow_support?: boolean,
|
||||
allow_create_poll?: boolean,
|
||||
allow_submitter_edit?: boolean,
|
||||
versioning?: boolean,
|
||||
leave_old_version_active?: boolean,
|
||||
dont_set_identifier?: boolean,
|
||||
show_state_extension_field?: boolean,
|
||||
show_recommendation_extension_field?: boolean,
|
||||
next_states_id?: number[],
|
||||
workflow_id?: number
|
||||
) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.action_word = action_word;
|
||||
this.recommendation_label = recommendation_label;
|
||||
this.css_class = css_class;
|
||||
this.required_permission_to_see = required_permission_to_see;
|
||||
this.allow_support = allow_support;
|
||||
this.allow_create_poll = allow_create_poll;
|
||||
this.allow_submitter_edit = allow_submitter_edit;
|
||||
this.versioning = versioning;
|
||||
this.leave_old_version_active = leave_old_version_active;
|
||||
this.dont_set_identifier = dont_set_identifier;
|
||||
this.show_state_extension_field = show_state_extension_field;
|
||||
this.show_recommendation_extension_field = show_recommendation_extension_field;
|
||||
this.next_states_id = next_states_id;
|
||||
this.workflow_id = workflow_id;
|
||||
}
|
||||
|
||||
deserialize(input: any): this {
|
||||
Object.assign(this, input);
|
||||
return this;
|
||||
}
|
||||
}
|
@ -1,20 +1,34 @@
|
||||
import { BaseModel } from 'app/core/models/baseModel';
|
||||
import { BaseModel } from 'app/core/models/base-model';
|
||||
import { WorkflowState } from './workflow-state';
|
||||
|
||||
/**
|
||||
* Representation of a motion workflow. Has the nested property 'states'
|
||||
* @ignore
|
||||
*/
|
||||
export class Workflow extends BaseModel {
|
||||
static collectionString = 'motions/workflow';
|
||||
protected _collectionString: string;
|
||||
id: number;
|
||||
first_state: number;
|
||||
name: string;
|
||||
states: Object[];
|
||||
states: WorkflowState[];
|
||||
first_state: number;
|
||||
|
||||
constructor(id: number, first_state?, name?, states?) {
|
||||
super(id);
|
||||
this.first_state = first_state;
|
||||
constructor(id?: number, name?: string, states?: WorkflowState[], first_state?: number) {
|
||||
super();
|
||||
this._collectionString = 'motions/workflow';
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.states = states;
|
||||
this.first_state = first_state;
|
||||
}
|
||||
|
||||
public getCollectionString(): string {
|
||||
return Workflow.collectionString;
|
||||
deserialize(input: any): this {
|
||||
Object.assign(this, input);
|
||||
if (input.states instanceof Array) {
|
||||
this.states = [];
|
||||
input.states.forEach(workflowStateData => {
|
||||
this.states.push(new WorkflowState().deserialize(workflowStateData));
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,32 @@
|
||||
import { BaseModel } from 'app/core/models/baseModel';
|
||||
import { BaseModel } from 'app/core/models/base-model';
|
||||
|
||||
/**
|
||||
* Representation of a topic.
|
||||
* @ignore
|
||||
*/
|
||||
export class Topic extends BaseModel {
|
||||
static collectionString = 'topics/topic';
|
||||
protected _collectionString: string;
|
||||
id: number;
|
||||
agenda_item_id: number;
|
||||
attachments_id: number[];
|
||||
text: string;
|
||||
title: string;
|
||||
text: string;
|
||||
attachments_id: number[];
|
||||
agenda_item_id: number;
|
||||
|
||||
constructor(id: number, agenda_item_id?: number, attachments_id?: number[], text?: string, title?: string) {
|
||||
super(id);
|
||||
this.agenda_item_id = agenda_item_id;
|
||||
this.attachments_id = attachments_id;
|
||||
this.text = text;
|
||||
constructor(id?: number, title?: string, text?: string, attachments_id?: number[], agenda_item_id?: number) {
|
||||
super();
|
||||
this._collectionString = 'topics/topic';
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.text = text;
|
||||
this.attachments_id = attachments_id;
|
||||
this.agenda_item_id = agenda_item_id;
|
||||
}
|
||||
|
||||
public getCollectionString(): string {
|
||||
return Topic.collectionString;
|
||||
getAttachments(): BaseModel | BaseModel[] {
|
||||
return this.DS.get('mediafiles/mediafile', ...this.attachments_id);
|
||||
}
|
||||
|
||||
getAgenda(): BaseModel | BaseModel[] {
|
||||
return this.DS.get('agenda/item', this.agenda_item_id);
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,20 @@
|
||||
import { BaseModel } from 'app/core/models/baseModel';
|
||||
import { BaseModel } from 'app/core/models/base-model';
|
||||
|
||||
/**
|
||||
* Representation of user group.
|
||||
* @ignore
|
||||
*/
|
||||
export class Group extends BaseModel {
|
||||
static collectionString = 'users/group';
|
||||
protected _collectionString: string;
|
||||
id: number;
|
||||
name: string;
|
||||
permissions: string[]; //TODO permissions could be an own model?
|
||||
permissions: string[];
|
||||
|
||||
constructor(id: number, name?: string, permissions?: string[]) {
|
||||
super(id);
|
||||
constructor(id?: number, name?: string, permissions?: string[]) {
|
||||
super();
|
||||
this._collectionString = 'users/group';
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.permissions = permissions;
|
||||
}
|
||||
|
||||
public getCollectionString(): string {
|
||||
return Group.collectionString;
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,24 @@
|
||||
import { BaseModel } from 'app/core/models/baseModel';
|
||||
import { BaseModel } from 'app/core/models/base-model';
|
||||
|
||||
/**
|
||||
* Representation of users personal note.
|
||||
* @ignore
|
||||
*/
|
||||
export class PersonalNote extends BaseModel {
|
||||
static collectionString = 'users/personal-note';
|
||||
protected _collectionString: string;
|
||||
id: number;
|
||||
notes: Object;
|
||||
user_id: number;
|
||||
notes: Object;
|
||||
|
||||
constructor(id: number, notes?: Object, user_id?: number) {
|
||||
super(id);
|
||||
this.notes = notes;
|
||||
constructor(id?: number, user_id?: number, notes?: Object) {
|
||||
super();
|
||||
this._collectionString = 'users/personal-note';
|
||||
this.id = id;
|
||||
this.user_id = user_id;
|
||||
this.notes = notes;
|
||||
}
|
||||
|
||||
public getCollectionString(): string {
|
||||
return PersonalNote.collectionString;
|
||||
getUser(): BaseModel | BaseModel[] {
|
||||
return this.DS.get('users/user', this.user_id);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { BaseModel } from 'app/core/models/baseModel';
|
||||
|
||||
// import { DS } from 'app/core/services/DS.service';
|
||||
import { BaseModel } from 'app/core/models/base-model';
|
||||
|
||||
/**
|
||||
* Representation of a user in contrast to the operator.
|
||||
* @ignore
|
||||
*/
|
||||
export class User extends BaseModel {
|
||||
static collectionString = 'users/user';
|
||||
protected _collectionString: string;
|
||||
id: number;
|
||||
username: string;
|
||||
title: string;
|
||||
@ -21,9 +23,8 @@ export class User extends BaseModel {
|
||||
is_active: boolean;
|
||||
default_password: string;
|
||||
|
||||
//default constructer with every possible optinal parameter for conventient usage
|
||||
constructor(
|
||||
id: number,
|
||||
id?: number,
|
||||
username?: string,
|
||||
title?: string,
|
||||
first_name?: string,
|
||||
@ -40,7 +41,9 @@ export class User extends BaseModel {
|
||||
is_active?: boolean,
|
||||
default_password?: string
|
||||
) {
|
||||
super(id);
|
||||
super();
|
||||
this._collectionString = 'users/user';
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.title = title;
|
||||
this.first_name = first_name;
|
||||
@ -58,7 +61,7 @@ export class User extends BaseModel {
|
||||
this.default_password = default_password;
|
||||
}
|
||||
|
||||
public getCollectionString(): string {
|
||||
return User.collectionString;
|
||||
getGroups(): BaseModel | BaseModel[] {
|
||||
return this.DS.get('users/group', ...this.groups_id);
|
||||
}
|
||||
}
|
||||
|
@ -11,12 +11,28 @@ import { map } from 'rxjs/operators/';
|
||||
*
|
||||
*/
|
||||
export class PruningTranslationLoader implements TranslateLoader {
|
||||
/**
|
||||
* Constructor to load the HttpClient
|
||||
*
|
||||
* @param http httpClient to load the translation files.
|
||||
* @param prefix Path to the language files. Can be adjusted of needed
|
||||
* @param suffix Suffix of the translation files. Usually '.json'.
|
||||
*/
|
||||
constructor(private http: HttpClient, private prefix: string = '/assets/i18n/', private suffix: string = '.json') {}
|
||||
|
||||
/**
|
||||
* Loads a language file, stores the content, give it to the process function.
|
||||
* @param lang language string (en, fr, de, ...)
|
||||
*/
|
||||
public getTranslation(lang: string): any {
|
||||
return this.http.get(`${this.prefix}${lang}${this.suffix}`).pipe(map((res: Object) => this.process(res)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent to display empty strings as a translation.
|
||||
* Falls back to the default language or simply copy the content of the key.
|
||||
* @param any the content of any language file.
|
||||
*/
|
||||
private process(object: any) {
|
||||
const newObject = {};
|
||||
for (const key in object) {
|
||||
|
@ -1,145 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpResponse, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable, of, BehaviorSubject } from 'rxjs';
|
||||
import { tap, map } from 'rxjs/operators';
|
||||
|
||||
import { ImproperlyConfiguredError } from 'app/core/exceptions';
|
||||
import { BaseModel, ModelId } from 'app/core/models/baseModel';
|
||||
|
||||
interface Collection {
|
||||
[id: number]: BaseModel;
|
||||
}
|
||||
|
||||
interface Storrage {
|
||||
[collectionString: string]: Collection;
|
||||
}
|
||||
|
||||
// Todo: DRY. This is a copy from /authService. probably repository service necessary
|
||||
const httpOptions = {
|
||||
withCredentials: true,
|
||||
headers: new HttpHeaders({
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
};
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DataStoreService {
|
||||
// needs to be static cause becauseusing dependency injection, services are unique for a scope.
|
||||
private static store: Storrage = {};
|
||||
// observe datastore to enable dynamic changes in models and view
|
||||
private static dataStoreSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
// read one, multiple or all ID's from DataStore
|
||||
// example: this.DS.get(User) || (User, 1) || (User, 1, 2) || (User, ...[1,2,3,4,5])
|
||||
get(Type, ...ids: ModelId[]): BaseModel[] | BaseModel {
|
||||
const collection: Collection = DataStoreService.store[Type.collectionString];
|
||||
const models = [];
|
||||
|
||||
if (!collection) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (ids.length === 0) {
|
||||
return Object.values(collection);
|
||||
} else {
|
||||
ids.forEach(id => {
|
||||
const model: BaseModel = collection[id];
|
||||
models.push(model);
|
||||
});
|
||||
}
|
||||
return models.length === 1 ? models[0] : models;
|
||||
}
|
||||
|
||||
// print the whole store for debug purposes
|
||||
printWhole(): void {
|
||||
console.log('Everythin in DataStore: ', DataStoreService.store);
|
||||
}
|
||||
|
||||
// TODO: type for callback function
|
||||
// example: this.DS.filter(User, myUser => myUser.first_name === "Max")
|
||||
filter(Type, callback): BaseModel[] {
|
||||
let filterCollection = [];
|
||||
const typeCollection = this.get(Type);
|
||||
|
||||
if (Array.isArray(typeCollection)) {
|
||||
filterCollection = [...filterCollection, ...typeCollection];
|
||||
} else {
|
||||
filterCollection.push(typeCollection);
|
||||
}
|
||||
return filterCollection.filter(callback);
|
||||
}
|
||||
|
||||
// add one or moultiple models to DataStore
|
||||
// use spread operator ("...") for arrays
|
||||
// example: this.DS.add(new User(1)) || (new User(2), new User(3)) || (arrayWithUsers)
|
||||
add(...models: BaseModel[]): void {
|
||||
models.forEach(model => {
|
||||
const collectionString = model.getCollectionString();
|
||||
if (!model.id) {
|
||||
throw new ImproperlyConfiguredError('The model must have an id!');
|
||||
} else if (collectionString === 'invalid-collection-string') {
|
||||
throw new ImproperlyConfiguredError('Cannot save a BaseModel');
|
||||
}
|
||||
if (typeof DataStoreService.store[collectionString] === 'undefined') {
|
||||
DataStoreService.store[collectionString] = {};
|
||||
}
|
||||
DataStoreService.store[collectionString][model.id] = model;
|
||||
this.setObservable(model);
|
||||
});
|
||||
}
|
||||
|
||||
// removes one or moultiple models from DataStore
|
||||
// use spread operator ("...") for arrays
|
||||
// Type should be any BaseModel
|
||||
// example: this.DS.remove(User, 1) || this.DS.remove(User, myUser.id, 3, 4)
|
||||
remove(Type, ...ids: ModelId[]): void {
|
||||
ids.forEach(id => {
|
||||
if (DataStoreService.store[Type.collectionString]) {
|
||||
delete DataStoreService.store[Type.collectionString][id];
|
||||
console.log(`did remove "${id}" from Datastore "${Type.collectionString}"`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO remove the any there and in BaseModel.
|
||||
save(model: BaseModel): Observable<BaseModel> {
|
||||
if (!model.id) {
|
||||
throw new ImproperlyConfiguredError('The model must have an id!');
|
||||
}
|
||||
|
||||
// TODO not tested
|
||||
return this.http.post<BaseModel>(model.getCollectionString() + '/', model, httpOptions).pipe(
|
||||
tap(response => {
|
||||
console.log('the response: ', response);
|
||||
this.add(model);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// send a http request to the server to delete the given model
|
||||
delete(model: BaseModel): Observable<BaseModel> {
|
||||
if (!model.id) {
|
||||
throw new ImproperlyConfiguredError('The model must have an id!');
|
||||
}
|
||||
|
||||
// TODO not tested
|
||||
return this.http.post<BaseModel>(model.getCollectionString() + '/', model, httpOptions).pipe(
|
||||
tap(response => {
|
||||
console.log('the response: ', response);
|
||||
this.remove(model, model.id);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public getObservable(): Observable<any> {
|
||||
return DataStoreService.dataStoreSubject.asObservable();
|
||||
}
|
||||
|
||||
private setObservable(value) {
|
||||
DataStoreService.dataStoreSubject.next(value);
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { AlertService } from './alert.service';
|
||||
|
||||
describe('AlertService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [AlertService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([AlertService], (service: AlertService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
@ -1,38 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Alert, AlertType } from 'app/core/models/alert';
|
||||
|
||||
/**TODO Drafted for now. Since the UI is not done yet, this might be replaced or disappear entirely.
|
||||
* Furtermore, Material UI does not support these kinds of alerts
|
||||
*/
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AlertService {
|
||||
constructor() {}
|
||||
|
||||
success(message: string): Alert {
|
||||
return this.alert(AlertType.Success, message);
|
||||
}
|
||||
|
||||
error(message: string): Alert {
|
||||
// console.log('this.alert : ', this.alert());
|
||||
// this.alert(AlertType.Error, message);
|
||||
return this.alert(AlertType.Error, message);
|
||||
}
|
||||
|
||||
info(message: string): Alert {
|
||||
return this.alert(AlertType.Info, message);
|
||||
}
|
||||
|
||||
warn(message: string): Alert {
|
||||
return this.alert(AlertType.Warning, message);
|
||||
}
|
||||
|
||||
alert(type: AlertType, message: string): Alert {
|
||||
return <Alert>{ type: type, message: message };
|
||||
}
|
||||
|
||||
// TODO fromHttpError() to generate alerts form http errors
|
||||
}
|
@ -4,12 +4,32 @@ import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from
|
||||
import { AuthService } from './auth.service';
|
||||
import { OperatorService } from './operator.service';
|
||||
|
||||
/**
|
||||
* Classical Auth-Guard. Checks if the user has to correct permissions to enter a page, and forwards to login if not.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthGuard implements CanActivate {
|
||||
/**
|
||||
* Initialises the authentication, the operator and the Router
|
||||
* @param authService
|
||||
* @param operator
|
||||
* @param router
|
||||
*/
|
||||
constructor(private authService: AuthService, private operator: OperatorService, private router: Router) {}
|
||||
|
||||
/**
|
||||
* Checks of the operator has n id.
|
||||
* If so, forward to the desired target.
|
||||
*
|
||||
* If not, forward to login.
|
||||
*
|
||||
* TODO: Test if this works for guests and on Projector
|
||||
*
|
||||
* @param route required by `canActivate()`
|
||||
* @param state the state (URL) that the user want to access
|
||||
*/
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): any {
|
||||
const url: string = state.url;
|
||||
|
||||
|
@ -4,47 +4,62 @@ import { Observable, of, throwError } from 'rxjs';
|
||||
import { catchError, tap } from 'rxjs/operators';
|
||||
|
||||
import { OperatorService } from 'app/core/services/operator.service';
|
||||
import { OpenSlidesComponent } from '../../openslides.component';
|
||||
|
||||
const httpOptions = {
|
||||
withCredentials: true,
|
||||
headers: new HttpHeaders({
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticates an OpenSlides user with username and password
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
export class AuthService extends OpenSlidesComponent {
|
||||
/**
|
||||
* if the user tries to access a certain URL without being authenticated, the URL will be stored here
|
||||
*/
|
||||
redirectUrl: string;
|
||||
|
||||
constructor(private http: HttpClient, private operator: OperatorService) {}
|
||||
/**
|
||||
* Initializes the httpClient and the {@link OperatorService}.
|
||||
*
|
||||
* Calls `super()` from the parent class.
|
||||
* @param http HttpClient
|
||||
* @param operator who is using OpenSlides
|
||||
*/
|
||||
constructor(private http: HttpClient, private operator: OperatorService) {
|
||||
super();
|
||||
}
|
||||
|
||||
//loggins a users. expects a user model
|
||||
/**
|
||||
* Try to log in a user.
|
||||
*
|
||||
* Returns an observable 'user' with the correct login information or an error.
|
||||
* The user will then be stored in the {@link OperatorService},
|
||||
* errors will be forwarded to the parents error function.
|
||||
*
|
||||
* @param username
|
||||
* @param password
|
||||
*/
|
||||
login(username: string, password: string): Observable<any> {
|
||||
const user: any = {
|
||||
username: username,
|
||||
password: password
|
||||
};
|
||||
return this.http.post<any>('/users/login/', user, httpOptions).pipe(
|
||||
return this.http.post<any>('/users/login/', user).pipe(
|
||||
tap(resp => this.operator.storeUser(resp.user)),
|
||||
catchError(this.handleError())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout function for both the client and the server.
|
||||
*
|
||||
* Will clear the current {@link OperatorService} and
|
||||
* send a `post`-requiest to `/users/logout/'`
|
||||
*/
|
||||
//logout the user
|
||||
//TODO not yet used
|
||||
logout(): Observable<any> {
|
||||
this.operator.clear();
|
||||
return this.http.post<any>('/users/logout/', {}, httpOptions);
|
||||
}
|
||||
|
||||
//very generic error handling function.
|
||||
//implicitly returns an observable that will display an error message
|
||||
private handleError<T>() {
|
||||
return (error: any): Observable<T> => {
|
||||
console.error(error);
|
||||
return of(error);
|
||||
};
|
||||
return this.http.post<any>('/users/logout/', {});
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BaseComponent } from 'app/base.component';
|
||||
import { WebsocketService } from './websocket.service';
|
||||
|
||||
import { BaseModel } from 'app/core/models/baseModel';
|
||||
import { OpenSlidesComponent } from 'app/openslides.component';
|
||||
import { WebsocketService } from './websocket.service';
|
||||
// the Models
|
||||
import { Item } from 'app/core/models/agenda/item';
|
||||
import { Assignment } from 'app/core/models/assignments/assignment';
|
||||
import { ChatMessage } from 'app/core/models/core/chat-message';
|
||||
@ -23,109 +23,118 @@ import { PersonalNote } from 'app/core/models/users/personal-note';
|
||||
import { User } from 'app/core/models/users/user';
|
||||
|
||||
/**
|
||||
* Basically handles the inital update and all automatic updates.
|
||||
* Handles the initial update and automatic updates using the {@link WebsocketService}
|
||||
* Incoming objects, usually BaseModels, will be saved in the dataStore (`this.DS`)
|
||||
* This service usually creates all models
|
||||
*
|
||||
* The dataStore will injected over the parent class: {@link OpenSlidesComponent}.
|
||||
*/
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AutoupdateService extends BaseComponent {
|
||||
export class AutoupdateService extends OpenSlidesComponent {
|
||||
/**
|
||||
* Stores the to create the socket created using {@link WebsocketService}.
|
||||
*/
|
||||
private socket;
|
||||
|
||||
/**
|
||||
* Constructor to create the AutoupdateService. Calls the constructor of the parent class.
|
||||
* @param websocketService
|
||||
*/
|
||||
constructor(private websocketService: WebsocketService) {
|
||||
super();
|
||||
}
|
||||
|
||||
// initialte autpupdate Service
|
||||
/**
|
||||
* Function to start the automatic update process
|
||||
* will build up a websocket connection using {@link WebsocketService}
|
||||
*/
|
||||
startAutoupdate(): void {
|
||||
console.log('start autoupdate');
|
||||
this.socket = this.websocketService.connect();
|
||||
this.socket.subscribe(response => {
|
||||
this.storeResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
// create models out of socket answer
|
||||
/**
|
||||
* Handle the answer of incoming data via {@link WebsocketService}.
|
||||
*
|
||||
* Detects the Class of an incomming model, creates a new empty object and assigns
|
||||
* the data to it using the deserialize function.
|
||||
*
|
||||
* Saves models in DataStore.
|
||||
*/
|
||||
storeResponse(socketResponse): void {
|
||||
socketResponse.forEach(model => {
|
||||
switch (model.collection) {
|
||||
case 'core/projector': {
|
||||
this.DS.add(BaseModel.fromJSON(model.data, Projector));
|
||||
break;
|
||||
}
|
||||
case 'core/chat-message': {
|
||||
this.DS.add(BaseModel.fromJSON(model.data, ChatMessage));
|
||||
break;
|
||||
}
|
||||
case 'core/tag': {
|
||||
this.DS.add(BaseModel.fromJSON(model.data, Tag));
|
||||
break;
|
||||
}
|
||||
case 'core/projector-message': {
|
||||
this.DS.add(BaseModel.fromJSON(model.data, ProjectorMessage));
|
||||
break;
|
||||
}
|
||||
case 'core/countdown': {
|
||||
this.DS.add(BaseModel.fromJSON(model.data, Countdown));
|
||||
break;
|
||||
}
|
||||
case 'core/config': {
|
||||
this.DS.add(BaseModel.fromJSON(model.data, Config));
|
||||
break;
|
||||
}
|
||||
case 'users/user': {
|
||||
this.DS.add(BaseModel.fromJSON(model.data, User));
|
||||
break;
|
||||
}
|
||||
case 'users/group': {
|
||||
this.DS.add(BaseModel.fromJSON(model.data, Group));
|
||||
break;
|
||||
}
|
||||
case 'users/personal-note': {
|
||||
this.DS.add(BaseModel.fromJSON(model.data, PersonalNote));
|
||||
break;
|
||||
}
|
||||
case 'agenda/item': {
|
||||
this.DS.add(BaseModel.fromJSON(model.data, Item));
|
||||
break;
|
||||
}
|
||||
case 'topics/topic': {
|
||||
this.DS.add(BaseModel.fromJSON(model.data, Topic));
|
||||
break;
|
||||
}
|
||||
case 'motions/category': {
|
||||
this.DS.add(BaseModel.fromJSON(model.data, Category));
|
||||
break;
|
||||
}
|
||||
case 'motions/motion': {
|
||||
this.DS.add(BaseModel.fromJSON(model.data, Motion));
|
||||
break;
|
||||
}
|
||||
case 'motions/motion-block': {
|
||||
this.DS.add(BaseModel.fromJSON(model.data, MotionBlock));
|
||||
break;
|
||||
}
|
||||
case 'motions/workflow': {
|
||||
this.DS.add(BaseModel.fromJSON(model.data, Workflow));
|
||||
break;
|
||||
}
|
||||
case 'motions/motion-change-recommendation': {
|
||||
this.DS.add(BaseModel.fromJSON(model.data, MotionChangeReco));
|
||||
break;
|
||||
}
|
||||
case 'assignments/assignment': {
|
||||
this.DS.add(BaseModel.fromJSON(model.data, Assignment));
|
||||
break;
|
||||
}
|
||||
case 'mediafiles/mediafile': {
|
||||
this.DS.add(BaseModel.fromJSON(model.data, Mediafile));
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.error('No rule for ', model.collection, '\n object was: ', model);
|
||||
break;
|
||||
}
|
||||
}
|
||||
socketResponse.forEach(jsonObj => {
|
||||
const targetClass = this.getClassFromCollectionString(jsonObj.collection);
|
||||
this.DS.add(new targetClass().deserialize(jsonObj.data));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* helper function to return the correct class from a collection string
|
||||
*/
|
||||
getClassFromCollectionString(collection: string): any {
|
||||
switch (collection) {
|
||||
case 'core/projector': {
|
||||
return Projector;
|
||||
}
|
||||
case 'core/chat-message': {
|
||||
return ChatMessage;
|
||||
}
|
||||
case 'core/tag': {
|
||||
return Tag;
|
||||
}
|
||||
case 'core/projector-message': {
|
||||
return ProjectorMessage;
|
||||
}
|
||||
case 'core/countdown': {
|
||||
return Countdown;
|
||||
}
|
||||
case 'core/config': {
|
||||
return Config;
|
||||
}
|
||||
case 'users/user': {
|
||||
return User;
|
||||
}
|
||||
case 'users/group': {
|
||||
return Group;
|
||||
}
|
||||
case 'users/personal-note': {
|
||||
return PersonalNote;
|
||||
}
|
||||
case 'agenda/item': {
|
||||
return Item;
|
||||
}
|
||||
case 'topics/topic': {
|
||||
return Topic;
|
||||
}
|
||||
case 'motions/category': {
|
||||
return Category;
|
||||
}
|
||||
case 'motions/motion': {
|
||||
return Motion;
|
||||
}
|
||||
case 'motions/motion-block': {
|
||||
return MotionBlock;
|
||||
}
|
||||
case 'motions/workflow': {
|
||||
return Workflow;
|
||||
}
|
||||
case 'motions/motion-change-recommendation': {
|
||||
return MotionChangeReco;
|
||||
}
|
||||
case 'assignments/assignment': {
|
||||
return Assignment;
|
||||
}
|
||||
case 'mediafiles/mediafile': {
|
||||
return Mediafile;
|
||||
}
|
||||
default: {
|
||||
console.error('No rule for ', collection);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { DataStoreService } from './DS.service';
|
||||
import { DataStoreService } from './dataStore.service';
|
||||
|
||||
describe('DS', () => {
|
||||
beforeEach(() => {
|
||||
@ -8,9 +8,4 @@ describe('DS', () => {
|
||||
providers: [DataStoreService]
|
||||
});
|
||||
});
|
||||
|
||||
/*it('should be created', inject([DSService], (DS: DSService) => {
|
||||
expect(DS).toBeTruthy();
|
||||
}));*/
|
||||
// just a static use
|
||||
});
|
211
client/src/app/core/services/dataStore.service.ts
Normal file
211
client/src/app/core/services/dataStore.service.ts
Normal file
@ -0,0 +1,211 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpResponse, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable, of, BehaviorSubject } from 'rxjs';
|
||||
import { tap, map } from 'rxjs/operators';
|
||||
|
||||
import { ImproperlyConfiguredError } from 'app/core/exceptions';
|
||||
import { BaseModel, ModelId } from 'app/core/models/base-model';
|
||||
|
||||
/**
|
||||
* represents a collection on the Django server, uses an ID to access a {@link BaseModel}.
|
||||
*
|
||||
* Part of {@link DataStoreService}
|
||||
*/
|
||||
interface Collection {
|
||||
[id: number]: BaseModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* The actual storage that stores collections, accessible by strings.
|
||||
*
|
||||
* {@link DataStoreService}
|
||||
*/
|
||||
interface Storage {
|
||||
[collectionString: string]: Collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* All mighty DataStore that comes with all OpenSlides components.
|
||||
* Use this.DS in an OpenSlides Component to Access the store.
|
||||
* Used by a lot of components, classes and services.
|
||||
* Changes can be observed
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DataStoreService {
|
||||
/**
|
||||
* Dependency injection, services are singletons 'per scope' and not per app anymore.
|
||||
* There will be multiple DataStores, all of them should share the same storage object
|
||||
*/
|
||||
private static store: Storage = {};
|
||||
|
||||
/**
|
||||
* Observable subject with changes to enable dynamic changes in models and views
|
||||
*/
|
||||
private static dataStoreSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
|
||||
|
||||
/**
|
||||
* Empty constructor for dataStore
|
||||
* @param http use HttpClient to send models back to the server
|
||||
*/
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
/**
|
||||
* Read one, multiple or all ID's from dataStore
|
||||
* @param collectionType The desired BaseModel or collectionString to be read from the dataStore
|
||||
* @param ids An or multiple IDs or a list of IDs of BaseModel
|
||||
* @return The BaseModel-list corresponding to the given ID(s)
|
||||
* @example: this.DS.get(User) returns all users
|
||||
* @example: this.DS.get(User, 1)
|
||||
* @example: this.DS.get(User, ...[1,2,3,4,5])
|
||||
* @example: this.DS.get(/core/countdown, 1)
|
||||
*/
|
||||
get(collectionType, ...ids: ModelId[]): BaseModel[] | BaseModel {
|
||||
let collectionString: string;
|
||||
if (typeof collectionType === 'string') {
|
||||
collectionString = collectionType;
|
||||
} else {
|
||||
//get the collection string by making an empty object
|
||||
const tempObject = new collectionType();
|
||||
collectionString = tempObject.collectionString;
|
||||
}
|
||||
|
||||
const collection: Collection = DataStoreService.store[collectionString];
|
||||
|
||||
const models = [];
|
||||
if (!collection) {
|
||||
return models;
|
||||
}
|
||||
|
||||
if (ids.length === 0) {
|
||||
return Object.values(collection);
|
||||
} else {
|
||||
ids.forEach(id => {
|
||||
const model: BaseModel = collection[id];
|
||||
models.push(model);
|
||||
});
|
||||
}
|
||||
return models.length === 1 ? models[0] : models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the whole dataStore
|
||||
*/
|
||||
printWhole(): void {
|
||||
console.log('Everything in DataStore: ', DataStoreService.store);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the dataStore by type
|
||||
* @param Type The desired BaseModel type to be read from the dataStore
|
||||
* @param callback a filter function
|
||||
* @return The BaseModel-list corresponding to the filter function
|
||||
* @example this.DS.filter(User, myUser => myUser.first_name === "Max")
|
||||
*/
|
||||
filter(Type, callback): BaseModel[] {
|
||||
// TODO: type for callback function
|
||||
let filterCollection = [];
|
||||
const typeCollection = this.get(Type);
|
||||
|
||||
if (Array.isArray(typeCollection)) {
|
||||
filterCollection = [...filterCollection, ...typeCollection];
|
||||
} else {
|
||||
filterCollection.push(typeCollection);
|
||||
}
|
||||
return filterCollection.filter(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one or multiple models to dataStore
|
||||
* @param ...models The model(s) that shall be add use spread operator ("...")
|
||||
* @example this.DS.add(new User(1))
|
||||
* @example this.DS.add((new User(2), new User(3)))
|
||||
* @example this.DS.add(...arrayWithUsers)
|
||||
*/
|
||||
add(...models: BaseModel[]): void {
|
||||
models.forEach(model => {
|
||||
const collectionString = model.collectionString;
|
||||
if (!model.id) {
|
||||
throw new ImproperlyConfiguredError('The model must have an id!');
|
||||
} else if (collectionString === 'invalid-collection-string') {
|
||||
throw new ImproperlyConfiguredError('Cannot save a BaseModel');
|
||||
}
|
||||
if (typeof DataStoreService.store[collectionString] === 'undefined') {
|
||||
DataStoreService.store[collectionString] = {};
|
||||
}
|
||||
DataStoreService.store[collectionString][model.id] = model;
|
||||
this.setObservable(model);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* removes one or multiple models from dataStore
|
||||
* @param Type The desired BaseModel type to be read from the dataStore
|
||||
* @param ...ids An or multiple IDs or a list of IDs of BaseModels. use spread operator ("...") for arrays
|
||||
* @example this.DS.remove(User, myUser.id, 3, 4)
|
||||
*/
|
||||
remove(Type, ...ids: ModelId[]): void {
|
||||
ids.forEach(id => {
|
||||
const tempObject = new Type();
|
||||
if (DataStoreService.store[tempObject.collectionString]) {
|
||||
delete DataStoreService.store[tempObject.collectionString][id];
|
||||
console.log(`did remove "${id}" from Datastore "${tempObject.collectionString}"`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the given model on the server
|
||||
* @param model the BaseModel that shall be removed
|
||||
* @return Observable of BaseModel
|
||||
*/
|
||||
save(model: BaseModel): Observable<BaseModel> {
|
||||
if (!model.id) {
|
||||
throw new ImproperlyConfiguredError('The model must have an id!');
|
||||
}
|
||||
|
||||
// TODO not tested
|
||||
return this.http.post<BaseModel>(model.collectionString + '/', model).pipe(
|
||||
tap(response => {
|
||||
console.log('the response: ', response);
|
||||
this.add(model);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given model on the server
|
||||
* @param model the BaseModel that shall be removed
|
||||
* @return Observable of BaseModel
|
||||
*/
|
||||
delete(model: BaseModel): Observable<BaseModel> {
|
||||
if (!model.id) {
|
||||
throw new ImproperlyConfiguredError('The model must have an id!');
|
||||
}
|
||||
|
||||
// TODO not tested
|
||||
return this.http.post<BaseModel>(model.collectionString + '/', model).pipe(
|
||||
tap(response => {
|
||||
console.log('the response: ', response);
|
||||
this.remove(model, model.id);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe the dataStore for changes.
|
||||
* @return an observable behaviorSubject
|
||||
*/
|
||||
public getObservable(): Observable<any> {
|
||||
return DataStoreService.dataStoreSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Informs the observers for changes
|
||||
* @param value the change that have been made
|
||||
*/
|
||||
private setObservable(value): void {
|
||||
DataStoreService.dataStoreSubject.next(value);
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { OpenslidesService } from './openslides.service';
|
||||
|
||||
describe('OpenslidesService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [OpenslidesService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([OpenslidesService], (service: OpenslidesService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
@ -1,24 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class OpenslidesService {
|
||||
constructor(private auth: AuthService, private router: Router) {}
|
||||
|
||||
// now in authService
|
||||
// bootup() {
|
||||
// // TODO Lock the interface..
|
||||
// this.auth.init().subscribe(whoami => {
|
||||
// console.log(whoami);
|
||||
// if (!whoami.user && !whoami.guest_enabled) {
|
||||
// this.router.navigate(['/login']);
|
||||
// } else {
|
||||
// // It's ok!
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
}
|
@ -1,23 +1,25 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, of, BehaviorSubject } from 'rxjs';
|
||||
import { HttpClient, HttpResponse, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable, BehaviorSubject } from 'rxjs';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { tap, catchError, share } from 'rxjs/operators';
|
||||
import { BaseComponent } from 'app/base.component';
|
||||
import { OpenSlidesComponent } from 'app/openslides.component';
|
||||
import { Group } from 'app/core/models/users/group';
|
||||
|
||||
// TODO: Dry
|
||||
const httpOptions = {
|
||||
withCredentials: true,
|
||||
headers: new HttpHeaders({
|
||||
'Content-Type': 'application/json'
|
||||
})
|
||||
};
|
||||
|
||||
/**
|
||||
* The operator represents the user who is using OpenSlides.
|
||||
*
|
||||
* Information is mostly redundant to user but has different purposes.
|
||||
* Changes in operator can be observed, directives do so on order to show
|
||||
* or hide certain information.
|
||||
*
|
||||
* Could extend User?
|
||||
*
|
||||
* The operator is an {@link OpenSlidesComponent}.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class OperatorService extends BaseComponent {
|
||||
// default variables
|
||||
export class OperatorService extends OpenSlidesComponent {
|
||||
about_me: string;
|
||||
comment: string;
|
||||
default_password: string;
|
||||
@ -35,11 +37,23 @@ export class OperatorService extends BaseComponent {
|
||||
title: string;
|
||||
username: string;
|
||||
logged_in: boolean;
|
||||
// subject
|
||||
|
||||
/**
|
||||
* The subject that can be observed by other instances using observing functions.
|
||||
*/
|
||||
private operatorSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
|
||||
// real groups, once they arrived in datastore
|
||||
|
||||
/**
|
||||
* Representation of the {@link Group}s that the operator has (in contrast the the `groups_id`-Array)
|
||||
*
|
||||
* The operator observes the dataStore (compare {@link OpenSlidesComponent} in Order to know it's groups)
|
||||
*/
|
||||
private groups: Group[] = new Array();
|
||||
|
||||
/**
|
||||
* Recreates the operator from localStorage if it's found and starts to observe the dataStore.
|
||||
* @param http HttpClient
|
||||
*/
|
||||
constructor(private http: HttpClient) {
|
||||
super();
|
||||
|
||||
@ -56,9 +70,11 @@ export class OperatorService extends BaseComponent {
|
||||
this.observeDataStore();
|
||||
}
|
||||
|
||||
// calls 'whoami' to find out the operator
|
||||
/**
|
||||
* calls `/users/whoami` to find out the real operator
|
||||
*/
|
||||
public whoAmI(): Observable<any> {
|
||||
return this.http.get<any>('/users/whoami/', httpOptions).pipe(
|
||||
return this.http.get<any>('/users/whoami/').pipe(
|
||||
tap(whoami => {
|
||||
if (whoami && whoami.user) {
|
||||
this.storeUser(whoami.user);
|
||||
@ -68,6 +84,10 @@ export class OperatorService extends BaseComponent {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the user Information in the operator, the localStorage and update the Observable
|
||||
* @param user usually a http response that represents a user.
|
||||
*/
|
||||
public storeUser(user: any): void {
|
||||
// store in file
|
||||
this.about_me = user.about_me;
|
||||
@ -87,11 +107,18 @@ export class OperatorService extends BaseComponent {
|
||||
this.title = user.title;
|
||||
this.username = user.username;
|
||||
// also store in localstorrage
|
||||
this.updateLocalStorrage();
|
||||
this.updateLocalStorage();
|
||||
// update mode to inform observers
|
||||
this.updateMode();
|
||||
this.setObservable(this.getUpdateObject());
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all stored information about the Operator.
|
||||
*
|
||||
* The Opposite of StoreUser. Usually a `logout()`-function.
|
||||
* Also removes the operator from localStorrage and
|
||||
* updates the observable.
|
||||
*/
|
||||
public clear() {
|
||||
this.about_me = null;
|
||||
this.comment = null;
|
||||
@ -109,19 +136,25 @@ export class OperatorService extends BaseComponent {
|
||||
this.structure_level = null;
|
||||
this.title = null;
|
||||
this.username = null;
|
||||
this.updateMode();
|
||||
this.setObservable(this.getUpdateObject());
|
||||
localStorage.removeItem('operator');
|
||||
}
|
||||
|
||||
private updateLocalStorrage(): void {
|
||||
/**
|
||||
* Saves the operator in the localStorage for easier and faster re-login
|
||||
*
|
||||
* This is a mere comfort feature, even if the operator can be recreated
|
||||
* it has to pass `this.whoAmI()` during page access.
|
||||
*/
|
||||
private updateLocalStorage(): void {
|
||||
localStorage.setItem('operator', JSON.stringify(this.getUpdateObject()));
|
||||
console.log('update local storrage: groups: ', this.groups_id);
|
||||
}
|
||||
|
||||
private updateMode(): void {
|
||||
this.setObservable(this.getUpdateObject());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current operator.
|
||||
*
|
||||
* Used to save the operator in localStorage or inform observers.
|
||||
*/
|
||||
private getUpdateObject(): any {
|
||||
return {
|
||||
about_me: this.about_me,
|
||||
@ -144,10 +177,12 @@ export class OperatorService extends BaseComponent {
|
||||
};
|
||||
}
|
||||
|
||||
// observe dataStore to set groups once they are there
|
||||
// TODO logic to remove groups / user from certain groups
|
||||
/**
|
||||
* Observe dataStore to set groups once they are loaded.
|
||||
*
|
||||
* TODO logic to remove groups / user from certain groups. Currently is is only set and was never removed
|
||||
*/
|
||||
private observeDataStore(): void {
|
||||
console.log('Operator observes DataStore');
|
||||
this.DS.getObservable().subscribe(newModel => {
|
||||
if (newModel instanceof Group) {
|
||||
this.addGroup(newModel);
|
||||
@ -155,9 +190,15 @@ export class OperatorService extends BaseComponent {
|
||||
});
|
||||
}
|
||||
|
||||
// read out the Groups from the DataStore by the operators 'groups_id'
|
||||
// requires that the DataStore has been setup (websocket.service)
|
||||
// requires that the whoAmI did return a valid operator
|
||||
/**
|
||||
* Read out the Groups from the DataStore by the operators 'groups_id'
|
||||
*
|
||||
* requires that the DataStore has been setup (websocket.service)
|
||||
* requires that the whoAmI did return a valid operator
|
||||
*
|
||||
* This is the normal behavior after a fresh login, everythin else can
|
||||
* be done by observers.
|
||||
*/
|
||||
public readGroupsFromStore(): void {
|
||||
this.DS.filter(Group, myGroup => {
|
||||
if (this.groups_id.includes(myGroup.id)) {
|
||||
@ -166,33 +207,40 @@ export class OperatorService extends BaseComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the behaviorSubject as an observable.
|
||||
*
|
||||
* Services an components can use it to get informed when something changes in
|
||||
* the operator
|
||||
*/
|
||||
public getObservable() {
|
||||
return this.operatorSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inform all observers about changes
|
||||
* @param value
|
||||
*/
|
||||
private setObservable(value) {
|
||||
this.operatorSubject.next(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the (real) {@link Group}s
|
||||
*/
|
||||
public getGroups() {
|
||||
return this.groups;
|
||||
}
|
||||
|
||||
// if the operator has the corresponding ID, set the group
|
||||
/**
|
||||
* if the operator has the corresponding ID, set the group
|
||||
* @param newGroup potential group that the operator has.
|
||||
*/
|
||||
private addGroup(newGroup: Group): void {
|
||||
if (this.groups_id.includes(newGroup.id)) {
|
||||
if (this.groups_id.includes(newGroup.id as number)) {
|
||||
this.groups.push(newGroup);
|
||||
// inform the observers about new groups (appOsPerms)
|
||||
console.log('pushed a group into operator');
|
||||
this.setObservable(newGroup);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Dry
|
||||
private handleError<T>() {
|
||||
return (error: any): Observable<T> => {
|
||||
console.error(error);
|
||||
return of(error);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +0,0 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { ToastService } from './toast.service';
|
||||
|
||||
describe('ToastService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [ToastService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([ToastService], (service: ToastService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
@ -1,46 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
|
||||
import { Alert, AlertType } from 'app/core/models/alert';
|
||||
|
||||
/**TODO Drafted for now. Since the UI is not done yet, this might be replaced or disappear entirely.
|
||||
* Furtermore, Material UI does not support these kinds of alerts
|
||||
*/
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ToastService {
|
||||
private subject = new Subject<Alert>();
|
||||
|
||||
constructor() {}
|
||||
|
||||
getToast(): Observable<any> {
|
||||
return this.subject.asObservable();
|
||||
}
|
||||
|
||||
success(message: string) {
|
||||
this.alert(AlertType.Success, message);
|
||||
}
|
||||
|
||||
error(message: string) {
|
||||
this.alert(AlertType.Error, message);
|
||||
}
|
||||
|
||||
info(message: string) {
|
||||
this.alert(AlertType.Info, message);
|
||||
}
|
||||
|
||||
warn(message: string) {
|
||||
this.alert(AlertType.Warning, message);
|
||||
}
|
||||
|
||||
alert(type: AlertType, message: string) {
|
||||
this.subject.next(<Alert>{ type: type, message: message });
|
||||
}
|
||||
|
||||
clear() {
|
||||
// clear alerts
|
||||
this.subject.next();
|
||||
}
|
||||
}
|
@ -2,17 +2,33 @@ import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
||||
|
||||
/**
|
||||
* Service that handles WebSocket connections.
|
||||
*
|
||||
* Creates or returns already created WebSockets.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class WebsocketService {
|
||||
/**
|
||||
* Constructor that handles the router
|
||||
* @param router the URL Router
|
||||
*/
|
||||
constructor(private router: Router) {}
|
||||
|
||||
//might be any for simplicity or MessageEvent or something different
|
||||
/**
|
||||
* Observable subject that might be `any` for simplicity, `MessageEvent` or something appropriate
|
||||
*/
|
||||
private subject: WebSocketSubject<any>;
|
||||
|
||||
/**
|
||||
* Creates a new WebSocket connection as WebSocketSubject
|
||||
*
|
||||
* Can return old Subjects to prevent multiple WebSocket connections.
|
||||
*/
|
||||
public connect(): WebSocketSubject<any> {
|
||||
const socketProtocol = this.getWebSocketProtocoll();
|
||||
const socketProtocol = this.getWebSocketProtocol();
|
||||
const socketPath = this.getWebSocketPath();
|
||||
const socketServer = window.location.hostname + ':' + window.location.port;
|
||||
if (!this.subject) {
|
||||
@ -21,7 +37,9 @@ export class WebsocketService {
|
||||
return this.subject;
|
||||
}
|
||||
|
||||
// delegates to websockets for either the side or projector websocket
|
||||
/**
|
||||
* Delegates to socket-path for either the side or projector websocket.
|
||||
*/
|
||||
private getWebSocketPath(): string {
|
||||
//currentRoute does not end with '/'
|
||||
const currentRoute = this.router.url;
|
||||
@ -32,8 +50,12 @@ export class WebsocketService {
|
||||
}
|
||||
}
|
||||
|
||||
// returns the websocket protocoll
|
||||
private getWebSocketProtocoll(): string {
|
||||
/**
|
||||
* returns the desired websocket protocol
|
||||
*
|
||||
* TODO: HTTPS is not yet tested
|
||||
*/
|
||||
private getWebSocketProtocol(): string {
|
||||
if (location.protocol === 'https') {
|
||||
return 'wss://';
|
||||
} else {
|
||||
|
54
client/src/app/openslides.component.ts
Normal file
54
client/src/app/openslides.component.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { Injector } from '@angular/core';
|
||||
import { Observable, of } from 'rxjs';
|
||||
|
||||
import { DataStoreService } from 'app/core/services/dataStore.service';
|
||||
|
||||
/**
|
||||
* injects the {@link DataStoreService} to all its children and provides a generic function to catch errors
|
||||
* should be abstract and a mere parent to all {@link DataStoreService} accessors
|
||||
*/
|
||||
export abstract class OpenSlidesComponent {
|
||||
/**
|
||||
* To inject the {@link DataStoreService} into the children of OpenSlidesComponent
|
||||
*/
|
||||
protected injector: Injector;
|
||||
|
||||
/**
|
||||
* The dataStore Service
|
||||
*/
|
||||
protected dataStore: DataStoreService;
|
||||
|
||||
/**
|
||||
* Empty constructor
|
||||
*
|
||||
* Static injection of {@link DataStoreService} in all child instances of OpenSlidesComponent
|
||||
* Throws a warning even tho it is the new syntax. Ignored for now.
|
||||
*/
|
||||
constructor() {
|
||||
this.injector = Injector.create([{ provide: DataStoreService, useClass: DataStoreService, deps: [] }]);
|
||||
}
|
||||
|
||||
/**
|
||||
* getter to access the {@link DataStoreService}
|
||||
* @example this.DS.get(User)
|
||||
* @return access to dataStoreService
|
||||
*/
|
||||
get DS(): DataStoreService {
|
||||
if (this.dataStore == null) {
|
||||
this.dataStore = this.injector.get(DataStoreService);
|
||||
}
|
||||
return this.dataStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic error handling for everything that makes HTTP Calls
|
||||
* TODO: could have more features
|
||||
* @return an observable error
|
||||
*/
|
||||
handleError<T>() {
|
||||
return (error: any): Observable<T> => {
|
||||
console.error(error);
|
||||
return of(error);
|
||||
};
|
||||
}
|
||||
}
|
3762
package-lock.json
generated
3762
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user