rework motion submitters to clear confusion about creation and normal update.

Also docs and cleanup
This commit is contained in:
FinnStutzenstein 2018-11-30 09:24:07 +01:00
parent 1de2161ded
commit 212bce1c08
17 changed files with 249 additions and 118 deletions

View File

@ -36,7 +36,7 @@ export class ChoiceService extends OpenSlidesComponent {
): Promise<ChoiceAnswer> {
const dialogRef = this.dialog.open(ChoiceDialogComponent, {
minWidth: '250px',
maxHeight:'90vh',
maxHeight: '90vh',
data: { title: title, choices: choices, multiSelect: multiSelect }
});
return dialogRef.afterClosed().toPromise();

View File

@ -28,5 +28,5 @@
<span translate>Ok</span>
</button>
<button mat-button (click)="closeDialog(false)"><span translate>Cancel</span></button>
</mat-dialog-actions>
</div>
</mat-dialog-actions>
</div>

View File

@ -13,5 +13,5 @@ mat-radio-group {
}
.scrollmenu-outer {
max-height:inherit;
}
max-height: inherit;
}

View File

@ -14,9 +14,23 @@ type ChoiceDialogOption = (Identifiable & Displayable) | (Identifiable & { label
*/
export type ChoiceDialogOptions = (Identifiable & Displayable)[] | (Identifiable & { label: string })[];
/**
* All data needed for this dialog
*/
interface ChoiceDialogData {
/**
* A title to display
*/
title: string;
/**
* The choices to display
*/
choices: ChoiceDialogOptions;
/**
* Select, if this should be a multiselect choice
*/
multiSelect: boolean;
}
@ -50,6 +64,12 @@ export class ChoiceDialogComponent {
@Inject(MAT_DIALOG_DATA) public data: ChoiceDialogData
) {}
/**
* Get the title from a choice. Maybe saved in a label property or using getTitle().
*
* @param choice The choice
* @return the title
*/
public getChoiceTitle(choice: ChoiceDialogOption): string {
if ('label' in choice) {
return choice.label;

View File

@ -23,7 +23,6 @@ export class Motion extends AgendaBaseModel {
public motion_block_id: number;
public origin: string;
public submitters: MotionSubmitter[];
public submitters_id: number[];
public supporters_id: number[];
public comments: MotionComment[];
public workflow_id: number;

View File

@ -9,8 +9,8 @@ import { TranslateService } from '@ngx-translate/core';
import { MotionRepositoryService } from '../../services/motion-repository.service';
import { ViewMotion } from '../../models/view-motion';
import { LinenumberingService } from '../../services/linenumbering.service';
import { Motion } from '../../../../shared/models/motions/motion';
import { BaseViewComponent } from '../../../base/base-view';
import { CreateMotion } from '../../models/create-motion';
/**
* Describes the single paragraphs from the base motion.
@ -167,10 +167,10 @@ export class AmendmentCreateWizardComponent extends BaseViewComponent {
amendment_paragraphs: amendedParagraphs
};
const fromForm = new Motion();
fromForm.deserialize(newMotionValues);
const motion = new CreateMotion();
motion.deserialize(newMotionValues);
const response = await this.repo.create(fromForm);
const response = await this.repo.create(motion);
this.router.navigate(['./motions/' + response.id]);
}
}

View File

@ -29,6 +29,8 @@ import { ConfigService } from '../../../../core/services/config.service';
import { Workflow } from 'app/shared/models/motions/workflow';
import { take, takeWhile, multicast, skipWhile } from 'rxjs/operators';
import { LocalPermissionsService } from '../../services/local-permissions.service';
import { ViewCreateMotion } from '../../models/view-create-motion';
import { CreateMotion } from '../../models/create-motion';
/**
* Component for the motion detail view
@ -260,28 +262,10 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
}
});
// load config variables
this.configService.get('motions_statutes_enabled').subscribe(
(enabled: boolean): void => {
this.statutesEnabled = enabled;
}
);
this.configService.get('motions_min_supporters').subscribe(
(supporters: number): void => {
this.minSupporters = supporters;
}
);
this.configService.get('motions_preamble').subscribe(
(preamble: string): void => {
this.preamble = preamble;
}
);
this.configService.get('motions_amendments_enabled').subscribe(
(enabled: boolean): void => {
this.amendmentsEnabled = enabled;
}
);
this.configService.get('motions_statutes_enabled').subscribe(enabled => (this.statutesEnabled = enabled));
this.configService.get('motions_min_supporters').subscribe(supporters => (this.minSupporters = supporters));
this.configService.get('motions_preamble').subscribe(preamble => (this.preamble = preamble));
this.configService.get('motions_amendments_enabled').subscribe(enabled => (this.amendmentsEnabled = enabled));
}
/**
@ -327,8 +311,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
// creates a new motion
this.newMotion = true;
this.editMotion = true;
this.motion = new ViewMotion();
this.motionCopy = new ViewMotion();
this.motion = new ViewCreateMotion();
this.motionCopy = new ViewCreateMotion();
} else {
// load existing motion
this.route.params.subscribe(params => {
@ -393,7 +377,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
state_id: [''],
recommendation_id: [''],
submitters_id: [],
supporters_id: [],
supporters_id: [[]],
workflow_id: [],
origin: ['']
});
@ -419,45 +403,65 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
}
/**
* Save a motion. Calls the "patchValues" function in the MotionObject
*
* http:post the motion to the server.
* The AutoUpdate-Service should see a change once it arrives and show it
* in the list view automatically
* Before updating or creating, the motions needs to be prepared for paragraph based amendments.
* A motion of type T is created, prepared and deserialized from the given motionValues
*
* @param motionValues valus for the new motion
* @param ctor The motion constructor, so different motion types can be created.
*/
public async saveMotion(): Promise<void> {
const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value };
const fromForm = new Motion();
private prepareMotionForSave<T extends Motion>(motionValues: any, ctor: new (...args: any[]) => T): T {
const motion = new ctor();
if (this.motion.isParagraphBasedAmendment()) {
fromForm.amendment_paragraphs = this.motion.amendment_paragraphs.map(
(para: string): string => {
if (para === null) {
motion.amendment_paragraphs = this.motion.amendment_paragraphs.map(
(paragraph: string): string => {
if (paragraph === null) {
return null;
} else {
return newMotionValues.text;
return motionValues.text;
}
}
);
newMotionValues.text = '';
motionValues.text = '';
}
fromForm.deserialize(newMotionValues);
motion.deserialize(motionValues);
return motion;
}
/**
* Creates a motion. Calls the "patchValues" function in the MotionObject
*/
public async createMotion(): Promise<void> {
const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value };
const motion = this.prepareMotionForSave(newMotionValues, CreateMotion);
try {
if (this.newMotion) {
const response = await this.repo.create(fromForm);
this.router.navigate(['./motions/' + response.id]);
} else {
await this.repo.update(fromForm, this.motionCopy);
// if the motion was successfully updated, change the edit mode.
this.editMotion = false;
}
const response = await this.repo.create(motion);
this.router.navigate(['./motions/' + response.id]);
} catch (e) {
this.raiseError(e);
}
}
/**
* Save a motion. Calls the "patchValues" function in the MotionObject
*/
public async updateMotion(): Promise<void> {
const newMotionValues = { ...this.metaInfoForm.value, ...this.contentForm.value };
const motion = this.prepareMotionForSave(newMotionValues, Motion);
this.repo.update(motion, this.motionCopy).then(() => (this.editMotion = false), this.raiseError);
}
/**
* In the ui are no distinct buttons for update or create. This is decided here.
*/
public saveMotion(): void {
if (this.newMotion) {
this.createMotion();
} else {
this.updateMotion();
}
}
/**
* get the formated motion text from the repository.
*/

View File

@ -42,7 +42,10 @@
<mat-cell *matCellDef="let motion">
<div class="innerTable">
<span class="motion-list-title">{{ motion.title }}</span> <br />
<span class="motion-list-from"> <span translate>by</span> {{ motion.submitters }} </span> <br />
<span class="motion-list-from" *ngIf="motion.submitters.length">
<span translate>by</span> {{ motion.submitters }}
</span>
<br *ngIf="motion.submitters.length" />
<!-- state -->
<mat-basic-chip
[ngClass]="{
@ -135,37 +138,37 @@
<mat-icon>sort</mat-icon>
<span translate>Move to agenda item</span>
</button>
<button mat-menu-item (click)="multiselectService.setStatus(selectedRows); toggleMultiSelect()">
<button mat-menu-item (click)="multiselectWrapper(multiselectService.setStatus(selectedRows))">
<mat-icon>label</mat-icon>
<span translate>Set status</span>
</button>
<button mat-menu-item (click)="multiselectService.setRecommendation(selectedRows); toggleMultiSelect()">
<button mat-menu-item (click)="multiselectWrapper(multiselectService.setRecommendation(selectedRows))">
<mat-icon>report</mat-icon>
<!-- TODO: better icon -->
<span translate>Set recommendation</span>
</button>
<button mat-menu-item (click)="multiselectService.setCategory(selectedRows); toggleMultiSelect()">
<button mat-menu-item (click)="multiselectWrapper(multiselectService.setCategory(selectedRows))">
<mat-icon>device_hub</mat-icon>
<!-- TODO: icon -->
<span translate>Set categories</span>
</button>
<mat-divider></mat-divider>
<button mat-menu-item (click)="multiselectService.addSubmitters(selectedRows); toggleMultiSelect()">
<button mat-menu-item (click)="multiselectWrapper(multiselectService.addSubmitters(selectedRows))">
<mat-icon>person_add</mat-icon>
<!-- TODO: icon -->
<span translate>Add submitters</span>
</button>
<button mat-menu-item (click)="multiselectService.removeSubmitters(selectedRows); toggleMultiSelect()">
<button mat-menu-item (click)="multiselectWrapper(multiselectService.removeSubmitters(selectedRows))">
<mat-icon>person_outline</mat-icon>
<!-- TODO: icon -->
<span translate>remove submitters</span>
</button>
<button mat-menu-item (click)="multiselectService.addTags(selectedRows); toggleMultiSelect()">
<button mat-menu-item (click)="multiselectWrapper(multiselectService.addTags(selectedRows))">
<mat-icon>bookmarks</mat-icon>
<!-- TODO: icon -->
<span translate>Add tags</span>
</button>
<button mat-menu-item (click)="multiselectService.removeTags(selectedRows); toggleMultiSelect()">
<button mat-menu-item (click)="multiselectWrapper(multiselectService.removeTags(selectedRows))">
<mat-icon>bookmark_border</mat-icon>
<!-- TODO: icon -->
<span translate>Remove tags</span>

View File

@ -172,4 +172,13 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
}
return this.columnsToDisplayMinWidth;
}
/**
* Wraps multiselect actions to close the multiselect mode or throw an error if one happens.
*
* @param multiselectPromise The promise returned by multiselect actions.
*/
public multiselectWrapper(multiselectPromise: Promise<void>): void {
multiselectPromise.then(() => this.toggleMultiSelect());
}
}

View File

@ -0,0 +1,13 @@
import { Motion } from 'app/shared/models/motions/motion';
/**
* Representation of Motion during creation. The submitters_id is added to send this information
* as an array of user ids to the server.
*/
export class CreateMotion extends Motion {
public submitters_id: number[];
public constructor(input?: any) {
super(input);
}
}

View File

@ -0,0 +1,62 @@
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 { Item } from 'app/shared/models/agenda/item';
import { MotionBlock } from 'app/shared/models/motions/motion-block';
import { ViewMotion } from './view-motion';
import { CreateMotion } from './create-motion';
/**
* Create motion class for the View. Its different to ViewMotion in fact that the submitter handling is different
* on motion creation.
*
* @ignore
*/
export class ViewCreateMotion extends ViewMotion {
protected _motion: CreateMotion;
public get motion(): CreateMotion {
return this._motion;
}
public get submitters(): User[] {
return this._submitters;
}
public get submitters_id(): number[] {
return this.motion ? this.motion.submitters_id : null;
}
public set submitters(users: User[]) {
this._submitters = users;
this._motion.submitters_id = users.map(user => user.id);
}
public constructor(
motion?: CreateMotion,
category?: Category,
submitters?: User[],
supporters?: User[],
workflow?: Workflow,
state?: WorkflowState,
item?: Item,
block?: MotionBlock
) {
super(motion, category, submitters, supporters, workflow, state, item, block);
}
/**
* Duplicate this motion into a copy of itself
*/
public copy(): ViewCreateMotion {
return new ViewCreateMotion(
this._motion,
this._category,
this._submitters,
this._supporters,
this._workflow,
this._state
);
}
}

View File

@ -31,14 +31,14 @@ export enum ChangeRecoMode {
* @ignore
*/
export class ViewMotion extends BaseViewModel {
private _motion: Motion;
private _category: Category;
private _submitters: User[];
private _supporters: User[];
private _workflow: Workflow;
private _state: WorkflowState;
private _item: Item;
private _block: MotionBlock;
protected _motion: Motion;
protected _category: Category;
protected _submitters: User[];
protected _supporters: User[];
protected _workflow: Workflow;
protected _state: WorkflowState;
protected _item: Item;
protected _block: MotionBlock;
/**
* Indicates the LineNumberingMode Mode.
@ -126,7 +126,7 @@ export class ViewMotion extends BaseViewModel {
}
public get submitters_id(): number[] {
return this.motion ? this.motion.submitters_id : null;
return this.motion ? this.motion.submitterIds : null;
}
public get supporters(): User[] {
@ -153,10 +153,6 @@ export class ViewMotion extends BaseViewModel {
return this.motion && this.motion.state_id ? this.motion.state_id : null;
}
public get possibleStates(): WorkflowState[] {
return this.workflow ? this.workflow.states : null;
}
public get recommendation_id(): number {
return this.motion && this.motion.recommendation_id ? this.motion.recommendation_id : null;
}
@ -188,11 +184,6 @@ export class ViewMotion extends BaseViewModel {
this._motion.supporters_id = users.map(user => user.id);
}
public set submitters(users: User[]) {
this._submitters = users;
this._motion.submitters_id = users.map(user => user.id);
}
public get item(): Item {
return this._item;
}

View File

@ -12,13 +12,8 @@ import { BaseViewModel } from '../../base/base-view-model';
export class ViewWorkflow extends BaseViewModel {
private _workflow: Workflow;
public constructor(workflow?: Workflow, id?: number, name?: string) {
public constructor(workflow?: Workflow) {
super();
if (!workflow) {
workflow = new Workflow();
workflow.id = id;
workflow.name = name;
}
this._workflow = workflow;
}

View File

@ -10,6 +10,7 @@ import { UserRepositoryService } from 'app/site/users/services/user-repository.s
import { WorkflowRepositoryService } from './workflow-repository.service';
import { CategoryRepositoryService } from './category-repository.service';
import { TagRepositoryService } from 'app/site/tags/services/tag-repository.service';
import { HttpService } from 'app/core/services/http.service';
/**
* Contains all multiselect actions for the motion list view.
@ -38,7 +39,8 @@ export class MotionMultiselectService {
private userRepo: UserRepositoryService,
private workflowRepo: WorkflowRepositoryService,
private categoryRepo: CategoryRepositoryService,
private tagRepo: TagRepositoryService
private tagRepo: TagRepositoryService,
private httpService: HttpService
) {}
/**
@ -88,11 +90,16 @@ export class MotionMultiselectService {
id: workflowState.id,
label: workflowState.recommendation_label
}));
choices.push({ id: 0, label: 'Delete recommendation' });
const selectedChoice = await this.choiceService.open(title, choices);
if (selectedChoice) {
for (const motion of motions) {
await this.repo.setRecommendation(motion, selectedChoice as number);
}
if (typeof selectedChoice === 'number') {
const requestData = motions.map(motion => ({
id: motion.id,
recommendation: selectedChoice
}));
await this.httpService.post('/rest/motions/motion/manage_multiple_recommendation', {
motions: requestData
});
}
}
@ -120,7 +127,15 @@ export class MotionMultiselectService {
const title = this.translate.instant('This will add the following submitters of all selected motions:');
const selectedChoice = await this.choiceService.open(title, this.userRepo.getViewModelList(), true);
if (selectedChoice) {
throw new Error('Not implemented on the server');
const requestData = motions.map(motion => {
let submitterIds = [...motion.submitters_id, ...(selectedChoice as number[])];
submitterIds = submitterIds.filter((id, index, self) => self.indexOf(id) === index); // remove duplicates
return {
id: motion.id,
submitters: submitterIds
};
});
await this.httpService.post('/rest/motions/motion/manage_multiple_submitters', { motions: requestData });
}
}
@ -133,7 +148,15 @@ export class MotionMultiselectService {
const title = this.translate.instant('This will remove the following submitters from all selected motions:');
const selectedChoice = await this.choiceService.open(title, this.userRepo.getViewModelList(), true);
if (selectedChoice) {
throw new Error('Not implemented on the server');
const requestData = motions.map(motion => {
const submitterIdsToRemove = selectedChoice as number[];
const submitterIds = motion.submitters_id.filter(id => !submitterIdsToRemove.includes(id));
return {
id: motion.id,
submitters: submitterIds
};
});
await this.httpService.post('/rest/motions/motion/manage_multiple_submitters', { motions: requestData });
}
}
@ -146,11 +169,15 @@ export class MotionMultiselectService {
const title = this.translate.instant('This will add the following tags to all selected motions:');
const selectedChoice = await this.choiceService.open(title, this.tagRepo.getViewModelList(), true);
if (selectedChoice) {
for (const motion of motions) {
const requestData = motions.map(motion => {
let tagIds = [...motion.tags_id, ...(selectedChoice as number[])];
tagIds = tagIds.filter((id, index, self) => self.indexOf(id) === index);
await this.repo.update({ tags_id: tagIds }, motion);
}
tagIds = tagIds.filter((id, index, self) => self.indexOf(id) === index); // remove duplicates
return {
id: motion.id,
tags: tagIds
};
});
await this.httpService.post('/rest/motions/motion/manage_multiple_tags', { motions: requestData });
}
}
@ -163,11 +190,15 @@ export class MotionMultiselectService {
const title = this.translate.instant('This will remove the following tags from all selected motions:');
const selectedChoice = await this.choiceService.open(title, this.tagRepo.getViewModelList(), true);
if (selectedChoice) {
for (const motion of motions) {
const requestData = motions.map(motion => {
const tagIdsToRemove = selectedChoice as number[];
const tagIds = motion.tags_id.filter(id => !tagIdsToRemove.includes(id));
await this.repo.update({ tags_id: tagIds }, motion);
}
return {
id: motion.id,
tags: tagIds
};
});
await this.httpService.post('/rest/motions/motion/manage_multiple_tags', { motions: requestData });
}
}
}

View File

@ -25,6 +25,7 @@ import { Item } from 'app/shared/models/agenda/item';
import { OSTreeSortEvent } from 'app/shared/components/sorting-tree/sorting-tree.component';
import { TreeService } from 'app/core/services/tree.service';
import { ViewMotionAmendedParagraph } from '../models/view-motion-amended-paragraph';
import { CreateMotion } from '../models/create-motion';
/**
* Repository Services for motions (and potentially categories)
@ -109,12 +110,8 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
*
* @param update the form data containing the updated values
* @param viewMotion The View Motion. If not present, a new motion will be created
* TODO: Remove the viewMotion and make it actually distignuishable from save()
*/
public async create(motion: Motion): Promise<Identifiable> {
if (!motion.supporters_id) {
delete motion.supporters_id;
}
public async create(motion: CreateMotion): Promise<Identifiable> {
return await this.dataSend.createModel(motion);
}

View File

@ -29,7 +29,7 @@ export class WorkflowRepositoryService extends BaseRepository<ViewWorkflow, Work
* @param dataSend
*/
public constructor(
protected DS: DataStoreService,
DS: DataStoreService,
mapperService: CollectionStringModelMapperService,
private dataSend: DataSendService
) {
@ -61,17 +61,11 @@ export class WorkflowRepositoryService extends BaseRepository<ViewWorkflow, Work
}
/**
* Returns the workflow for the ID
* @param workflow_id workflow ID
* Collects all existing states from all workflows
*/
public getWorkflowByID(workflow_id: number): Workflow {
const wfList = this.DS.getAll(Workflow);
return wfList.find(workflow => workflow.id === workflow_id);
}
public getAllWorkflowStates(): WorkflowState[] {
let states: WorkflowState[] = [];
this.DS.getAll(Workflow).forEach(workflow => {
this.getViewModelList().forEach(workflow => {
states = states.concat(workflow.states);
});
return states;

View File

@ -105,12 +105,25 @@ export class UserRepositoryService extends BaseRepository<ViewUser, User> {
return pw;
}
/**
* Updates the default password and sets the real password.
*
* @param user The user to update
* @param password The password to set
*/
public async resetPassword(user: ViewUser, password: string): Promise<void> {
await this.update({ default_password: password }, user);
const path = `/rest/users/user/${user.id}/reset_password/`;
await this.httpService.post(path, { password: password });
}
/**
* Sends invitation emails to all given users. Returns a prepared string to show the user.
* This string should always be shown, becuase even in success cases, some users may not get
* an email and the user should be notified about this.
*
* @param users All affected users
*/
public async sendInvitationEmail(users: ViewUser[]): Promise<string> {
const user_ids = users.map(user => user.id);
const subject = this.translate.instant(this.configService.instant('users_email_subject'));