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:
Sean Engelhardt 2018-07-12 14:11:31 +02:00 committed by FinnStutzenstein
parent 30ac9c8e36
commit 6b09427565
64 changed files with 4403 additions and 2649 deletions

1
.gitignore vendored
View File

@ -43,6 +43,7 @@ openslides_*
client/dist client/dist
client/tmp client/tmp
client/out-tsc client/out-tsc
client/documentation
# dependencies # dependencies
client/node_modules client/node_modules

View File

@ -1,12 +1,46 @@
# OpenSlides 3 Client # 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 Start OpenSlides as usual using
`python manage.py start --no-browser --host 0.0.0.0` `python manage.py start --no-browser --host 0.0.0.0`

949
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,11 +3,12 @@
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"ng": "ng", "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", "build": "ng build",
"test": "ng test", "test": "ng test",
"lint": "ng lint", "lint": "ng lint",
"e2e": "ng e2e", "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", "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", "format:fix": "pretty-quick --staged",
"precommit": "run-s format:fix lint" "precommit": "run-s format:fix lint"
@ -40,6 +41,7 @@
"@angular/compiler-cli": "^6.0.6", "@angular/compiler-cli": "^6.0.6",
"@angular/language-service": "^6.0.6", "@angular/language-service": "^6.0.6",
"@biesbjerg/ngx-translate-extract": "^2.3.4", "@biesbjerg/ngx-translate-extract": "^2.3.4",
"@compodoc/compodoc": "^1.1.3",
"@types/jasmine": "~2.8.6", "@types/jasmine": "~2.8.6",
"@types/jasminewd2": "~2.0.3", "@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4", "@types/node": "~8.9.4",

View File

@ -1,16 +1,23 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { AutoupdateService } from 'app/core/services/autoupdate.service'; import { AutoupdateService } from 'app/core/services/autoupdate.service';
// import { AuthService } from 'app/core/services/auth.service';
import { OperatorService } from 'app/core/services/operator.service'; import { OperatorService } from 'app/core/services/operator.service';
/**
* Angular's global App Component
*/
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.css'] styleUrls: ['./app.component.css']
}) })
// export class AppComponent implements OnInit {
export class AppComponent { 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( constructor(
private operator: OperatorService, private operator: OperatorService,
private autoupdate: AutoupdateService, private autoupdate: AutoupdateService,

View File

@ -3,7 +3,7 @@ import { BrowserModule, Title } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms'; 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 // MaterialUI modules
import { import {
@ -34,15 +34,26 @@ import { MotionsComponent } from './site/motions/motions.component';
import { AgendaComponent } from './site/agenda/agenda.component'; import { AgendaComponent } from './site/agenda/agenda.component';
import { SiteComponent } from './site/site.component'; import { SiteComponent } from './site/site.component';
import { StartComponent } from './site/start/start.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 { 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 { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { PruningTranslationLoader } from './core/pruning-loader'; import { PruningTranslationLoader } from './core/pruning-loader';
import { OsPermsDirective } from './core/directives/os-perms.directive'; 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) { export function HttpLoaderFactory(http: HttpClient) {
return new PruningTranslationLoader(http); return new PruningTranslationLoader(http);
} }
@ -61,7 +72,6 @@ library.add(fas);
SiteComponent, SiteComponent,
StartComponent, StartComponent,
ProjectorContainerComponent, ProjectorContainerComponent,
AlertComponent,
OsPermsDirective OsPermsDirective
], ],
imports: [ imports: [
@ -94,7 +104,20 @@ library.add(fas);
}), }),
AppRoutingModule AppRoutingModule
], ],
providers: [Title, WebsocketService], providers: [
Title,
AuthGuard,
AuthService,
AutoupdateService,
DataStoreService,
OperatorService,
WebsocketService,
{
provide: HTTP_INTERCEPTORS,
useClass: AddHeaderInterceptor,
multi: true
}
],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })
export class AppModule {} export class AppModule {}

View File

@ -1,28 +1,35 @@
import { Injector } from '@angular/core'; import { Injector } from '@angular/core';
import { Title } from '@angular/platform-browser'; 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 { * Provides functionalities that will be used by most components
protected injector: Injector; * currently able to set the title with the suffix ' - OpenSlides 3'
protected dataStore: DataStoreService; *
* 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'; private titleSuffix = ' - OpenSlides 3';
/**
* Child constructor that implements the titleServices and calls Super from OpenSlidesComponent
*/
constructor(protected titleService?: Title) { constructor(protected titleService?: Title) {
// throws a warning even tho it is the new syntax. Ignored for now. super();
this.injector = Injector.create([{ provide: DataStoreService, useClass: DataStoreService, deps: [] }]);
} }
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); 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;
}
} }

