Motion Repository

Adds a repository to remove the logic in the motion domain object
The repository will add a new model layer between the components
and the domain objects

implicitly add a new buttion into the motion detail view
This commit is contained in:
Sean Engelhardt 2018-09-04 14:55:07 +02:00
parent a5f06d3347
commit 39f266d2de
24 changed files with 565 additions and 222 deletions

View File

@ -37,7 +37,7 @@ export class DataSendService {
) )
); );
} else { } else {
return this.http.put<BaseModel>('rest/' + model.collectionString + '/' + model.id, model).pipe( return this.http.patch<BaseModel>('rest/' + model.collectionString + '/' + model.id, model).pipe(
tap( tap(
response => { response => {
console.log('Update model. Response : ', response); console.log('Update model. Response : ', response);

View File

@ -39,10 +39,23 @@ export abstract class BaseModel extends OpenSlidesComponent implements Deseriali
this._collectionString = collectionString; this._collectionString = collectionString;
if (input) { if (input) {
this.changeNullValuesToUndef(input);
this.deserialize(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. * returns the collectionString.
* *

View File

@ -11,6 +11,7 @@ export abstract class Deserializer implements Deserializable {
*/ */
protected constructor(input?: any) { protected constructor(input?: any) {
if (input) { if (input) {
this.changeNullValuesToUndef(input);
this.deserialize(input); this.deserialize(input);
} }
} }
@ -22,4 +23,16 @@ export abstract class Deserializer implements Deserializable {
public deserialize(input: any): void { public deserialize(input: any): void {
Object.assign(this, input); 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;
}
});
}
} }

View File

@ -1,12 +1,9 @@
import { BaseModel } from '../base.model'; import { BaseModel } from '../base.model';
import { MotionSubmitter } from './motion-submitter'; import { MotionSubmitter } from './motion-submitter';
import { MotionLog } from './motion-log'; import { MotionLog } from './motion-log';
import { Config } from '../core/config';
import { Workflow } from './workflow';
import { User } from '../users/user';
import { Category } from './category'; import { Category } from './category';
import { WorkflowState } from './workflow-state';
import { MotionComment } from './motion-comment'; import { MotionComment } from './motion-comment';
import { Workflow } from './workflow';
/** /**
* Representation of Motion. * Representation of Motion.
@ -21,7 +18,7 @@ export class Motion extends BaseModel {
public title: string; public title: string;
public text: string; public text: string;
public reason: string; public reason: string;
public amendment_paragraphs: string; public amendment_paragraphs: string[];
public modified_final_version: string; public modified_final_version: string;
public parent_id: number; public parent_id: number;
public category_id: number; public category_id: number;
@ -30,6 +27,7 @@ export class Motion extends BaseModel {
public submitters: MotionSubmitter[]; public submitters: MotionSubmitter[];
public supporters_id: number[]; public supporters_id: number[];
public comments: MotionComment[]; public comments: MotionComment[];
public workflow_id: number;
public state_id: number; public state_id: number;
public state_extension: string; public state_extension: string;
public state_required_permission_to_see: string; public state_required_permission_to_see: string;
@ -41,12 +39,8 @@ export class Motion extends BaseModel {
public agenda_item_id: number; public agenda_item_id: number;
public log_messages: MotionLog[]; public log_messages: MotionLog[];
// dynamic values
public workflow: Workflow;
public constructor(input?: any) { public constructor(input?: any) {
super('motions/motion', input); 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 { public get submitterIds(): number[] {
// check the containing Workflows in DataStore return this.submitters
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
.sort((a: MotionSubmitter, b: MotionSubmitter) => { .sort((a: MotionSubmitter, b: MotionSubmitter) => {
return a.weight - b.weight; return a.weight - b.weight;
}) })
.map((submitter: MotionSubmitter) => submitter.user_id); .map((submitter: MotionSubmitter) => submitter.user_id);
return this.DS.getMany<User>('users/user', submitterIds);
}
/**
* get the category of a motion as object
*/
public get category(): Category {
return this.DS.get<Category>(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 => 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 { public deserialize(input: any): void {
Object.assign(this, input); Object.assign(this, input);
this.submitters = [];
if (input.submitters instanceof Array) { if (input.submitters instanceof Array) {
input.submitters.forEach(SubmitterData => { input.submitters.forEach(SubmitterData => {
this.submitters.push(new MotionSubmitter(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/motion', Motion);
BaseModel.registerCollectionElement('motions/category', Category);
BaseModel.registerCollectionElement('motions/workflow', Workflow);

View File

@ -34,7 +34,7 @@ export class Workflow extends BaseModel {
}); });
} }
public state_by_id(id: number): WorkflowState { public getStateById(id: number): WorkflowState {
let targetState; let targetState;
this.states.forEach(state => { this.states.forEach(state => {
if (id === state.id) { if (id === state.id) {

View File

@ -1,7 +1,8 @@
<main> <main>
<mat-toolbar color="primary" translate>
Legal Notice <mat-card class="os-card">
</mat-toolbar> <h2 translate>Legal Notice</h2>
</mat-card>
<os-legal-notice-content></os-legal-notice-content> <os-legal-notice-content></os-legal-notice-content>

View File

@ -1,7 +1,8 @@
<main> <main>
<mat-toolbar color="primary" translate>
Privacy Policy <mat-card class="os-card">
</mat-toolbar> <h2 translate>Privacy Policy</h2>
</mat-card>
<os-privacy-policy-content></os-privacy-policy-content> <os-privacy-policy-content></os-privacy-policy-content>

View File

@ -1,10 +1,11 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { MatSort, MatTable, MatTableDataSource } from '@angular/material'; import { MatSort, MatTable, MatTableDataSource } from '@angular/material';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { BaseComponent } from '../../../base.component'; import { BaseComponent } from '../../../../base.component';
import { Category } from '../../../shared/models/motions/category'; import { Category } from '../../../../shared/models/motions/category';
/** /**
* List view for the categories. * List view for the categories.

View File

@ -16,16 +16,26 @@
<span *ngIf="editMotion"> {{contentForm.get('title').value}}</span> <span *ngIf="editMotion"> {{contentForm.get('title').value}}</span>
<br> <br>
<div *ngIf="motion" class='motion-submitter'> <div *ngIf="motion" class='motion-submitter'>
<span translate>by</span> {{motion.submitterAsUser}} <span translate>by</span> {{motion.submitters}}
</div> </div>
</div> </div>
<span class='spacer'></span> <span class='spacer'></span>
<!-- Button on the right-->
<div *ngIf="editMotion">
<button (click)='cancelEditMotionButton()' class='on-transition-fade' mat-icon-button>
<fa-icon icon='times'></fa-icon>
</button>
</div>
<div *ngIf="!editMotion">
<button class='on-transition-fade' mat-icon-button [matMenuTriggerFor]="motionExtraMenu"> <button class='on-transition-fade' mat-icon-button [matMenuTriggerFor]="motionExtraMenu">
<fa-icon icon='ellipsis-v'></fa-icon> <fa-icon icon='ellipsis-v'></fa-icon>
</button> </button>
</div>
<mat-menu #motionExtraMenu="matMenu"> <mat-menu #motionExtraMenu="matMenu">
<!-- TODO the functions for the buttons --> <!-- TODO: the functions for the buttons -->
<button mat-menu-item translate>Export As...</button> <button mat-menu-item translate>Export As...</button>
<button mat-menu-item translate>Project</button> <button mat-menu-item translate>Project</button>
<mat-divider></mat-divider> <mat-divider></mat-divider>
@ -137,24 +147,24 @@
<!-- Submitter --> <!-- Submitter -->
<div *ngIf="motion && motion.submitters || editMotion"> <div *ngIf="motion && motion.submitters || editMotion">
<h3 translate>Submitters</h3> <h3 translate>Submitters</h3>
{{motion.submitterAsUser}} {{motion.submitters}}
</div> </div>
<!-- Supporter --> <!-- Supporter -->
<div *ngIf='motion && motion.supporters_id && motion.supporters_id.length > 0 || editMotion'> <div *ngIf='motion && motion.hasSupporters() || editMotion'>
<h3 translate>Supporters</h3> <h3 translate>Supporters</h3>
<!-- print all motion supporters --> <!-- print all motion supporters -->
</div> </div>
<!-- State --> <!-- State -->
<div *ngIf='!newMotion && motion && motion.workflow && motion.state_id || editMotion'> <div *ngIf='!newMotion && motion && motion.workflow && motion.state || editMotion'>
<div *ngIf='!editMotion'> <div *ngIf='!editMotion'>
<h3 translate>State</h3> <h3 translate>State</h3>
{{motion.state}} {{motion.state}}
</div> </div>
<mat-form-field *ngIf="editMotion && !newMotion"> <mat-form-field *ngIf="editMotion && !newMotion">
<mat-select placeholder='State' formControlName='state_id'> <mat-select placeholder='State' formControlName='state_id'>
<mat-option [value]="motionCopy.state.id">{{motionCopy.state}}</mat-option> <mat-option [value]="motionCopy.stateId">{{motionCopy.state}}</mat-option>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<mat-option *ngFor="let state of motionCopy.nextStates" [value]="state.id">{{state}}</mat-option> <mat-option *ngFor="let state of motionCopy.nextStates" [value]="state.id">{{state}}</mat-option>
<mat-divider></mat-divider> <mat-divider></mat-divider>
@ -167,10 +177,10 @@
<!-- Recommendation --> <!-- Recommendation -->
<!-- The suggestion of the work group weather or not a motion should be accepted --> <!-- The suggestion of the work group weather or not a motion should be accepted -->
<div *ngIf='motion && motion.recomBy && (motion.recommendation_id || editMotion)'> <div *ngIf='motion && motion.recommender && (motion.recommendationId || editMotion)'>
<div *ngIf='!editMotion'> <div *ngIf='!editMotion'>
<h3>{{motion.recomBy}}</h3> <h3>{{motion.recommender}}</h3>
{{motion.recommendation.name}} {{motion.recommendation}}
</div> </div>
<mat-form-field *ngIf="motion && editMotion"> <mat-form-field *ngIf="motion && editMotion">
<mat-select placeholder='Recommendation' formControlName='recommendation_id'> <mat-select placeholder='Recommendation' formControlName='recommendation_id'>
@ -187,7 +197,7 @@
</div> </div>
<!-- Category --> <!-- Category -->
<div *ngIf="motion && motion.category_id || editMotion"> <div *ngIf="motion && motion.categoryId || editMotion">
<div *ngIf='!editMotion'> <div *ngIf='!editMotion'>
<h3 translate> Category</h3> <h3 translate> Category</h3>
{{motion.category}} {{motion.category}}

View File

@ -3,11 +3,11 @@ import { ActivatedRoute, Router } from '@angular/router';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { MatExpansionPanel } from '@angular/material'; import { MatExpansionPanel } from '@angular/material';
import { BaseComponent } from '../../../base.component'; import { BaseComponent } from '../../../../base.component';
import { Motion } from '../../../shared/models/motions/motion'; import { Category } from '../../../../shared/models/motions/category';
import { Category } from '../../../shared/models/motions/category'; import { ViewportService } from '../../../../core/services/viewport.service';
import { DataSendService } from '../../../core/services/data-send.service'; import { MotionRepositoryService } from '../../services/motion-repository.service';
import { ViewportService } from '../../../core/services/viewport.service'; import { ViewMotion } from '../../models/view-motion';
/** /**
* Component for the motion detail view * Component for the motion detail view
@ -20,24 +20,16 @@ import { ViewportService } from '../../../core/services/viewport.service';
export class MotionDetailComponent extends BaseComponent implements OnInit { export class MotionDetailComponent extends BaseComponent implements OnInit {
/** /**
* MatExpansionPanel for the meta info * MatExpansionPanel for the meta info
* Only relevant in mobile view
*/ */
@ViewChild('metaInfoPanel') public metaInfoPanel: MatExpansionPanel; @ViewChild('metaInfoPanel') public metaInfoPanel: MatExpansionPanel;
/** /**
* MatExpansionPanel for the content panel * MatExpansionPanel for the content panel
* Only relevant in mobile view
*/ */
@ViewChild('contentPanel') public contentPanel: MatExpansionPanel; @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 * Motions meta-info
*/ */
@ -58,6 +50,16 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
*/ */
public newMotion = false; 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. * 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 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 route determine if this is a new or an existing motion
* @param formBuilder For reactive forms. Form Group and Form Control * @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 constructor(
public vp: ViewportService, public vp: ViewportService,
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private dataSend: DataSendService private repo: MotionRepositoryService
) { ) {
super(); super();
this.createForm(); this.createForm();
@ -82,21 +84,14 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
this.editMotion = true; this.editMotion = true;
// Both are (temporarily) necessary until submitter and supporters are implemented // Both are (temporarily) necessary until submitter and supporters are implemented
this.motion = new Motion(); // TODO new Motion and ViewMotion
this.motionCopy = new Motion(); this.motion = new ViewMotion();
this.motionCopy = new ViewMotion();
} else { } else {
// load existing motion // load existing motion
this.route.params.subscribe(params => { this.route.params.subscribe(params => {
// has the motion of the DataStore was initialized before. this.repo.getViewMotionObservable(params.id).subscribe(newViewMotion => {
this.motion = this.DS.get(Motion, params.id); this.motion = newViewMotion;
// 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;
}
}
}); });
}); });
} }
@ -105,11 +100,11 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
/** /**
* Async load the values of the motion in the Form. * Async load the values of the motion in the Form.
*/ */
public patchForm(formMotion: Motion): void { public patchForm(formMotion: ViewMotion): void {
this.metaInfoForm.patchValue({ this.metaInfoForm.patchValue({
category_id: formMotion.category_id, category_id: formMotion.categoryId,
state_id: formMotion.state_id, state_id: formMotion.stateId,
recommendation_id: formMotion.recommendation_id, recommendation_id: formMotion.recommendationId,
identifier: formMotion.identifier, identifier: formMotion.identifier,
origin: formMotion.origin origin: formMotion.origin
}); });
@ -148,21 +143,22 @@ export class MotionDetailComponent extends BaseComponent implements OnInit {
* in the list view automatically * in the list view automatically
* *
* TODO: state is not yet saved. Need a special "put" command * TODO: state is not yet saved. Need a special "put" command
*
* TODO: Repo should handle
*/ */
public saveMotion(): void { public saveMotion(): void {
const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value }; const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value };
this.motionCopy.patchValues(newMotionValues); if (this.newMotion) {
this.repo.saveMotion(newMotionValues).subscribe(response => {
// TODO: send to normal motion to verify this.router.navigate(['./motions/' + response.id]);
this.dataSend.saveModel(this.motionCopy).subscribe(answer => {
if (answer && answer.id && this.newMotion) {
this.router.navigate(['./motions/' + answer.id]);
}
}); });
} else {
this.repo.saveMotion(newMotionValues, this.motionCopy).subscribe();
}
} }
/** /**
* return all Categories. * return all Categories
*/ */
public getMotionCategories(): Category[] { public getMotionCategories(): Category[] {
return this.DS.getAll(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); this.editMotion ? (this.editMotion = false) : (this.editMotion = true);
if (this.editMotion) { if (this.editMotion) {
// copy the motion // copy the motion
this.motionCopy = new Motion(); this.motionCopy = this.motion.copy();
this.motionCopy.patchValues(this.motion);
this.patchForm(this.motionCopy); this.patchForm(this.motionCopy);
if (this.vp.isMobile) { if (this.vp.isMobile) {
this.metaInfoPanel.open(); this.metaInfoPanel.open();
this.contentPanel.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 * Trigger to delete the motion
*
* TODO: Repo should handle
*/ */
public deleteMotionButton(): void { public deleteMotionButton(): void {
this.dataSend.delete(this.motion).subscribe(answer => { this.repo.deleteMotion(this.motion).subscribe(answer => {
this.router.navigate(['./motions/']); this.router.navigate(['./motions/']);
}); });
} }

View File

@ -26,11 +26,11 @@
<mat-header-cell *matHeaderCellDef mat-sort-header> Title </mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header> Title </mat-header-cell>
<mat-cell *matCellDef="let motion"> <mat-cell *matCellDef="let motion">
<div class='innerTable'> <div class='innerTable'>
<span class='motion-list-title'>{{motion.versions[0].title}}</span> <span class='motion-list-title'>{{motion.title}}</span>
<br> <br>
<span class='motion-list-from'> <span class='motion-list-from'>
<span translate>by</span> <span translate>by</span>
{{motion.submitterAsUser}} {{motion.submitters}}
</span> </span>
</div> </div>
</mat-cell> </mat-cell>

View File

@ -1,12 +1,14 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser'; 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 { 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. * 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'] styleUrls: ['./motion-list.component.scss']
}) })
export class MotionListComponent extends BaseComponent implements OnInit { export class MotionListComponent extends BaseComponent implements OnInit {
/**
* Store motion workflows (to check the status of the motions)
*/
public workflowArray: Array<Workflow>;
/**
* Store the motions
*/
public motionArray: Array<Motion>;
/** /**
* Will be processed by the mat-table * Will be processed by the mat-table
*
* Will represent the object that comes from the repository
*/ */
public dataSource: MatTableDataSource<Motion>; public dataSource: MatTableDataSource<ViewMotion>;
/** /**
* The table itself. * The table itself.
*/ */
@ViewChild(MatTable) public table: MatTable<Motion>; @ViewChild(MatTable) public table: MatTable<ViewMotion>;
/** /**
* Pagination. Might be turned off to all motions at once. * 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 * Use for maximal width
*
* TODO: Needs vp.desktop check
*/ */
public columnsToDisplayFullWidth = ['identifier', 'title', 'meta', 'state']; public columnsToDisplayFullWidth = ['identifier', 'title', 'meta', 'state'];
@ -79,12 +75,14 @@ export class MotionListComponent extends BaseComponent implements OnInit {
* @param translate Translation * @param translate Translation
* @param router Router * @param router Router
* @param route Current route * @param route Current route
* @param repo Motion Repository
*/ */
public constructor( public constructor(
protected titleService: Title, protected titleService: Title,
protected translate: TranslateService, protected translate: TranslateService,
private router: Router, private router: Router,
private route: ActivatedRoute private route: ActivatedRoute,
private repo: MotionRepositoryService
) { ) {
super(titleService, translate); super(titleService, translate);
} }
@ -94,19 +92,13 @@ export class MotionListComponent extends BaseComponent implements OnInit {
*/ */
public ngOnInit(): void { public ngOnInit(): void {
super.setTitle('Motions'); super.setTitle('Motions');
this.workflowArray = this.DS.getAll(Workflow);
this.motionArray = this.DS.getAll(Motion); this.dataSource = new MatTableDataSource();
this.dataSource = new MatTableDataSource(this.motionArray);
this.dataSource.paginator = this.paginator; this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
// Observe DataStore for motions. Initially, executes once for every motion. this.repo.getViewMotionListObservable().subscribe(newMotions => {
// The alternative approach is to put the observable as DataSource to the table this.dataSource.data = newMotions;
this.DS.changeObservable.subscribe(newModel => {
if (newModel instanceof Motion) {
this.motionArray = this.DS.getAll(Motion);
this.dataSource.data = this.motionArray;
}
}); });
} }
@ -115,12 +107,12 @@ export class MotionListComponent extends BaseComponent implements OnInit {
* *
* @param motion The row the user clicked at * @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 }); 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) * TODO Needs to be more accessible (Motion workflow needs adjustment on the server)
* @param state the name of the state * @param state the name of the state
*/ */
@ -142,7 +134,11 @@ export class MotionListComponent extends BaseComponent implements OnInit {
* @param state * @param state
*/ */
public isDisplayIcon(state: WorkflowState): boolean { public isDisplayIcon(state: WorkflowState): boolean {
if (state) {
return state.name === 'accepted' || state.name === 'rejected' || state.name === 'not decided'; return state.name === 'accepted' || state.name === 'rejected' || state.name === 'not decided';
} else {
return false;
}
} }
/** /**

View File

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

View File

@ -1,8 +1,8 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { MotionListComponent } from './motion-list/motion-list.component'; import { MotionListComponent } from './components/motion-list/motion-list.component';
import { MotionDetailComponent } from './motion-detail/motion-detail.component'; import { MotionDetailComponent } from './components/motion-detail/motion-detail.component';
import { CategoryListComponent } from './category-list/category-list.component'; import { CategoryListComponent } from './components/category-list/category-list.component';
const routes: Routes = [ const routes: Routes = [
{ path: '', component: MotionListComponent }, { path: '', component: MotionListComponent },

View File

@ -3,9 +3,9 @@ import { CommonModule } from '@angular/common';
import { MotionsRoutingModule } from './motions-routing.module'; import { MotionsRoutingModule } from './motions-routing.module';
import { SharedModule } from '../../shared/shared.module'; import { SharedModule } from '../../shared/shared.module';
import { MotionListComponent } from './motion-list/motion-list.component'; import { MotionListComponent } from './components/motion-list/motion-list.component';
import { MotionDetailComponent } from './motion-detail/motion-detail.component'; import { MotionDetailComponent } from './components/motion-detail/motion-detail.component';
import { CategoryListComponent } from './category-list/category-list.component'; import { CategoryListComponent } from './components/category-list/category-list.component';
@NgModule({ @NgModule({
imports: [CommonModule, MotionsRoutingModule, SharedModule], imports: [CommonModule, MotionsRoutingModule, SharedModule],

View File

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

View File

@ -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<ViewMotion> } = {};
/**
* Observable subject for the whole list
*/
private viewMotionListSubject: BehaviorSubject<ViewMotion[]> = new BehaviorSubject<ViewMotion[]>(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>(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<any> {
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<ViewMotion> {
if (!this.viewMotionSubjects[id]) {
this.updateViewMotionObservable(id);
}
return this.viewMotionSubjects[id].asObservable();
}
/**
* return the Observable of the whole store
*/
public getViewMotionListObservable(): Observable<ViewMotion[]> {
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<any> {
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<ViewMotion>(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);
}
}