diff --git a/client/src/app/core/services/data-send.service.ts b/client/src/app/core/services/data-send.service.ts index bab6431f5..4321ec527 100644 --- a/client/src/app/core/services/data-send.service.ts +++ b/client/src/app/core/services/data-send.service.ts @@ -37,7 +37,7 @@ export class DataSendService { ) ); } else { - return this.http.put('rest/' + model.collectionString + '/' + model.id, model).pipe( + return this.http.patch('rest/' + model.collectionString + '/' + model.id, model).pipe( tap( response => { console.log('Update model. Response : ', response); diff --git a/client/src/app/shared/models/base.model.ts b/client/src/app/shared/models/base.model.ts index 19d4f8131..c48d1929a 100644 --- a/client/src/app/shared/models/base.model.ts +++ b/client/src/app/shared/models/base.model.ts @@ -39,10 +39,23 @@ export abstract class BaseModel extends OpenSlidesComponent implements Deseriali this._collectionString = collectionString; if (input) { + this.changeNullValuesToUndef(input); this.deserialize(input); } } + /** + * Prevent to send literally "null" if should be send + * @param input object to deserialize + */ + public changeNullValuesToUndef(input: any): void { + Object.keys(input).forEach(key => { + if (input[key] === null) { + input[key] = undefined; + } + }); + } + /** * returns the collectionString. * diff --git a/client/src/app/shared/models/deserializer.model.ts b/client/src/app/shared/models/deserializer.model.ts index 8013ec618..33d50ef2a 100644 --- a/client/src/app/shared/models/deserializer.model.ts +++ b/client/src/app/shared/models/deserializer.model.ts @@ -11,6 +11,7 @@ export abstract class Deserializer implements Deserializable { */ protected constructor(input?: any) { if (input) { + this.changeNullValuesToUndef(input); this.deserialize(input); } } @@ -22,4 +23,16 @@ export abstract class Deserializer implements Deserializable { public deserialize(input: any): void { Object.assign(this, input); } + + /** + * Prevent to send literally "null" if should be send + * @param input object to deserialize + */ + public changeNullValuesToUndef(input: any): void { + Object.keys(input).forEach(key => { + if (input[key] === null) { + input[key] = undefined; + } + }); + } } diff --git a/client/src/app/shared/models/motions/motion.ts b/client/src/app/shared/models/motions/motion.ts index 4535467a9..431c2146b 100644 --- a/client/src/app/shared/models/motions/motion.ts +++ b/client/src/app/shared/models/motions/motion.ts @@ -1,12 +1,9 @@ import { BaseModel } from '../base.model'; import { MotionSubmitter } from './motion-submitter'; import { MotionLog } from './motion-log'; -import { Config } from '../core/config'; -import { Workflow } from './workflow'; -import { User } from '../users/user'; import { Category } from './category'; -import { WorkflowState } from './workflow-state'; import { MotionComment } from './motion-comment'; +import { Workflow } from './workflow'; /** * Representation of Motion. @@ -21,7 +18,7 @@ export class Motion extends BaseModel { public title: string; public text: string; public reason: string; - public amendment_paragraphs: string; + public amendment_paragraphs: string[]; public modified_final_version: string; public parent_id: number; public category_id: number; @@ -30,6 +27,7 @@ export class Motion extends BaseModel { public submitters: MotionSubmitter[]; public supporters_id: number[]; public comments: MotionComment[]; + public workflow_id: number; public state_id: number; public state_extension: string; public state_required_permission_to_see: string; @@ -41,12 +39,8 @@ export class Motion extends BaseModel { public agenda_item_id: number; public log_messages: MotionLog[]; - // dynamic values - public workflow: Workflow; - public constructor(input?: any) { super('motions/motion', input); - this.initDataStoreValues(); } /** @@ -57,121 +51,19 @@ export class Motion extends BaseModel { } /** - * sets the and the workflow from either dataStore or WebSocket + * returns the motion submitters userIDs */ - public initDataStoreValues(): void { - // check the containing Workflows in DataStore - const allWorkflows = this.DS.getAll(Workflow); - allWorkflows.forEach(localWorkflow => { - if (localWorkflow.isStateContained(this.state_id)) { - this.workflow = localWorkflow as Workflow; - } - }); - - // observe for new models - this.DS.changeObservable.subscribe(newModel => { - if (newModel instanceof Workflow) { - if (newModel.isStateContained(this.state_id)) { - this.workflow = newModel as Workflow; - } - } - }); - } - - /** - * add a new motionSubmitter from user-object - * @param user the user - */ - public addSubmitter(user: User): void { - const newSubmitter = new MotionSubmitter(); - newSubmitter.user_id = user.id; - this.submitters.push(newSubmitter); - console.log('did addSubmitter. this.submitters: ', this.submitters); - } - - /** - * return the submitters as uses objects - */ - public get submitterAsUser(): User[] { - const submitterIds: number[] = this.submitters + public get submitterIds(): number[] { + return this.submitters .sort((a: MotionSubmitter, b: MotionSubmitter) => { return a.weight - b.weight; }) .map((submitter: MotionSubmitter) => submitter.user_id); - return this.DS.getMany('users/user', submitterIds); - } - - /** - * get the category of a motion as object - */ - public get category(): Category { - return this.DS.get(Category, this.category_id); - } - - /** - * Set the category in the motion - */ - public set category(newCategory: Category) { - this.category_id = newCategory.id; - } - - /** - * return the workflow state - */ - public get state(): WorkflowState { - if (this.workflow) { - return this.workflow.state_by_id(this.state_id); - } else { - return null; - } - } - - /** - * returns possible states for the motion - */ - public get nextStates(): WorkflowState[] { - if (this.workflow && this.state) { - return this.state.getNextStates(this.workflow); - } else { - return null; - } - } - - /** - * Returns the name of the recommendation. - * - * TODO: Motion workflow needs to be specific on the server - */ - public get recommendation(): WorkflowState { - if (this.recommendation_id && this.workflow && this.workflow.id) { - const state = this.workflow.state_by_id(this.recommendation_id); - return state; - } else { - return null; - } - } - - /** - * returns the value of 'config.motions_recommendations_by' - */ - public get recomBy(): string { - const motionsRecommendationsByConfig = this.DS.filter( - Config, - config => config.key === 'motions_recommendations_by' - )[0] as Config; - - if (motionsRecommendationsByConfig) { - const recomByString: string = motionsRecommendationsByConfig.value as string; - return recomByString; - } else { - return ''; - } } public deserialize(input: any): void { Object.assign(this, input); - this.submitters = []; if (input.submitters instanceof Array) { input.submitters.forEach(SubmitterData => { this.submitters.push(new MotionSubmitter(SubmitterData)); @@ -194,4 +86,9 @@ export class Motion extends BaseModel { } } +/** + * Hack to get them loaded at last + */ BaseModel.registerCollectionElement('motions/motion', Motion); +BaseModel.registerCollectionElement('motions/category', Category); +BaseModel.registerCollectionElement('motions/workflow', Workflow); diff --git a/client/src/app/shared/models/motions/workflow.ts b/client/src/app/shared/models/motions/workflow.ts index 231d4c4c8..2c418188d 100644 --- a/client/src/app/shared/models/motions/workflow.ts +++ b/client/src/app/shared/models/motions/workflow.ts @@ -34,7 +34,7 @@ export class Workflow extends BaseModel { }); } - public state_by_id(id: number): WorkflowState { + public getStateById(id: number): WorkflowState { let targetState; this.states.forEach(state => { if (id === state.id) { diff --git a/client/src/app/site/login/components/login-legal-notice/login-legal-notice.component.html b/client/src/app/site/login/components/login-legal-notice/login-legal-notice.component.html index 7592a1cf1..2de984b0f 100644 --- a/client/src/app/site/login/components/login-legal-notice/login-legal-notice.component.html +++ b/client/src/app/site/login/components/login-legal-notice/login-legal-notice.component.html @@ -1,7 +1,8 @@
- - Legal Notice - + + +

Legal Notice

+
diff --git a/client/src/app/site/login/components/login-privacy-policy/login-privacy-policy.component.html b/client/src/app/site/login/components/login-privacy-policy/login-privacy-policy.component.html index e327e262d..9f2be700c 100644 --- a/client/src/app/site/login/components/login-privacy-policy/login-privacy-policy.component.html +++ b/client/src/app/site/login/components/login-privacy-policy/login-privacy-policy.component.html @@ -1,7 +1,8 @@
- - Privacy Policy - + + +

Privacy Policy

+
diff --git a/client/src/app/site/motions/category-list/category-list.component.html b/client/src/app/site/motions/components/category-list/category-list.component.html similarity index 100% rename from client/src/app/site/motions/category-list/category-list.component.html rename to client/src/app/site/motions/components/category-list/category-list.component.html diff --git a/client/src/app/site/motions/category-list/category-list.component.scss b/client/src/app/site/motions/components/category-list/category-list.component.scss similarity index 100% rename from client/src/app/site/motions/category-list/category-list.component.scss rename to client/src/app/site/motions/components/category-list/category-list.component.scss diff --git a/client/src/app/site/motions/category-list/category-list.component.spec.ts b/client/src/app/site/motions/components/category-list/category-list.component.spec.ts similarity index 100% rename from client/src/app/site/motions/category-list/category-list.component.spec.ts rename to client/src/app/site/motions/components/category-list/category-list.component.spec.ts diff --git a/client/src/app/site/motions/category-list/category-list.component.ts b/client/src/app/site/motions/components/category-list/category-list.component.ts similarity index 94% rename from client/src/app/site/motions/category-list/category-list.component.ts rename to client/src/app/site/motions/components/category-list/category-list.component.ts index 53e89079e..1d44bc341 100644 --- a/client/src/app/site/motions/category-list/category-list.component.ts +++ b/client/src/app/site/motions/components/category-list/category-list.component.ts @@ -1,10 +1,11 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { MatSort, MatTable, MatTableDataSource } from '@angular/material'; + import { TranslateService } from '@ngx-translate/core'; -import { BaseComponent } from '../../../base.component'; -import { Category } from '../../../shared/models/motions/category'; +import { BaseComponent } from '../../../../base.component'; +import { Category } from '../../../../shared/models/motions/category'; /** * List view for the categories. diff --git a/client/src/app/site/motions/motion-detail/motion-detail.component.html b/client/src/app/site/motions/components/motion-detail/motion-detail.component.html similarity index 90% rename from client/src/app/site/motions/motion-detail/motion-detail.component.html rename to client/src/app/site/motions/components/motion-detail/motion-detail.component.html index 5b0b425c5..962055354 100644 --- a/client/src/app/site/motions/motion-detail/motion-detail.component.html +++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.html @@ -16,16 +16,26 @@ {{contentForm.get('title').value}}
- by {{motion.submitterAsUser}} + by {{motion.submitters}}
- + +
+ +
+
+ +
+ + - + @@ -137,24 +147,24 @@

Submitters

- {{motion.submitterAsUser}} + {{motion.submitters}}
-
+

Supporters

-
+

State

{{motion.state}}
- {{motionCopy.state}} + {{motionCopy.state}} {{state}} @@ -167,10 +177,10 @@ -
+
-

{{motion.recomBy}}

- {{motion.recommendation.name}} +

{{motion.recommender}}

+ {{motion.recommendation}}
@@ -187,7 +197,7 @@
-
+

Category

{{motion.category}} diff --git a/client/src/app/site/motions/motion-detail/motion-detail.component.scss b/client/src/app/site/motions/components/motion-detail/motion-detail.component.scss similarity index 100% rename from client/src/app/site/motions/motion-detail/motion-detail.component.scss rename to client/src/app/site/motions/components/motion-detail/motion-detail.component.scss diff --git a/client/src/app/site/motions/motion-detail/motion-detail.component.spec.ts b/client/src/app/site/motions/components/motion-detail/motion-detail.component.spec.ts similarity index 100% rename from client/src/app/site/motions/motion-detail/motion-detail.component.spec.ts rename to client/src/app/site/motions/components/motion-detail/motion-detail.component.spec.ts diff --git a/client/src/app/site/motions/motion-detail/motion-detail.component.ts b/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts similarity index 70% rename from client/src/app/site/motions/motion-detail/motion-detail.component.ts rename to client/src/app/site/motions/components/motion-detail/motion-detail.component.ts index 264dadf24..a954e1fcf 100644 --- a/client/src/app/site/motions/motion-detail/motion-detail.component.ts +++ b/client/src/app/site/motions/components/motion-detail/motion-detail.component.ts @@ -3,11 +3,11 @@ import { ActivatedRoute, Router } from '@angular/router'; import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { MatExpansionPanel } from '@angular/material'; -import { BaseComponent } from '../../../base.component'; -import { Motion } from '../../../shared/models/motions/motion'; -import { Category } from '../../../shared/models/motions/category'; -import { DataSendService } from '../../../core/services/data-send.service'; -import { ViewportService } from '../../../core/services/viewport.service'; +import { BaseComponent } from '../../../../base.component'; +import { Category } from '../../../../shared/models/motions/category'; +import { ViewportService } from '../../../../core/services/viewport.service'; +import { MotionRepositoryService } from '../../services/motion-repository.service'; +import { ViewMotion } from '../../models/view-motion'; /** * Component for the motion detail view @@ -20,24 +20,16 @@ import { ViewportService } from '../../../core/services/viewport.service'; export class MotionDetailComponent extends BaseComponent implements OnInit { /** * MatExpansionPanel for the meta info + * Only relevant in mobile view */ @ViewChild('metaInfoPanel') public metaInfoPanel: MatExpansionPanel; /** * MatExpansionPanel for the content panel + * Only relevant in mobile view */ @ViewChild('contentPanel') public contentPanel: MatExpansionPanel; - /** - * Target motion. Might be new or old - */ - public motion: Motion; - - /** - * Copy of the motion that the user might edit - */ - public motionCopy: Motion; - /** * Motions meta-info */ @@ -58,6 +50,16 @@ export class MotionDetailComponent extends BaseComponent implements OnInit { */ public newMotion = false; + /** + * Target motion. Might be new or old + */ + public motion: ViewMotion; + + /** + * Copy of the motion that the user might edit + */ + public motionCopy: ViewMotion; + /** * Constuct the detail view. * @@ -65,14 +67,14 @@ export class MotionDetailComponent extends BaseComponent implements OnInit { * @param router to navigate back to the motion list and to an existing motion * @param route determine if this is a new or an existing motion * @param formBuilder For reactive forms. Form Group and Form Control - * @param dataSend To send changes of the motion + * @param repo: Motion Repository */ public constructor( public vp: ViewportService, private router: Router, private route: ActivatedRoute, private formBuilder: FormBuilder, - private dataSend: DataSendService + private repo: MotionRepositoryService ) { super(); this.createForm(); @@ -82,21 +84,14 @@ export class MotionDetailComponent extends BaseComponent implements OnInit { this.editMotion = true; // Both are (temporarily) necessary until submitter and supporters are implemented - this.motion = new Motion(); - this.motionCopy = new Motion(); + // TODO new Motion and ViewMotion + this.motion = new ViewMotion(); + this.motionCopy = new ViewMotion(); } else { // load existing motion this.route.params.subscribe(params => { - // has the motion of the DataStore was initialized before. - this.motion = this.DS.get(Motion, params.id); - - // Observe motion to get the motion in the parameter and also get the changes - this.DS.changeObservable.subscribe(newModel => { - if (newModel instanceof Motion) { - if (newModel.id === +params.id) { - this.motion = newModel as Motion; - } - } + this.repo.getViewMotionObservable(params.id).subscribe(newViewMotion => { + this.motion = newViewMotion; }); }); } @@ -105,11 +100,11 @@ export class MotionDetailComponent extends BaseComponent implements OnInit { /** * Async load the values of the motion in the Form. */ - public patchForm(formMotion: Motion): void { + public patchForm(formMotion: ViewMotion): void { this.metaInfoForm.patchValue({ - category_id: formMotion.category_id, - state_id: formMotion.state_id, - recommendation_id: formMotion.recommendation_id, + category_id: formMotion.categoryId, + state_id: formMotion.stateId, + recommendation_id: formMotion.recommendationId, identifier: formMotion.identifier, origin: formMotion.origin }); @@ -148,21 +143,22 @@ export class MotionDetailComponent extends BaseComponent implements OnInit { * in the list view automatically * * TODO: state is not yet saved. Need a special "put" command + * + * TODO: Repo should handle */ public saveMotion(): void { const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value }; - this.motionCopy.patchValues(newMotionValues); - - // TODO: send to normal motion to verify - this.dataSend.saveModel(this.motionCopy).subscribe(answer => { - if (answer && answer.id && this.newMotion) { - this.router.navigate(['./motions/' + answer.id]); - } - }); + if (this.newMotion) { + this.repo.saveMotion(newMotionValues).subscribe(response => { + this.router.navigate(['./motions/' + response.id]); + }); + } else { + this.repo.saveMotion(newMotionValues, this.motionCopy).subscribe(); + } } /** - * return all Categories. + * return all Categories */ public getMotionCategories(): Category[] { return this.DS.getAll(Category); @@ -175,10 +171,8 @@ export class MotionDetailComponent extends BaseComponent implements OnInit { this.editMotion ? (this.editMotion = false) : (this.editMotion = true); if (this.editMotion) { // copy the motion - this.motionCopy = new Motion(); - this.motionCopy.patchValues(this.motion); + this.motionCopy = this.motion.copy(); this.patchForm(this.motionCopy); - if (this.vp.isMobile) { this.metaInfoPanel.open(); this.contentPanel.open(); @@ -188,11 +182,26 @@ export class MotionDetailComponent extends BaseComponent implements OnInit { } } + /** + * Cancel the editing process + * + * If a new motion was created, return to the list. + */ + public cancelEditMotionButton(): void { + if (this.newMotion) { + this.router.navigate(['./motions/']); + } else { + this.editMotion = false; + } + } + /** * Trigger to delete the motion + * + * TODO: Repo should handle */ public deleteMotionButton(): void { - this.dataSend.delete(this.motion).subscribe(answer => { + this.repo.deleteMotion(this.motion).subscribe(answer => { this.router.navigate(['./motions/']); }); } diff --git a/client/src/app/site/motions/motion-list/motion-list.component.html b/client/src/app/site/motions/components/motion-list/motion-list.component.html similarity index 93% rename from client/src/app/site/motions/motion-list/motion-list.component.html rename to client/src/app/site/motions/components/motion-list/motion-list.component.html index abdbf21dd..85902db71 100644 --- a/client/src/app/site/motions/motion-list/motion-list.component.html +++ b/client/src/app/site/motions/components/motion-list/motion-list.component.html @@ -26,11 +26,11 @@ Title
- {{motion.versions[0].title}} + {{motion.title}}
by - {{motion.submitterAsUser}} + {{motion.submitters}}
diff --git a/client/src/app/site/motions/motion-list/motion-list.component.scss b/client/src/app/site/motions/components/motion-list/motion-list.component.scss similarity index 100% rename from client/src/app/site/motions/motion-list/motion-list.component.scss rename to client/src/app/site/motions/components/motion-list/motion-list.component.scss diff --git a/client/src/app/site/motions/motion-list/motion-list.component.spec.ts b/client/src/app/site/motions/components/motion-list/motion-list.component.spec.ts similarity index 100% rename from client/src/app/site/motions/motion-list/motion-list.component.spec.ts rename to client/src/app/site/motions/components/motion-list/motion-list.component.spec.ts diff --git a/client/src/app/site/motions/motion-list/motion-list.component.ts b/client/src/app/site/motions/components/motion-list/motion-list.component.ts similarity index 73% rename from client/src/app/site/motions/motion-list/motion-list.component.ts rename to client/src/app/site/motions/components/motion-list/motion-list.component.ts index 417a487fe..3afd288a3 100644 --- a/client/src/app/site/motions/motion-list/motion-list.component.ts +++ b/client/src/app/site/motions/components/motion-list/motion-list.component.ts @@ -1,12 +1,14 @@ import { Component, OnInit, ViewChild } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; import { Title } from '@angular/platform-browser'; -import { BaseComponent } from 'app/base.component'; -import { TranslateService } from '@ngx-translate/core'; -import { Motion } from '../../../shared/models/motions/motion'; import { MatTable, MatPaginator, MatSort, MatTableDataSource } from '@angular/material'; -import { Workflow } from '../../../shared/models/motions/workflow'; -import { WorkflowState } from '../../../shared/models/motions/workflow-state'; + +import { TranslateService } from '@ngx-translate/core'; + +import { BaseComponent } from '../../../../base.component'; +import { MotionRepositoryService } from '../../services/motion-repository.service'; +import { ViewMotion } from '../../models/view-motion'; +import { WorkflowState } from '../../../../shared/models/motions/workflow-state'; /** * Component that displays all the motions in a Table using DataSource. @@ -17,25 +19,17 @@ import { WorkflowState } from '../../../shared/models/motions/workflow-state'; styleUrls: ['./motion-list.component.scss'] }) export class MotionListComponent extends BaseComponent implements OnInit { - /** - * Store motion workflows (to check the status of the motions) - */ - public workflowArray: Array; - - /** - * Store the motions - */ - public motionArray: Array; - /** * Will be processed by the mat-table + * + * Will represent the object that comes from the repository */ - public dataSource: MatTableDataSource; + public dataSource: MatTableDataSource; /** * The table itself. */ - @ViewChild(MatTable) public table: MatTable; + @ViewChild(MatTable) public table: MatTable; /** * Pagination. Might be turned off to all motions at once. @@ -54,6 +48,8 @@ export class MotionListComponent extends BaseComponent implements OnInit { /** * Use for maximal width + * + * TODO: Needs vp.desktop check */ public columnsToDisplayFullWidth = ['identifier', 'title', 'meta', 'state']; @@ -79,12 +75,14 @@ export class MotionListComponent extends BaseComponent implements OnInit { * @param translate Translation * @param router Router * @param route Current route + * @param repo Motion Repository */ public constructor( protected titleService: Title, protected translate: TranslateService, private router: Router, - private route: ActivatedRoute + private route: ActivatedRoute, + private repo: MotionRepositoryService ) { super(titleService, translate); } @@ -94,19 +92,13 @@ export class MotionListComponent extends BaseComponent implements OnInit { */ public ngOnInit(): void { super.setTitle('Motions'); - this.workflowArray = this.DS.getAll(Workflow); - this.motionArray = this.DS.getAll(Motion); - this.dataSource = new MatTableDataSource(this.motionArray); + + this.dataSource = new MatTableDataSource(); this.dataSource.paginator = this.paginator; this.dataSource.sort = this.sort; - // Observe DataStore for motions. Initially, executes once for every motion. - // The alternative approach is to put the observable as DataSource to the table - this.DS.changeObservable.subscribe(newModel => { - if (newModel instanceof Motion) { - this.motionArray = this.DS.getAll(Motion); - this.dataSource.data = this.motionArray; - } + this.repo.getViewMotionListObservable().subscribe(newMotions => { + this.dataSource.data = newMotions; }); } @@ -115,12 +107,12 @@ export class MotionListComponent extends BaseComponent implements OnInit { * * @param motion The row the user clicked at */ - public selectMotion(motion: Motion): void { + public selectMotion(motion: ViewMotion): void { this.router.navigate(['./' + motion.id], { relativeTo: this.route }); } /** - * Get the icon to the coresponding Motion Status + * Get the icon to the corresponding Motion Status * TODO Needs to be more accessible (Motion workflow needs adjustment on the server) * @param state the name of the state */ @@ -142,7 +134,11 @@ export class MotionListComponent extends BaseComponent implements OnInit { * @param state */ public isDisplayIcon(state: WorkflowState): boolean { - return state.name === 'accepted' || state.name === 'rejected' || state.name === 'not decided'; + if (state) { + return state.name === 'accepted' || state.name === 'rejected' || state.name === 'not decided'; + } else { + return false; + } } /** diff --git a/client/src/app/site/motions/models/view-motion.ts b/client/src/app/site/motions/models/view-motion.ts new file mode 100644 index 000000000..d251276be --- /dev/null +++ b/client/src/app/site/motions/models/view-motion.ts @@ -0,0 +1,193 @@ +import { Motion } from '../../../shared/models/motions/motion'; +import { Category } from '../../../shared/models/motions/category'; +import { User } from '../../../shared/models/users/user'; +import { Workflow } from '../../../shared/models/motions/workflow'; +import { WorkflowState } from '../../../shared/models/motions/workflow-state'; +import { BaseModel } from '../../../shared/models/base.model'; + +/** + * Motion class for the View + * + * Stores a motion including all (implicit) references + * Provides "safe" access to variables and functions in {@link Motion} + * @ignore + */ +export class ViewMotion { + private _motion: Motion; + private _category: Category; + private _submitters: User[]; + private _supporters: User[]; + private _workflow: Workflow; + private _state: WorkflowState; + + public get motion(): Motion { + return this._motion; + } + + public get id(): number { + if (this.motion) { + return this.motion.id; + } else { + return null; + } + } + + public get identifier(): string { + if (this.motion) { + return this.motion.identifier; + } else { + return null; + } + } + + public get title(): string { + if (this.motion) { + return this.motion.title; + } else { + return null; + } + } + + public get text(): string { + if (this.motion) { + return this.motion.text; + } else { + return null; + } + } + + public get reason(): string { + if (this.motion) { + return this.motion.reason; + } else { + return null; + } + } + + public get category(): Category { + return this._category; + } + + public get categoryId(): number { + if (this._motion && this._motion.category_id) { + return this.category.id; + } else { + return null; + } + } + + public get submitters(): User[] { + return this._submitters; + } + + public get supporters(): User[] { + return this._supporters; + } + + public get workflow(): Workflow { + return this._workflow; + } + + public get state(): WorkflowState { + return this._state; + } + + public get stateId(): number { + if (this._motion && this._motion.state_id) { + return this._motion.state_id; + } else { + return null; + } + } + + public get recommendationId(): number { + return this._motion.recommendation_id; + } + + /** + * FIXME: + * name of recommender exist in a config + * previously solved using `this.DS.filter(Config)` + * and checking: motionsRecommendationsByConfig.value + * + */ + public get recommender(): string { + return null; + } + + public get recommendation(): WorkflowState { + if (this.recommendationId && this.workflow) { + return this.workflow.getStateById(this.recommendationId); + } else { + return null; + } + } + + public get origin(): string { + if (this.motion) { + return this.motion.origin; + } else { + return null; + } + } + + public get nextStates(): WorkflowState[] { + if (this.state && this.workflow) { + return this.state.getNextStates(this.workflow); + } else { + return null; + } + } + + public constructor( + motion?: Motion, + category?: Category, + submitters?: User[], + supporters?: User[], + workflow?: Workflow, + state?: WorkflowState + ) { + this._motion = motion; + this._category = category; + this._submitters = submitters; + this._supporters = supporters; + this._workflow = workflow; + this._state = state; + } + + /** + * Updates the local objects if required + * @param update + */ + public updateValues(update: BaseModel): void { + if (update instanceof Workflow) { + if (this.motion && update.id === this.motion.workflow_id) { + this._workflow = update as Workflow; + } + } else if (update instanceof Category) { + if (this.motion && update.id === this.motion.category_id) { + this._category = update as Category; + } + } + // TODO: There is no way (yet) to add Submitters to a motion + // Thus, this feature could not be tested + } + + public hasSupporters(): boolean { + return !!(this.supporters && this.supporters.length > 0); + } + + /** + * Duplicate this motion into a copy of itself + */ + public copy(): ViewMotion { + return new ViewMotion( + this._motion, + this._category, + this._submitters, + this._supporters, + this._workflow, + this._state + ); + } +} diff --git a/client/src/app/site/motions/motions-routing.module.ts b/client/src/app/site/motions/motions-routing.module.ts index ca3021483..67f954cf2 100644 --- a/client/src/app/site/motions/motions-routing.module.ts +++ b/client/src/app/site/motions/motions-routing.module.ts @@ -1,8 +1,8 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; -import { MotionListComponent } from './motion-list/motion-list.component'; -import { MotionDetailComponent } from './motion-detail/motion-detail.component'; -import { CategoryListComponent } from './category-list/category-list.component'; +import { MotionListComponent } from './components/motion-list/motion-list.component'; +import { MotionDetailComponent } from './components/motion-detail/motion-detail.component'; +import { CategoryListComponent } from './components/category-list/category-list.component'; const routes: Routes = [ { path: '', component: MotionListComponent }, diff --git a/client/src/app/site/motions/motions.module.ts b/client/src/app/site/motions/motions.module.ts index d76a4a4c9..8f60922ad 100644 --- a/client/src/app/site/motions/motions.module.ts +++ b/client/src/app/site/motions/motions.module.ts @@ -3,9 +3,9 @@ import { CommonModule } from '@angular/common'; import { MotionsRoutingModule } from './motions-routing.module'; import { SharedModule } from '../../shared/shared.module'; -import { MotionListComponent } from './motion-list/motion-list.component'; -import { MotionDetailComponent } from './motion-detail/motion-detail.component'; -import { CategoryListComponent } from './category-list/category-list.component'; +import { MotionListComponent } from './components/motion-list/motion-list.component'; +import { MotionDetailComponent } from './components/motion-detail/motion-detail.component'; +import { CategoryListComponent } from './components/category-list/category-list.component'; @NgModule({ imports: [CommonModule, MotionsRoutingModule, SharedModule], diff --git a/client/src/app/site/motions/services/motion-repository.service.spec.ts b/client/src/app/site/motions/services/motion-repository.service.spec.ts new file mode 100644 index 000000000..580f65bf8 --- /dev/null +++ b/client/src/app/site/motions/services/motion-repository.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { MotionRepositoryService } from './motion-repository.service'; + +describe('MotionRepositoryService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [MotionRepositoryService] + }); + }); + + it('should be created', inject([MotionRepositoryService], (service: MotionRepositoryService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/client/src/app/site/motions/services/motion-repository.service.ts b/client/src/app/site/motions/services/motion-repository.service.ts new file mode 100644 index 000000000..b7ea78b3e --- /dev/null +++ b/client/src/app/site/motions/services/motion-repository.service.ts @@ -0,0 +1,194 @@ +import { Injectable } from '@angular/core'; + +import { DataSendService } from '../../../core/services/data-send.service'; +import { OpenSlidesComponent } from '../../../openslides.component'; +import { Motion } from '../../../shared/models/motions/motion'; +import { User } from '../../../shared/models/users/user'; +import { Category } from '../../../shared/models/motions/category'; +import { Workflow } from '../../../shared/models/motions/workflow'; +import { WorkflowState } from '../../../shared/models/motions/workflow-state'; +import { ViewMotion } from '../models/view-motion'; +import { Observable, BehaviorSubject } from 'rxjs'; + +/** + * Repository Services for motions (and potentially categories) + * + * The repository is meant to process domain objects (those found under + * shared/models), so components can display them and interact with them. + * + * Rather than manipulating models directly, the repository is meant to + * inform the {@link DataSendService} about changes which will send + * them to the Server. + */ +@Injectable({ + providedIn: 'root' +}) +export class MotionRepositoryService extends OpenSlidesComponent { + /** + * Stores all the viewMotion in an object + */ + private viewMotionStore: { [motionId: number]: ViewMotion } = {}; + + /** + * Stores subjects to viewMotions in a list + */ + private viewMotionSubjects: { [motionId: number]: BehaviorSubject } = {}; + + /** + * Observable subject for the whole list + */ + private viewMotionListSubject: BehaviorSubject = new BehaviorSubject(null); + + /** + * Creates a MotionRepository + * + * Converts existing and incoming motions to ViewMotions + * Handles CRUD using an observer to the DataStore + * @param DataSend + */ + public constructor(private dataSend: DataSendService) { + super(); + + this.populateViewMotions(); + + // Could be raise in error if the root injector is not known + this.DS.changeObservable.subscribe(model => { + if (model instanceof Motion) { + // Add new and updated motions to the viewMotionStore + this.AddViewMotion(model); + this.updateObservables(model.id); + } else if (model instanceof Category || model instanceof User || model instanceof Workflow) { + // if an domain object we need was added or changed, update ViewMotionStore + this.getViewMotionList().forEach(viewMotion => { + viewMotion.updateValues(model); + }); + this.updateObservables(model.id); + } + }); + + // Watch the Observables for deleting + this.DS.deletedObservable.subscribe(model => { + if (model.collection === 'motions/motion') { + delete this.viewMotionStore[model.id]; + this.updateObservables(model.id); + } + }); + } + + /** + * called from the constructor. + * + * Populate the local viewMotionStore with ViewMotion Objects. + * Does nothing if the database was not created yet. + */ + private populateViewMotions(): void { + this.DS.getAll(Motion).forEach(motion => { + this.AddViewMotion(motion); + this.updateViewMotionObservable(motion.id); + }); + this.updateViewMotionListObservable(); + } + + /** + * Converts a motion to a ViewMotion and adds it to the store. + * + * Foreign references of the motion will be resolved (e.g submitters to users) + * Expandable to all (server side) changes that might occur on the motion object. + * + * @param motion blank motion domain object + */ + private AddViewMotion(motion: Motion): void { + const category = this.DS.get(Category, motion.category_id); + const submitters = this.DS.getMany(User, motion.submitterIds); + const supporters = this.DS.getMany(User, motion.supporters_id); + const workflow = this.DS.get(Workflow, motion.workflow_id); + let state: WorkflowState = null; + if (workflow) { + state = workflow.getStateById(motion.state_id); + } + this.viewMotionStore[motion.id] = new ViewMotion(motion, category, submitters, supporters, workflow, state); + } + + /** + * Creates and updates a motion + * + * Creates a (real) motion with patched data and delegate it + * to the {@link DataSendService} + * + * @param update the form data containing the update values + * @param viewMotion The View Motion. If not present, a new motion will be created + */ + public saveMotion(update: any, viewMotion?: ViewMotion): Observable { + let updateMotion: Motion; + + if (viewMotion) { + // implies that an existing motion was updated + updateMotion = viewMotion.motion; + } else { + // implies that a new motion was created + updateMotion = new Motion(); + } + updateMotion.patchValues(update); + return this.dataSend.saveModel(updateMotion); + } + + /** + * returns the current observable MotionView + */ + public getViewMotionObservable(id: number): Observable { + if (!this.viewMotionSubjects[id]) { + this.updateViewMotionObservable(id); + } + return this.viewMotionSubjects[id].asObservable(); + } + + /** + * return the Observable of the whole store + */ + public getViewMotionListObservable(): Observable { + return this.viewMotionListSubject.asObservable(); + } + + /** + * Deleting a motion. + * + * Extract the motion out of the motionView and delegate + * to {@link DataSendService} + * @param viewMotion + */ + public deleteMotion(viewMotion: ViewMotion): Observable { + return this.dataSend.delete(viewMotion.motion); + } + + /** + * Updates the ViewMotion observable using a ViewMotion corresponding to the id + */ + private updateViewMotionObservable(id: number): void { + if (!this.viewMotionSubjects[id]) { + this.viewMotionSubjects[id] = new BehaviorSubject(null); + } + this.viewMotionSubjects[id].next(this.viewMotionStore[id]); + } + + /** + * helper function to return the viewMotions as array + */ + private getViewMotionList(): ViewMotion[] { + return Object.values(this.viewMotionStore); + } + + /** + * update the observable of the list + */ + private updateViewMotionListObservable(): void { + this.viewMotionListSubject.next(this.getViewMotionList()); + } + + /** + * Triggers both the observable update routines + */ + private updateObservables(id: number): void { + this.updateViewMotionListObservable(); + this.updateViewMotionObservable(id); + } +}