View File

@ -1,4 +0,0 @@
<div *ngIf="alert" [ngClass]="cssClass(alert)" class="alert-dismissable">
{{alert.message}}
<a class="close" (click)="removeAlert(alert)">&times;</a>
</div>

View File

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

View File

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

View File

@ -1,19 +1,40 @@
import { Directive, Input, ElementRef, TemplateRef, ViewContainerRef, OnInit } from '@angular/core'; import { Directive, Input, ElementRef, TemplateRef, ViewContainerRef, OnInit } from '@angular/core';
import { OperatorService } from 'app/core/services/operator.service'; 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'; 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({ @Directive({
selector: '[appOsPerms]' selector: '[appOsPerms]'
}) })
export class OsPermsDirective extends BaseComponent { export class OsPermsDirective extends OpenSlidesComponent {
/**
* Holds the {@link OperatorService} permissions
*/
private userPermissions: string[]; private userPermissions: string[];
/**
* Holds the required permissions the access a feature
*/
private permissions; 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( constructor(
private element: ElementRef,
private template: TemplateRef<any>, private template: TemplateRef<any>,
private viewContainer: ViewContainerRef, //TODO private operator. OperatorService private viewContainer: ViewContainerRef,
private operator: OperatorService private operator: OperatorService
) { ) {
super(); super();
@ -30,6 +51,10 @@ export class OsPermsDirective extends BaseComponent {
}); });
} }
/**
* Comes directly from the view.
* The value defines the requires permissions.
*/
@Input() @Input()
set appOsPerms(value) { set appOsPerms(value) {
this.permissions = value; this.permissions = value;
@ -37,6 +62,10 @@ export class OsPermsDirective extends BaseComponent {
this.updateView(); this.updateView();
} }
/**
* Updates the local `userPermissions[]` by the permissions found in the operators groups
* Will just set, but not remove them.
*/
private readUserPermissions(): void { private readUserPermissions(): void {
const opGroups = this.operator.getGroups(); const opGroups = this.operator.getGroups();
console.log('operator Groups: ', opGroups); 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 { private updateView(): void {
if (this.checkPermissions()) { if (this.checkPermissions()) {
// will just render the page normally // 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 { private checkPermissions(): boolean {
let isPermitted = false; let isPermitted = false;
if (this.userPermissions && this.permissions) { if (this.userPermissions && this.permissions) {

View File

@ -1,4 +0,0 @@
<div *ngFor="let toast of toasts" class="{{ cssClass(toast) }} alert-dismissable">
{{toast.message}}
<a class="close" (click)="removeAlert(toast)">&times;</a>
</div>

View File

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

View File

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

View File

@ -1,4 +1,11 @@
/**
* custom exception that indicated that a collectionString is invalid.
*/
export class ImproperlyConfiguredError extends Error { export class ImproperlyConfiguredError extends Error {
/**
* Default Constructor for Errors
* @param m The Error Message
*/
constructor(m: string) { constructor(m: string) {
super(m); super(m);
} }

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

View 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;
}
}

View File

@ -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 { export class Item extends BaseModel {
static collectionString = 'agenda/item'; protected _collectionString: string;
id: number; id: number;
closed: boolean;
comment: string;
content_object: Object;
duration: number; //time?
is_hidden: boolean;
item_number: string; 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; title: string;
list_view_title: string;
comment: string;
closed: boolean;
type: number; type: number;
is_hidden: boolean;
duration: number;
speakers: Speaker[];
speaker_list_closed: boolean;
content_object: ContentObject;
weight: number; weight: number;
parent_id: number;
constructor( constructor(
id: number, id?: number,
closed?: boolean,
comment?: string,
content_object?: Object,
duration?: number,
is_hidden?: boolean,
item_number?: string, item_number?: string,
list_view_title?: string,
parent_id?: number,
speaker_list_closed?: boolean,
speakers?: BaseModel[],
title?: string, title?: string,
list_view_title?: string,
comment?: string,
closed?: boolean,
type?: number, 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); super();
this.comment = comment; this._collectionString = 'agenda/item';
this.content_object = content_object; this.id = id;
this.duration = duration;
this.is_hidden = is_hidden;
this.item_number = item_number; 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.title = title;
this.list_view_title = list_view_title;
this.comment = comment;
this.closed = closed;
this.type = type; 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.weight = weight;
this.parent_id = parent_id;
} }
public getCollectionString(): string { getSpeakersAsUser(): BaseModel | BaseModel[] {
return Item.collectionString; 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;
} }
} }

View 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;
}
}

View File

@ -1,11 +0,0 @@
export class Alert {
type: AlertType;
message: string;
}
export enum AlertType {
Success,
Error,
Info,
Warning
}

View 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;
}
}

View File

@ -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 { export class Assignment extends BaseModel {
static collectionString = 'assignments/assignment'; protected _collectionString: string;
id: number; id: number;
agenda_item_id: number; title: string;
description: string; description: string;
open_posts: number; open_posts: number;
phase: number; phase: number;
assignment_related_users: AssignmentUser[];
poll_description_default: number; poll_description_default: number;
polls: Object[]; polls: Poll[];
agenda_item_id: number;
tags_id: number[]; tags_id: number[];
title: string;
constructor( constructor(
id: number, id?: number,
agenda_item_id?: number, title?: string,
description?: string, description?: string,
open_posts?: number, open_posts?: number,
phase?: number, phase?: number,
assignment_related_users?: AssignmentUser[],
poll_description_default?: number, poll_description_default?: number,
polls?: Object[], polls?: Poll[],
tags_id?: number[], agenda_item_id?: number,
title?: string tags_id?: number[]
) { ) {
super(id); super();
this._collectionString = 'assignments/assignment';
this.id = id; this.id = id;
this.agenda_item_id = agenda_item_id; this.title = title;
this.description = description; this.description = description;
this.open_posts = open_posts; this.open_posts = open_posts;
this.phase = phase; this.phase = phase;
this.assignment_related_users = assignment_related_users || []; //TODO Array
this.poll_description_default = poll_description_default; 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.tags_id = tags_id;
this.title = title;
} }
public getCollectionString(): string { getAssignmentReleatedUsers(): BaseModel | BaseModel[] {
return Assignment.collectionString; 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;
} }
} }

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

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

View File

@ -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 { export class ChatMessage extends BaseModel {
static collectionString = 'core/chat-message'; protected _collectionString: string;
id: number; id: number;
message: string; message: string;
timestamp: string; // TODO: Type for timestamp timestamp: string; // TODO: Type for timestamp
user_id: number; user_id: number;
constructor(id: number, message?: string, timestamp?: string, user_id?: number) { constructor(id?: number, message?: string, timestamp?: string, user_id?: number) {
super(id); super();
this._collectionString = 'core/chat-message';
this.id = id;
this.message = message; this.message = message;
this.timestamp = timestamp; this.timestamp = timestamp;
this.user_id = user_id;
} }
public getCollectionString(): string { getUser(): BaseModel | BaseModel[] {
return ChatMessage.collectionString; return this.DS.get('users/user', this.user_id);
} }
} }

View File

@ -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 { export class Config extends BaseModel {
static collectionString = 'core/config'; protected _collectionString: string;
id: number; id: number;
key: string; key: string;
value: Object; value: Object;
constructor(id: number, key?: string, value?: Object) { constructor(id?: number, key?: string, value?: Object) {
super(id); super();
this._collectionString = 'core/config';
this.id = id;
this.key = key; this.key = key;
this.value = value; this.value = value;
} }
public getCollectionString(): string {
return Config.collectionString;
}
} }

View File

@ -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 { export class Countdown extends BaseModel {
static collectionString = 'core/countdown'; protected _collectionString: string;
id: number; id: number;
countdown_time: number;
default_time: number;
description: string; description: string;
default_time: number;
countdown_time: number;
running: boolean;
constructor(id: number, countdown_time?: number, default_time?: number, description?: string) { constructor(id?: number, countdown_time?: number, default_time?: number, description?: string, running?: boolean) {
super(id); super();
this._collectionString = 'core/countdown';
this.id = id; this.id = id;
this.countdown_time = countdown_time;
this.default_time = default_time;
this.description = description; this.description = description;
} this.default_time = default_time;
this.countdown_time = countdown_time;
public getCollectionString(): string { this.running = running;
return Countdown.collectionString;
} }
} }

View File

@ -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 { export class ProjectorMessage extends BaseModel {
static collectionString = 'core/projector-message'; protected _collectionString: string;
id: number; id: number;
message: string; message: string;
constructor(id: number, message?: string) { constructor(id?: number, message?: string) {
super(id); super();
this._collectionString = 'core/projector-message';
this.id = id;
this.message = message; this.message = message;
} }
public getCollectionString(): string {
return ProjectorMessage.collectionString;
}
} }

View File

@ -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 { export class Projector extends BaseModel {
static collectionString = 'core/projector'; protected _collectionString: string;
id: number; id: number;
blank: boolean;
elements: Object; elements: Object;
height: number;
name: string;
projectiondefaults: BaseModel[];
scale: number; scale: number;
scroll: number; scroll: number;
name: string;
blank: boolean;
width: number; width: number;
height: number;
projectiondefaults: Object[];
constructor( constructor(
id: number, id?: number,
blank?: boolean,
elements?: Object, elements?: Object,
height?: number,
name?: string,
projectiondefaults?: BaseModel[],
scale?: number, scale?: number,
scroll?: number, scroll?: number,
width?: number name?: string,
blank?: boolean,
width?: number,
height?: number,
projectiondefaults?: Object[]
) { ) {
super(id); super();
this.blank = blank; this._collectionString = 'core/projector';
this.id = id;
this.elements = elements; this.elements = elements;
this.height = height;
this.name = name;
this.projectiondefaults = projectiondefaults;
this.scale = scale; this.scale = scale;
this.scroll = scroll; this.scroll = scroll;
this.name = name;
this.blank = blank;
this.width = width; this.width = width;
} this.height = height;
this.projectiondefaults = projectiondefaults;
public getCollectionString(): string {
return Projector.collectionString;
} }
} }

View File

@ -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 { export class Tag extends BaseModel {
static collectionString = 'core/tag'; protected _collectionString: string;
id: number; id: number;
name: string; name: string;
constructor(id: number, name?: string) { constructor(id?: number, name?: string) {
super(id); super();
this._collectionString = 'core/tag';
this.id = id;
this.name = name; this.name = name;
} }
public getCollectionString(): string {
return Tag.collectionString;
}
} }

View 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;
}

View 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;
}
}

View File

@ -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 { export class Mediafile extends BaseModel {
static collectionString = 'mediafiles/mediafile'; protected _collectionString: string;
id: number; id: number;
title: string;
mediafile: File;
media_url_prefix: string;
uploader_id: number;
filesize: string; filesize: string;
hidden: boolean; hidden: boolean;
media_url_prefix: string;
mediafile: Object;
timestamp: string; timestamp: string;
title: string;
uploader_id: number;
constructor( constructor(
id: number, id?: number,
title?: string,
mediafile?: File,
media_url_prefix?: string,
uploader_id?: number,
filesize?: string, filesize?: string,
hidden?: boolean, hidden?: boolean,
media_url_prefix?: string, timestamp?: string
mediafile?: Object,
timestamp?: string,
title?: string,
uploader_id?: number
) { ) {
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.filesize = filesize;
this.hidden = hidden; this.hidden = hidden;
this.media_url_prefix = media_url_prefix;
this.mediafile = mediafile;
this.timestamp = timestamp; this.timestamp = timestamp;
this.title = title;
this.uploader_id = uploader_id;
} }
public getCollectionString(): string { deserialize(input: any): this {
return Mediafile.collectionString; 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);
} }
} }

View File

@ -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 { export class Category extends BaseModel {
static collectionString = 'motions/category'; protected _collectionString: string;
id: number; id: number;
name: string; name: string;
prefix: string; prefix: string;
constructor(id: number, name?: string, prefix?: string) { constructor(id?: number, name?: string, prefix?: string) {
super(id); super();
this._collectionString = 'motions/category';
this.id = id;
this.name = name; this.name = name;
this.prefix = prefix; this.prefix = prefix;
} }
public getCollectionString(): string {
return Category.collectionString;
}
} }

View File

@ -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 { export class MotionBlock extends BaseModel {
static collectionString = 'motions/motion-block'; protected _collectionString: string;
id: number; id: number;
agenda_item_id: number;
title: string; title: string;
agenda_item_id: number;
constructor(id: number, agenda_item_id?: number, title?: string) { constructor(id?: number, title?: string, agenda_item_id?: number) {
super(id); super();
this.agenda_item_id = agenda_item_id; this._collectionString = 'motions/motion-block';
this.id = id;
this.title = title; this.title = title;
this.agenda_item_id = agenda_item_id;
} }
public getCollectionString(): string { getAgenda(): BaseModel | BaseModel[] {
return MotionBlock.collectionString; return this.DS.get('agenda/item', this.agenda_item_id);
} }
} }

View File

@ -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 { export class MotionChangeReco extends BaseModel {
static collectionString = 'motions/motion-change-recommendation'; protected _collectionString: string;
id: number; id: number;
creation_time: string; motion_version_id: number;
rejected: boolean;
type: number;
other_description: string;
line_from: number; line_from: number;
line_to: number; line_to: number;
motion_version_id: number;
other_description: string;
rejected: boolean;
text: string; text: string;
type: number; creation_time: string;
constructor( constructor(
id: number, id?: number,
creation_time?: string, motion_version_id?: number,
rejected?: boolean,
type?: number,
other_description?: string,
line_from?: number, line_from?: number,
line_to?: number, line_to?: number,
motion_version_id?: number,
other_description?: string,
rejected?: boolean,
text?: string, text?: string,
type?: number creation_time?: string
) { ) {
super(id); super();
this.creation_time = creation_time; 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_from = line_from;
this.line_to = line_to; this.line_to = line_to;
this.motion_version_id = motion_version_id;
this.other_description = other_description;
this.rejected = rejected;
this.text = text; this.text = text;
this.type = type; this.creation_time = creation_time;
}
public getCollectionString(): string {
return MotionChangeReco.collectionString;
} }
} }

View File

@ -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 { export class Motion extends BaseModel {
static collectionString = 'motions/motion'; protected _collectionString: string;
id: number; id: number;
active_version: number;
agenda_item_id: number;
attachments_id: number[];
category_id: number;
comments: Object;
identifier: string; identifier: string;
log_messages: Object[]; versions: Object[];
active_version: number;
parent_id: number;
category_id: number;
motion_block_id: number; motion_block_id: number;
origin: string; origin: string;
parent_id: number;
polls: BaseModel[];
recommendation_id: number;
state_id: number;
state_required_permission_to_see: string;
submitters: Object[]; submitters: Object[];
supporters_id: number[]; supporters_id: number[];
comments: Object;
state_id: number;
state_required_permission_to_see: string;
recommendation_id: number;
tags_id: number[]; tags_id: number[];
versions: Object[]; attachments_id: number[];
polls: BaseModel[];
agenda_item_id: number;
log_messages: Object[];
constructor( constructor(
id: number, id?: number,
active_version?: number,
agenda_item_id?: number,
attachments_id?: number[],
category_id?: number,
comments?: Object,
identifier?: string, identifier?: string,
log_messages?: Object[], versions?: Object[],
active_version?: number,
parent_id?: number,
category_id?: number,
motion_block_id?: number, motion_block_id?: number,
origin?: string, origin?: string,
parent_id?: number,
polls?: BaseModel[],
recommendation_id?: number,
state_id?: number,
state_required_permission_to_see?: string,
submitters?: Object[], submitters?: Object[],
supporters_id?: number[], supporters_id?: number[],
comments?: Object,
state_id?: number,
state_required_permission_to_see?: string,
recommendation_id?: number,
tags_id?: number[], tags_id?: number[],
versions?: Object[] attachments_id?: number[],
polls?: BaseModel[],
agenda_item_id?: number,
log_messages?: Object[]
) { ) {
super(id); super();
this.active_version = active_version; this._collectionString = 'motions/motion';
this.agenda_item_id = agenda_item_id; this.id = id;
this.attachments_id = attachments_id;
this.category_id = category_id;
this.comments = comments;
this.identifier = identifier; 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.motion_block_id = motion_block_id;
this.origin = origin; 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.submitters = submitters;
this.supporters_id = supporters_id; 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.tags_id = tags_id;
this.versions = versions; this.attachments_id = attachments_id;
} this.polls = polls;
this.agenda_item_id = agenda_item_id;
public getCollectionString(): string { this.log_messages = log_messages;
return Motion.collectionString;
} }
} }

View 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;
}
}

View File

@ -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 { export class Workflow extends BaseModel {
static collectionString = 'motions/workflow'; protected _collectionString: string;
id: number; id: number;
first_state: number;
name: string; name: string;
states: Object[]; states: WorkflowState[];
first_state: number;
constructor(id: number, first_state?, name?, states?) { constructor(id?: number, name?: string, states?: WorkflowState[], first_state?: number) {
super(id); super();
this.first_state = first_state; this._collectionString = 'motions/workflow';
this.id = id;
this.name = name; this.name = name;
this.states = states; this.states = states;
this.first_state = first_state;
} }
public getCollectionString(): string { deserialize(input: any): this {
return Workflow.collectionString; Object.assign(this, input);
if (input.states instanceof Array) {
this.states = [];
input.states.forEach(workflowStateData => {
this.states.push(new WorkflowState().deserialize(workflowStateData));
});
}
return this;
} }
} }

View File

@ -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 { export class Topic extends BaseModel {
static collectionString = 'topics/topic'; protected _collectionString: string;
id: number; id: number;
agenda_item_id: number;
attachments_id: number[];
text: string;
title: 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) { constructor(id?: number, title?: string, text?: string, attachments_id?: number[], agenda_item_id?: number) {
super(id); super();
this.agenda_item_id = agenda_item_id; this._collectionString = 'topics/topic';
this.attachments_id = attachments_id; this.id = id;
this.text = text;
this.title = title; this.title = title;
this.text = text;
this.attachments_id = attachments_id;
this.agenda_item_id = agenda_item_id;
} }
public getCollectionString(): string { getAttachments(): BaseModel | BaseModel[] {
return Topic.collectionString; return this.DS.get('mediafiles/mediafile', ...this.attachments_id);
}
getAgenda(): BaseModel | BaseModel[] {
return this.DS.get('agenda/item', this.agenda_item_id);
} }
} }

View File

@ -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 { export class Group extends BaseModel {
static collectionString = 'users/group'; protected _collectionString: string;
id: number; id: number;
name: string; name: string;
permissions: string[]; //TODO permissions could be an own model? permissions: string[];
constructor(id: number, name?: string, permissions?: string[]) { constructor(id?: number, name?: string, permissions?: string[]) {
super(id); super();
this._collectionString = 'users/group';
this.id = id; this.id = id;
this.name = name; this.name = name;
this.permissions = permissions; this.permissions = permissions;
} }
public getCollectionString(): string {
return Group.collectionString;
}
} }

View File

@ -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 { export class PersonalNote extends BaseModel {
static collectionString = 'users/personal-note'; protected _collectionString: string;
id: number; id: number;
notes: Object;
user_id: number; user_id: number;
notes: Object;
constructor(id: number, notes?: Object, user_id?: number) { constructor(id?: number, user_id?: number, notes?: Object) {
super(id); super();
this.notes = notes; this._collectionString = 'users/personal-note';
this.id = id;
this.user_id = user_id; this.user_id = user_id;
this.notes = notes;
} }
public getCollectionString(): string { getUser(): BaseModel | BaseModel[] {
return PersonalNote.collectionString; return this.DS.get('users/user', this.user_id);
} }
} }

View File

@ -1,9 +1,11 @@
import { BaseModel } from 'app/core/models/baseModel'; import { BaseModel } from 'app/core/models/base-model';
// import { DS } from 'app/core/services/DS.service';
/**
* Representation of a user in contrast to the operator.
* @ignore
*/
export class User extends BaseModel { export class User extends BaseModel {
static collectionString = 'users/user'; protected _collectionString: string;
id: number; id: number;
username: string; username: string;
title: string; title: string;
@ -21,9 +23,8 @@ export class User extends BaseModel {
is_active: boolean; is_active: boolean;
default_password: string; default_password: string;
//default constructer with every possible optinal parameter for conventient usage
constructor( constructor(
id: number, id?: number,
username?: string, username?: string,
title?: string, title?: string,
first_name?: string, first_name?: string,
@ -40,7 +41,9 @@ export class User extends BaseModel {
is_active?: boolean, is_active?: boolean,
default_password?: string default_password?: string
) { ) {
super(id); super();
this._collectionString = 'users/user';
this.id = id;
this.username = username; this.username = username;
this.title = title; this.title = title;
this.first_name = first_name; this.first_name = first_name;
@ -58,7 +61,7 @@ export class User extends BaseModel {
this.default_password = default_password; this.default_password = default_password;
} }
public getCollectionString(): string { getGroups(): BaseModel | BaseModel[] {
return User.collectionString; return this.DS.get('users/group', ...this.groups_id);
} }
} }

View File

@ -11,12 +11,28 @@ import { map } from 'rxjs/operators/';
* *
*/ */
export class PruningTranslationLoader implements TranslateLoader { 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') {} 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 { public getTranslation(lang: string): any {
return this.http.get(`${this.prefix}${lang}${this.suffix}`).pipe(map((res: Object) => this.process(res))); 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) { private process(object: any) {
const newObject = {}; const newObject = {};
for (const key in object) { for (const key in object) {

View File

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

View File

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

View File

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

View File

@ -4,12 +4,32 @@ import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { OperatorService } from './operator.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({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class AuthGuard implements CanActivate { 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) {} 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 { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): any {
const url: string = state.url; const url: string = state.url;

View File

@ -4,47 +4,62 @@ import { Observable, of, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators'; import { catchError, tap } from 'rxjs/operators';
import { OperatorService } from 'app/core/services/operator.service'; import { OperatorService } from 'app/core/services/operator.service';
import { OpenSlidesComponent } from '../../openslides.component';
const httpOptions = { /**
withCredentials: true, * Authenticates an OpenSlides user with username and password
headers: new HttpHeaders({ */
'Content-Type': 'application/json'
})
};
@Injectable({ @Injectable({
providedIn: 'root' 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; 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> { login(username: string, password: string): Observable<any> {
const user: any = { const user: any = {
username: username, username: username,
password: password 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)), tap(resp => this.operator.storeUser(resp.user)),
catchError(this.handleError()) 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 //logout the user
//TODO not yet used //TODO not yet used
logout(): Observable<any> { logout(): Observable<any> {
this.operator.clear(); this.operator.clear();
return this.http.post<any>('/users/logout/', {}, httpOptions); return this.http.post<any>('/users/logout/', {});
}
//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);
};
} }
} }

View File

@ -1,8 +1,8 @@
import { Injectable } from '@angular/core'; 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 { Item } from 'app/core/models/agenda/item';
import { Assignment } from 'app/core/models/assignments/assignment'; import { Assignment } from 'app/core/models/assignments/assignment';
import { ChatMessage } from 'app/core/models/core/chat-message'; 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'; 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({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class AutoupdateService extends BaseComponent { export class AutoupdateService extends OpenSlidesComponent {
/**
* Stores the to create the socket created using {@link WebsocketService}.
*/
private socket; private socket;
/**
* Constructor to create the AutoupdateService. Calls the constructor of the parent class.
* @param websocketService
*/
constructor(private websocketService: WebsocketService) { constructor(private websocketService: WebsocketService) {
super(); super();
} }
// initialte autpupdate Service /**
* Function to start the automatic update process
* will build up a websocket connection using {@link WebsocketService}
*/
startAutoupdate(): void { startAutoupdate(): void {
console.log('start autoupdate');
this.socket = this.websocketService.connect(); this.socket = this.websocketService.connect();
this.socket.subscribe(response => { this.socket.subscribe(response => {
this.storeResponse(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 { storeResponse(socketResponse): void {
socketResponse.forEach(model => { socketResponse.forEach(jsonObj => {
switch (model.collection) { const targetClass = this.getClassFromCollectionString(jsonObj.collection);
case 'core/projector': { this.DS.add(new targetClass().deserialize(jsonObj.data));
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;
}
}
}); });
} }
/**
* 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;
}
}
}
} }

View File

@ -1,6 +1,6 @@
import { TestBed, inject } from '@angular/core/testing'; import { TestBed, inject } from '@angular/core/testing';
import { DataStoreService } from './DS.service'; import { DataStoreService } from './dataStore.service';
describe('DS', () => { describe('DS', () => {
beforeEach(() => { beforeEach(() => {
@ -8,9 +8,4 @@ describe('DS', () => {
providers: [DataStoreService] providers: [DataStoreService]
}); });
}); });
/*it('should be created', inject([DSService], (DS: DSService) => {
expect(DS).toBeTruthy();
}));*/
// just a static use
}); });

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

View File

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

View File

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

View File

@ -1,23 +1,25 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable, of, BehaviorSubject } from 'rxjs'; import { Observable, BehaviorSubject } from 'rxjs';
import { HttpClient, HttpResponse, HttpErrorResponse, HttpHeaders } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { tap, catchError, share } from 'rxjs/operators'; 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'; import { Group } from 'app/core/models/users/group';
// TODO: Dry /**
const httpOptions = { * The operator represents the user who is using OpenSlides.
withCredentials: true, *
headers: new HttpHeaders({ * Information is mostly redundant to user but has different purposes.
'Content-Type': 'application/json' * 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({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class OperatorService extends BaseComponent { export class OperatorService extends OpenSlidesComponent {
// default variables
about_me: string; about_me: string;
comment: string; comment: string;
default_password: string; default_password: string;
@ -35,11 +37,23 @@ export class OperatorService extends BaseComponent {
title: string; title: string;
username: string; username: string;
logged_in: boolean; logged_in: boolean;
// subject
/**
* The subject that can be observed by other instances using observing functions.
*/
private operatorSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null); 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(); 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) { constructor(private http: HttpClient) {
super(); super();
@ -56,9 +70,11 @@ export class OperatorService extends BaseComponent {
this.observeDataStore(); this.observeDataStore();
} }
// calls 'whoami' to find out the operator /**
* calls `/users/whoami` to find out the real operator
*/
public whoAmI(): Observable<any> { public whoAmI(): Observable<any> {
return this.http.get<any>('/users/whoami/', httpOptions).pipe( return this.http.get<any>('/users/whoami/').pipe(
tap(whoami => { tap(whoami => {
if (whoami && whoami.user) { if (whoami && whoami.user) {
this.storeUser(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 { public storeUser(user: any): void {
// store in file // store in file
this.about_me = user.about_me; this.about_me = user.about_me;
@ -87,11 +107,18 @@ export class OperatorService extends BaseComponent {
this.title = user.title; this.title = user.title;
this.username = user.username; this.username = user.username;
// also store in localstorrage // also store in localstorrage
this.updateLocalStorrage(); this.updateLocalStorage();
// update mode to inform observers // 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() { public clear() {
this.about_me = null; this.about_me = null;
this.comment = null; this.comment = null;
@ -109,19 +136,25 @@ export class OperatorService extends BaseComponent {
this.structure_level = null; this.structure_level = null;
this.title = null; this.title = null;
this.username = null; this.username = null;
this.updateMode(); this.setObservable(this.getUpdateObject());
localStorage.removeItem('operator'); 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())); 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 { private getUpdateObject(): any {
return { return {
about_me: this.about_me, 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 { private observeDataStore(): void {
console.log('Operator observes DataStore');
this.DS.getObservable().subscribe(newModel => { this.DS.getObservable().subscribe(newModel => {
if (newModel instanceof Group) { if (newModel instanceof Group) {
this.addGroup(newModel); 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) * Read out the Groups from the DataStore by the operators 'groups_id'
// requires that the whoAmI did return a valid operator *
* 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 { public readGroupsFromStore(): void {
this.DS.filter(Group, myGroup => { this.DS.filter(Group, myGroup => {
if (this.groups_id.includes(myGroup.id)) { 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() { public getObservable() {
return this.operatorSubject.asObservable(); return this.operatorSubject.asObservable();
} }
/**
* Inform all observers about changes
* @param value
*/
private setObservable(value) { private setObservable(value) {
this.operatorSubject.next(value); this.operatorSubject.next(value);
} }
/**
* Getter for the (real) {@link Group}s
*/
public getGroups() { public getGroups() {
return this.groups; 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 { 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); this.groups.push(newGroup);
// inform the observers about new groups (appOsPerms) // inform the observers about new groups (appOsPerms)
console.log('pushed a group into operator');
this.setObservable(newGroup); this.setObservable(newGroup);
} }
} }
// TODO Dry
private handleError<T>() {
return (error: any): Observable<T> => {
console.error(error);
return of(error);
};
}
} }

View File

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

View File

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

View File

@ -2,17 +2,33 @@ import { Injectable } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
/**
* Service that handles WebSocket connections.
*
* Creates or returns already created WebSockets.
*/
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class WebsocketService { export class WebsocketService {
/**
* Constructor that handles the router
* @param router the URL Router
*/
constructor(private router: 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>; private subject: WebSocketSubject<any>;
/**
* Creates a new WebSocket connection as WebSocketSubject
*
* Can return old Subjects to prevent multiple WebSocket connections.
*/
public connect(): WebSocketSubject<any> { public connect(): WebSocketSubject<any> {
const socketProtocol = this.getWebSocketProtocoll(); const socketProtocol = this.getWebSocketProtocol();
const socketPath = this.getWebSocketPath(); const socketPath = this.getWebSocketPath();
const socketServer = window.location.hostname + ':' + window.location.port; const socketServer = window.location.hostname + ':' + window.location.port;
if (!this.subject) { if (!this.subject) {
@ -21,7 +37,9 @@ export class WebsocketService {
return this.subject; 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 { private getWebSocketPath(): string {
//currentRoute does not end with '/' //currentRoute does not end with '/'
const currentRoute = this.router.url; 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') { if (location.protocol === 'https') {
return 'wss://'; return 'wss://';
} else { } else {

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

3608
package-lock.json generated

File diff suppressed because it is too large Load Diff