From 9cfc0bbd42badfedd731b12db0ac045661559953 Mon Sep 17 00:00:00 2001 From: FinnStutzenstein Date: Fri, 12 Apr 2019 15:11:12 +0200 Subject: [PATCH] Background structure for assignments - moved some components - added consistend namin scheme: (View)Assignment, (View)AssignmentPoll (View)AssignmentPollOption and (View)AssignmentRelatedUser. - Added precisionPipe, which needs to be added everywhere. - Cleaned up converting deciml fields (as strings in the REST API) to floats - The repository creates the View* structure and enabled user updates. --- .travis.yml | 1 + client/package.json | 2 +- .../assignment-repository.service.ts | 55 +++++++++++--- ...ll-option.ts => assignment-poll-option.ts} | 21 ++---- .../{poll.ts => assignment-poll.ts} | 29 +++----- .../assignments/assignment-related-user.ts | 27 +++++++ .../models/assignments/assignment-user.ts | 41 ----------- .../shared/models/assignments/assignment.ts | 22 +++--- client/src/app/shared/pipes/precision.pipe.ts | 31 ++++++++ client/src/app/shared/shared.module.ts | 12 ++-- .../assignments/assignments-routing.module.ts | 2 +- .../site/assignments/assignments.module.ts | 4 +- .../assignment-detail.component.html | 7 +- .../assignment-detail.component.ts | 24 ++++--- .../assignment-list.component.html | 2 +- .../assignment-list.component.scss | 0 .../assignment-list.component.spec.ts | 2 +- .../assignment-list.component.ts | 13 ++-- .../assignment-poll-dialog.component.html | 0 .../assignment-poll-dialog.component.scss | 0 .../assignment-poll-dialog.component.ts | 10 +-- .../assignment-poll.component.html | 3 +- .../assignment-poll.component.ts | 38 ++++------ .../models/view-assignment-poll-option.ts | 63 ++++++++++++++++ .../models/view-assignment-poll.ts | 71 +++++++++++++++++++ .../models/view-assignment-related-user.ts | 49 +++++++++++++ .../assignments/models/view-assignment.ts | 61 +++++++++++----- .../services/assignment-poll.service.ts | 24 ++++--- client/src/app/site/base/base-view-model.ts | 3 +- client/src/app/site/base/updateable.ts | 5 ++ .../motion-detail/motion-detail.component.ts | 4 +- 31 files changed, 436 insertions(+), 190 deletions(-) rename client/src/app/shared/models/assignments/{poll-option.ts => assignment-poll-option.ts} (56%) rename client/src/app/shared/models/assignments/{poll.ts => assignment-poll.ts} (57%) create mode 100644 client/src/app/shared/models/assignments/assignment-related-user.ts delete mode 100644 client/src/app/shared/models/assignments/assignment-user.ts create mode 100644 client/src/app/shared/pipes/precision.pipe.ts rename client/src/app/site/assignments/{ => components}/assignment-list/assignment-list.component.html (99%) rename client/src/app/site/assignments/{ => components}/assignment-list/assignment-list.component.scss (100%) rename client/src/app/site/assignments/{ => components}/assignment-list/assignment-list.component.spec.ts (91%) rename client/src/app/site/assignments/{ => components}/assignment-list/assignment-list.component.ts (91%) rename client/src/app/site/assignments/components/{assignment-poll => assignment-poll-dialog}/assignment-poll-dialog.component.html (100%) rename client/src/app/site/assignments/components/{assignment-poll => assignment-poll-dialog}/assignment-poll-dialog.component.scss (100%) rename client/src/app/site/assignments/components/{assignment-poll => assignment-poll-dialog}/assignment-poll-dialog.component.ts (91%) create mode 100644 client/src/app/site/assignments/models/view-assignment-poll-option.ts create mode 100644 client/src/app/site/assignments/models/view-assignment-poll.ts create mode 100644 client/src/app/site/assignments/models/view-assignment-related-user.ts create mode 100644 client/src/app/site/base/updateable.ts diff --git a/.travis.yml b/.travis.yml index eecd9f435..f1b8b62ca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -87,6 +87,7 @@ matrix: install: - npm install script: + - npm list --depth=0 || cat --help - npm run prettify-check - language: node_js diff --git a/client/package.json b/client/package.json index 5e570669e..856b02b23 100644 --- a/client/package.json +++ b/client/package.json @@ -79,7 +79,7 @@ "karma-jasmine": "~2.0.1", "karma-jasmine-html-reporter": "^1.4.0", "npm-run-all": "^4.1.5", - "prettier": "^1.16.4", + "prettier": "^1.17.0", "protractor": "^5.4.2", "source-map-explorer": "^1.7.0", "terser": "3.16.1", diff --git a/client/src/app/core/repositories/assignments/assignment-repository.service.ts b/client/src/app/core/repositories/assignments/assignment-repository.service.ts index d055eb8ab..7f62dd8c7 100644 --- a/client/src/app/core/repositories/assignments/assignment-repository.service.ts +++ b/client/src/app/core/repositories/assignments/assignment-repository.service.ts @@ -3,14 +3,14 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { Assignment } from 'app/shared/models/assignments/assignment'; -import { AssignmentUser } from 'app/shared/models/assignments/assignment-user'; +import { AssignmentRelatedUser } from 'app/shared/models/assignments/assignment-related-user'; import { BaseAgendaContentObjectRepository } from '../base-agenda-content-object-repository'; import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service'; import { DataSendService } from 'app/core/core-services/data-send.service'; import { DataStoreService } from '../../core-services/data-store.service'; import { HttpService } from 'app/core/core-services/http.service'; import { Item } from 'app/shared/models/agenda/item'; -import { Poll } from 'app/shared/models/assignments/poll'; +import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll'; import { Tag } from 'app/shared/models/core/tag'; import { User } from 'app/shared/models/users/user'; import { ViewAssignment } from 'app/site/assignments/models/view-assignment'; @@ -18,6 +18,9 @@ import { ViewItem } from 'app/site/agenda/models/view-item'; import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; import { ViewTag } from 'app/site/tags/models/view-tag'; import { ViewUser } from 'app/site/users/models/view-user'; +import { ViewAssignmentRelatedUser } from 'app/site/assignments/models/view-assignment-related-user'; +import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll'; +import { ViewAssignmentPollOption } from 'app/site/assignments/models/view-assignment-poll-option'; /** * Repository Service for Assignments. @@ -69,17 +72,43 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito }; public createViewModel(assignment: Assignment): ViewAssignment { - const relatedUser = this.viewModelStoreService.getMany(ViewUser, assignment.candidates_id); const agendaItem = this.viewModelStoreService.get(ViewItem, assignment.agenda_item_id); const tags = this.viewModelStoreService.getMany(ViewTag, assignment.tags_id); + const assignmentRelatedUsers = this.createViewAssignmentRelatedUsers(assignment.assignment_related_users); + const assignmentPolls = this.createViewAssignmentPolls(assignment.polls); - const viewAssignment = new ViewAssignment(assignment, relatedUser, agendaItem, tags); + const viewAssignment = new ViewAssignment( + assignment, + assignmentRelatedUsers, + assignmentPolls, + agendaItem, + tags + ); viewAssignment.getVerboseName = this.getVerboseName; viewAssignment.getAgendaTitle = () => this.getAgendaTitle(viewAssignment); viewAssignment.getAgendaTitleWithType = () => this.getAgendaTitleWithType(viewAssignment); return viewAssignment; } + private createViewAssignmentRelatedUsers( + assignmentRelatedUsers: AssignmentRelatedUser[] + ): ViewAssignmentRelatedUser[] { + return assignmentRelatedUsers.map(aru => { + const user = this.viewModelStoreService.get(ViewUser, aru.user_id); + return new ViewAssignmentRelatedUser(aru, user); + }); + } + + private createViewAssignmentPolls(assignmentPolls: AssignmentPoll[]): ViewAssignmentPoll[] { + return assignmentPolls.map(poll => { + const options = poll.options.map(option => { + const user = this.viewModelStoreService.get(ViewUser, option.candidate_id); + return new ViewAssignmentPollOption(option, user); + }); + return new ViewAssignmentPoll(poll, options); + }); + } + /** * Adds another user as a candidate * @@ -127,7 +156,7 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito * * @param id id of the poll to delete */ - public async deletePoll(poll: Poll): Promise { + public async deletePoll(poll: ViewAssignmentPoll): Promise { await this.httpService.delete(`${this.restPollPath}${poll.id}/`); } @@ -139,8 +168,8 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito * * TODO: check if votes is untouched */ - public async updatePoll(poll: Partial, originalPoll: Poll): Promise { - const data: Poll = Object.assign(originalPoll, poll); + public async updatePoll(poll: Partial, originalPoll: ViewAssignmentPoll): Promise { + const data: AssignmentPoll = Object.assign(originalPoll.poll, poll); await this.httpService.patch(`${this.restPollPath}${originalPoll.id}/`, data); } @@ -151,7 +180,7 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito * @param poll the updated Poll * @param originalPoll the original poll */ - public async updateVotes(poll: Partial, originalPoll: Poll): Promise { + public async updateVotes(poll: Partial, originalPoll: ViewAssignmentPoll): Promise { poll.options.sort((a, b) => a.weight - b.weight); const votes = poll.options.map(option => { switch (poll.pollmethod) { @@ -185,12 +214,16 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito /** * change the 'elected' state of an election candidate * - * @param user + * @param assignmentRelatedUser * @param assignment * @param elected true if the candidate is to be elected, false if unelected */ - public async markElected(user: AssignmentUser, assignment: ViewAssignment, elected: boolean): Promise { - const data = { user: user.user_id }; + public async markElected( + assignmentRelatedUser: ViewAssignmentRelatedUser, + assignment: ViewAssignment, + elected: boolean + ): Promise { + const data = { user: assignmentRelatedUser.user_id }; if (elected) { await this.httpService.post(this.restPath + assignment.id + this.markElectedPath, data); } else { diff --git a/client/src/app/shared/models/assignments/poll-option.ts b/client/src/app/shared/models/assignments/assignment-poll-option.ts similarity index 56% rename from client/src/app/shared/models/assignments/poll-option.ts rename to client/src/app/shared/models/assignments/assignment-poll-option.ts index 2b2aca993..298c56f3a 100644 --- a/client/src/app/shared/models/assignments/poll-option.ts +++ b/client/src/app/shared/models/assignments/assignment-poll-option.ts @@ -7,12 +7,12 @@ import { PollVoteValue } from 'app/core/ui-services/poll.service'; * part of the 'polls-options'-array in poll * @ignore */ -export class PollOption extends Deserializer { +export class AssignmentPollOption extends Deserializer { public id: number; // The AssignmentUser id of the candidate public candidate_id: number; // the User id of the candidate public is_elected: boolean; public votes: { - weight: number; // TODO arrives as string? + weight: number; // represented as a string because it's a decimal field value: PollVoteValue; }[]; public poll_id: number; @@ -24,21 +24,12 @@ export class PollOption extends Deserializer { * @param input */ public constructor(input?: any) { - // cast stringify numbers - if (typeof input === 'object') { - Object.keys(input).forEach(key => { - if (typeof input[key] === 'string') { - input[key] = parseInt(input[key], 10); + if (input && input.votes) { + input.votes.forEach(vote => { + if (vote.weight) { + vote.weight = parseFloat(vote.weight); } }); - if (input.votes) { - input.votes = input.votes.map(vote => { - return { - value: vote.value, - weight: parseInt(vote.weight, 10) - }; - }); - } } super(input); } diff --git a/client/src/app/shared/models/assignments/poll.ts b/client/src/app/shared/models/assignments/assignment-poll.ts similarity index 57% rename from client/src/app/shared/models/assignments/poll.ts rename to client/src/app/shared/models/assignments/assignment-poll.ts index a27aacc32..20a95203b 100644 --- a/client/src/app/shared/models/assignments/poll.ts +++ b/client/src/app/shared/models/assignments/assignment-poll.ts @@ -1,44 +1,37 @@ import { AssignmentPollMethod } from 'app/site/assignments/services/assignment-poll.service'; import { Deserializer } from '../base/deserializer'; -import { PollOption } from './poll-option'; +import { AssignmentPollOption } from './assignment-poll-option'; /** * Content of the 'polls' property of assignments * @ignore */ -export class Poll extends Deserializer { +export class AssignmentPoll extends Deserializer { + private static DECIMAL_FIELDS = ['votesvalid', 'votesinvalid', 'votescast']; + public id: number; public pollmethod: AssignmentPollMethod; public description: string; public published: boolean; - public options: PollOption[]; + public options: AssignmentPollOption[]; public votesvalid: number; public votesinvalid: number; public votescast: number; public has_votes: boolean; public assignment_id: number; - /** - * (temporary?) storing the base values for percentage calculations, - * to avoid recalculating pollBases too often - * (the calculation iterates through all pollOptions in some use cases) - */ - public pollBase: number; - /** * Needs to be completely optional because assignment has (yet) the optional parameter 'polls' * @param input */ public constructor(input?: any) { // cast stringify numbers - if (typeof input === 'object') { - const numberifyKeys = ['id', 'votesvalid', 'votesinvalid', 'votescast', 'assignment_id']; - - for (const key of Object.keys(input)) { - if (numberifyKeys.includes(key) && typeof input[key] === 'string') { - input[key] = parseInt(input[key], 10); + if (input) { + AssignmentPoll.DECIMAL_FIELDS.forEach(field => { + if (input[field] && typeof input[field] === 'string') { + input[field] = parseFloat(input[field]); } - } + }); } super(input); } @@ -47,7 +40,7 @@ export class Poll extends Deserializer { Object.assign(this, input); this.options = []; if (input.options instanceof Array) { - this.options = input.options.map(pollOptionData => new PollOption(pollOptionData)); + this.options = input.options.map(pollOptionData => new AssignmentPollOption(pollOptionData)); } } } diff --git a/client/src/app/shared/models/assignments/assignment-related-user.ts b/client/src/app/shared/models/assignments/assignment-related-user.ts new file mode 100644 index 000000000..95e718f8d --- /dev/null +++ b/client/src/app/shared/models/assignments/assignment-related-user.ts @@ -0,0 +1,27 @@ +/** + * Content of the 'assignment_related_users' property. + */ +export interface AssignmentRelatedUser { + id: number; + + /** + * id of the user this assignment user relates to + */ + user_id: number; + + /** + * The current 'elected' state + */ + elected: boolean; + + /** + * id of the related assignment + */ + assignment_id: number; + + /** + * A weight to determine the position in the list of candidates + * (determined by the server) + */ + weight: number; +} diff --git a/client/src/app/shared/models/assignments/assignment-user.ts b/client/src/app/shared/models/assignments/assignment-user.ts deleted file mode 100644 index 2c075c015..000000000 --- a/client/src/app/shared/models/assignments/assignment-user.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Deserializer } from '../base/deserializer'; - -/** - * Content of the 'assignment_related_users' property. - * Note that this differs from a ViewUser (e.g. different id) - * @ignore - */ -export class AssignmentUser extends Deserializer { - public id: number; - - /** - * id of the user this assignment user relates to - */ - public user_id: number; - - /** - * The current 'elected' state - */ - public elected: boolean; - - /** - * id of the related assignment - */ - public assignment_id: number; - - /** - * A weight to determine the position in the list of candidates - * (determined by the server) - */ - public weight: number; - - /** - * Constructor. Needs to be completely optional because assignment has - * (yet) the optional parameter 'assignment_related_users' - * - * @param input - */ - public constructor(input?: any) { - super(input); - } -} diff --git a/client/src/app/shared/models/assignments/assignment.ts b/client/src/app/shared/models/assignments/assignment.ts index eff41fc98..f7b3bf9e6 100644 --- a/client/src/app/shared/models/assignments/assignment.ts +++ b/client/src/app/shared/models/assignments/assignment.ts @@ -1,6 +1,6 @@ -import { AssignmentUser } from './assignment-user'; -import { Poll } from './poll'; import { BaseModel } from '../base/base-model'; +import { AssignmentRelatedUser } from './assignment-related-user'; +import { AssignmentPoll } from './assignment-poll'; /** * Representation of an assignment. @@ -8,14 +8,15 @@ import { BaseModel } from '../base/base-model'; */ export class Assignment extends BaseModel { public static COLLECTIONSTRING = 'assignments/assignment'; + public id: number; public title: string; public description: string; public open_posts: number; public phase: number; // see Openslides constants - public assignment_related_users: AssignmentUser[]; + public assignment_related_users: AssignmentRelatedUser[]; public poll_description_default: number; - public polls: Poll[]; + public polls: AssignmentPoll[]; public agenda_item_id: number; public tags_id: number[]; @@ -25,25 +26,18 @@ export class Assignment extends BaseModel { public get candidates_id(): number[] { return this.assignment_related_users - .sort((a: AssignmentUser, b: AssignmentUser) => { + .sort((a: AssignmentRelatedUser, b: AssignmentRelatedUser) => { return a.weight - b.weight; }) - .map((candidate: AssignmentUser) => candidate.user_id); + .map((candidate: AssignmentRelatedUser) => candidate.user_id); } public deserialize(input: any): void { Object.assign(this, input); - this.assignment_related_users = []; - if (input.assignment_related_users instanceof Array) { - this.assignment_related_users = input.assignment_related_users.map( - assignmentUserData => new AssignmentUser(assignmentUserData) - ); - } - this.polls = []; if (input.polls instanceof Array) { - this.polls = input.polls.map(pollData => new Poll(pollData)); + this.polls = input.polls.map(pollData => new AssignmentPoll(pollData)); } } } diff --git a/client/src/app/shared/pipes/precision.pipe.ts b/client/src/app/shared/pipes/precision.pipe.ts new file mode 100644 index 000000000..cb1b3a19a --- /dev/null +++ b/client/src/app/shared/pipes/precision.pipe.ts @@ -0,0 +1,31 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DecimalPipe } from '@angular/common'; + +/** + * Formats floats to have a defined precision. + * + * In this default application, the precision is 0, so no decimal places. Plugins + * may override this pipe to enable more precise votes. + */ +@Pipe({ + name: 'precisionPipe' +}) +export class PrecisionPipe implements PipeTransform { + protected precision: number; + + public constructor(private decimalPipe: DecimalPipe) { + this.precision = 0; + } + + public transform(value: number, precision?: number): string { + if (!precision) { + precision = this.precision; + } + + /** + * Min 1 place before the comma and exactly precision places after. + */ + const digitsInfo = `1.${precision}-${precision}`; + return this.decimalPipe.transform(value, digitsInfo); + } +} diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index afff74077..5de72877a 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { CommonModule } from '@angular/common'; +import { CommonModule, DecimalPipe } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { OwlDateTimeModule, OwlNativeDateTimeModule } from 'ng-pick-datetime'; @@ -81,6 +81,7 @@ import { ProjectorComponent } from './components/projector/projector.component'; import { SlideContainerComponent } from './components/slide-container/slide-container.component'; import { CountdownTimeComponent } from './components/contdown-time/countdown-time.component'; import { MediaUploadContentComponent } from './components/media-upload-content/media-upload-content.component'; +import { PrecisionPipe } from './pipes/precision.pipe'; /** * Share Module for all "dumb" components and pipes. @@ -201,7 +202,8 @@ import { MediaUploadContentComponent } from './components/media-upload-content/m OwlDateTimeModule, OwlNativeDateTimeModule, CountdownTimeComponent, - MediaUploadContentComponent + MediaUploadContentComponent, + PrecisionPipe ], declarations: [ PermsDirective, @@ -228,7 +230,8 @@ import { MediaUploadContentComponent } from './components/media-upload-content/m ProjectorComponent, SlideContainerComponent, CountdownTimeComponent, - MediaUploadContentComponent + MediaUploadContentComponent, + PrecisionPipe ], providers: [ { provide: DateAdapter, useClass: OpenSlidesDateAdapter }, @@ -236,7 +239,8 @@ import { MediaUploadContentComponent } from './components/media-upload-content/m SortingListComponent, SortingTreeComponent, OsSortFilterBarComponent, - OsSortBottomSheetComponent + OsSortBottomSheetComponent, + DecimalPipe ], entryComponents: [OsSortBottomSheetComponent, C4DialogComponent] }) diff --git a/client/src/app/site/assignments/assignments-routing.module.ts b/client/src/app/site/assignments/assignments-routing.module.ts index a181c49fe..91c7efb52 100644 --- a/client/src/app/site/assignments/assignments-routing.module.ts +++ b/client/src/app/site/assignments/assignments-routing.module.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { AssignmentDetailComponent } from './components/assignment-detail/assignment-detail.component'; -import { AssignmentListComponent } from './assignment-list/assignment-list.component'; +import { AssignmentListComponent } from './components/assignment-list/assignment-list.component'; const routes: Routes = [ { path: '', component: AssignmentListComponent, pathMatch: 'full' }, diff --git a/client/src/app/site/assignments/assignments.module.ts b/client/src/app/site/assignments/assignments.module.ts index d542dc4bf..d9a595259 100644 --- a/client/src/app/site/assignments/assignments.module.ts +++ b/client/src/app/site/assignments/assignments.module.ts @@ -2,9 +2,9 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { AssignmentDetailComponent } from './components/assignment-detail/assignment-detail.component'; -import { AssignmentListComponent } from './assignment-list/assignment-list.component'; +import { AssignmentListComponent } from './components/assignment-list/assignment-list.component'; import { AssignmentPollComponent } from './components/assignment-poll/assignment-poll.component'; -import { AssignmentPollDialogComponent } from './components/assignment-poll/assignment-poll-dialog.component'; +import { AssignmentPollDialogComponent } from './components/assignment-poll-dialog/assignment-poll-dialog.component'; import { AssignmentsRoutingModule } from './assignments-routing.module'; import { SharedModule } from '../../shared/shared.module'; diff --git a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html index 2c191f167..414797702 100644 --- a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html +++ b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.html @@ -9,8 +9,7 @@

- Election -   {{ assignment.title }} + {{ assignment.getTitle() }} {{ assignmentForm.get('title').value }}

New election

@@ -47,7 +46,7 @@
-

{{ assignment.title }}

+

{{ assignment.getTitle() }}

@@ -131,7 +130,7 @@
- {{ candidate.username }} + {{ candidate.full_name }}
diff --git a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts index e67b11464..00d654c2b 100644 --- a/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts +++ b/client/src/app/site/assignments/components/assignment-detail/assignment-detail.component.ts @@ -1,4 +1,3 @@ -import { BehaviorSubject } from 'rxjs'; import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { MatSnackBar, MatSelectChange } from '@angular/material'; @@ -6,6 +5,7 @@ import { Router, ActivatedRoute } from '@angular/router'; import { Title } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; +import { BehaviorSubject } from 'rxjs'; import { Assignment } from 'app/shared/models/assignments/assignment'; import { AssignmentPollService } from '../../services/assignment-poll.service'; @@ -15,7 +15,7 @@ import { ConstantsService } from 'app/core/ui-services/constants.service'; import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service'; import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service'; import { OperatorService } from 'app/core/core-services/operator.service'; -import { Poll } from 'app/shared/models/assignments/poll'; +import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll'; import { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service'; import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service'; import { ViewAssignment, AssignmentPhase } from '../../models/view-assignment'; @@ -23,6 +23,7 @@ import { ViewItem } from 'app/site/agenda/models/view-item'; import { ViewportService } from 'app/core/ui-services/viewport.service'; import { ViewTag } from 'app/site/tags/models/view-tag'; import { ViewUser } from 'app/site/users/models/view-user'; +import { PromptService } from 'app/core/ui-services/prompt.service'; /** * Component for the assignment detail view @@ -87,7 +88,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn */ public set assignment(assignment: ViewAssignment) { this._assignment = assignment; - if (this.assignment.polls && this.assignment.polls.length) { + if (this.assignment.polls.length) { this.assignment.polls.forEach(poll => { poll.pollBase = this.pollService.getBaseAmount(poll); }); @@ -148,6 +149,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn * @param pollService * @param agendaRepo * @param tagRepo + * @param promptService */ public constructor( title: Title, @@ -164,7 +166,8 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn private constants: ConstantsService, public pollService: AssignmentPollService, private agendaRepo: ItemRepositoryService, - private tagRepo: TagRepositoryService + private tagRepo: TagRepositoryService, + private promptService: PromptService ) { super(title, translate, matSnackBar); /* Server side constants for phases */ @@ -342,7 +345,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn } else { this.newAssignment = true; // TODO set defaults? - this.assignment = new ViewAssignment(new Assignment()); + this.assignment = new ViewAssignment(new Assignment(), [], []); this.patchForm(this.assignment); this.setEditMode(true); } @@ -350,9 +353,14 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn /** * Handler for deleting the assignment - * TODO: navigating to assignment overview on delete */ - public onDeleteAssignmentButton(): void {} + public async onDeleteAssignmentButton(): Promise { + const title = this.translate.instant('Are you sure you want to delete this election?'); + if (await this.promptService.open(title, this.assignment.getTitle())) { + await this.repo.delete(this.assignment); + this.router.navigate(['../assignments/']); + } + } /** * Handler for actions to be done on change of displayed poll @@ -420,7 +428,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn * Assemble a meaningful label for the poll * TODO (currently e.g. 'Ballot 10 (unublished)') */ - public getPollLabel(poll: Poll, index: number): string { + public getPollLabel(poll: AssignmentPoll, index: number): string { const pubState = poll.published ? this.translate.instant('published') : this.translate.instant('unpublished'); const title = this.translate.instant('Ballot'); return `${title} ${index + 1} (${pubState})`; diff --git a/client/src/app/site/assignments/assignment-list/assignment-list.component.html b/client/src/app/site/assignments/components/assignment-list/assignment-list.component.html similarity index 99% rename from client/src/app/site/assignments/assignment-list/assignment-list.component.html rename to client/src/app/site/assignments/components/assignment-list/assignment-list.component.html index c704a36ff..65ec6778d 100644 --- a/client/src/app/site/assignments/assignment-list/assignment-list.component.html +++ b/client/src/app/site/assignments/components/assignment-list/assignment-list.component.html @@ -38,7 +38,7 @@ Title - {{ assignment.getTitle() }} + {{ assignment.getListTitle() }} diff --git a/client/src/app/site/assignments/assignment-list/assignment-list.component.scss b/client/src/app/site/assignments/components/assignment-list/assignment-list.component.scss similarity index 100% rename from client/src/app/site/assignments/assignment-list/assignment-list.component.scss rename to client/src/app/site/assignments/components/assignment-list/assignment-list.component.scss diff --git a/client/src/app/site/assignments/assignment-list/assignment-list.component.spec.ts b/client/src/app/site/assignments/components/assignment-list/assignment-list.component.spec.ts similarity index 91% rename from client/src/app/site/assignments/assignment-list/assignment-list.component.spec.ts rename to client/src/app/site/assignments/components/assignment-list/assignment-list.component.spec.ts index a059f7e7b..a7b016e44 100644 --- a/client/src/app/site/assignments/assignment-list/assignment-list.component.spec.ts +++ b/client/src/app/site/assignments/components/assignment-list/assignment-list.component.spec.ts @@ -1,7 +1,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { AssignmentListComponent } from './assignment-list.component'; -import { E2EImportsModule } from '../../../../e2e-imports.module'; +import { E2EImportsModule } from '../../../../../e2e-imports.module'; describe('AssignmentListComponent', () => { let component: AssignmentListComponent; diff --git a/client/src/app/site/assignments/assignment-list/assignment-list.component.ts b/client/src/app/site/assignments/components/assignment-list/assignment-list.component.ts similarity index 91% rename from client/src/app/site/assignments/assignment-list/assignment-list.component.ts rename to client/src/app/site/assignments/components/assignment-list/assignment-list.component.ts index 208702a59..a070164d1 100644 --- a/client/src/app/site/assignments/assignment-list/assignment-list.component.ts +++ b/client/src/app/site/assignments/components/assignment-list/assignment-list.component.ts @@ -1,18 +1,19 @@ import { Component, OnInit } from '@angular/core'; import { MatSnackBar } from '@angular/material'; import { Router, ActivatedRoute } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; import { Title } from '@angular/platform-browser'; +import { TranslateService } from '@ngx-translate/core'; + import { Assignment } from 'app/shared/models/assignments/assignment'; -import { AssignmentFilterListService } from '../services/assignment-filter.service'; -import { AssignmentSortListService } from '../services/assignment-sort-list.service'; +import { AssignmentFilterListService } from '../../services/assignment-filter.service'; +import { AssignmentSortListService } from '../../services/assignment-sort-list.service'; import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service'; -import { ListViewBaseComponent } from '../../base/list-view-base'; +import { ListViewBaseComponent } from 'app/site/base/list-view-base'; import { OperatorService } from 'app/core/core-services/operator.service'; import { PromptService } from 'app/core/ui-services/prompt.service'; import { StorageService } from 'app/core/core-services/storage.service'; -import { ViewAssignment } from '../models/view-assignment'; +import { ViewAssignment } from '../../models/view-assignment'; /** * List view for the assignments @@ -97,7 +98,7 @@ export class AssignmentListComponent extends ListViewBaseComponent { const title = this.translate.instant('Are you sure you want to delete all selected elections?'); - if (await this.promptService.open(title, null)) { + if (await this.promptService.open(title, '')) { for (const assignment of this.selectedRows) { await this.repo.delete(assignment); } diff --git a/client/src/app/site/assignments/components/assignment-poll/assignment-poll-dialog.component.html b/client/src/app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component.html similarity index 100% rename from client/src/app/site/assignments/components/assignment-poll/assignment-poll-dialog.component.html rename to client/src/app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component.html diff --git a/client/src/app/site/assignments/components/assignment-poll/assignment-poll-dialog.component.scss b/client/src/app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component.scss similarity index 100% rename from client/src/app/site/assignments/components/assignment-poll/assignment-poll-dialog.component.scss rename to client/src/app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component.scss diff --git a/client/src/app/site/assignments/components/assignment-poll/assignment-poll-dialog.component.ts b/client/src/app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component.ts similarity index 91% rename from client/src/app/site/assignments/components/assignment-poll/assignment-poll-dialog.component.ts rename to client/src/app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component.ts index 21829f280..e9499eacb 100644 --- a/client/src/app/site/assignments/components/assignment-poll/assignment-poll-dialog.component.ts +++ b/client/src/app/site/assignments/components/assignment-poll-dialog/assignment-poll-dialog.component.ts @@ -4,9 +4,9 @@ import { TranslateService } from '@ngx-translate/core'; import { AssignmentPollService } from '../../services/assignment-poll.service'; import { CalculablePollKey, PollVoteValue } from 'app/core/ui-services/poll.service'; -import { Poll } from 'app/shared/models/assignments/poll'; -import { PollOption } from 'app/shared/models/assignments/poll-option'; +import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll'; import { ViewUser } from 'app/site/users/models/view-user'; +import { AssignmentPollOption } from 'app/shared/models/assignments/assignment-poll-option'; /** * Vote entries included once for summary (e.g. total votes cast) @@ -40,7 +40,7 @@ export class AssignmentPollDialogComponent { */ public constructor( public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: { poll: Poll; users: ViewUser[] }, + @Inject(MAT_DIALOG_DATA) public data: { poll: AssignmentPoll; users: ViewUser[] }, private matSnackBar: MatSnackBar, private translate: TranslateService, private pollService: AssignmentPollService @@ -123,7 +123,7 @@ export class AssignmentPollDialogComponent { * @param candidate the candidate for whom to update the value * @param newData the new value */ - public setValue(value: PollVoteValue, candidate: PollOption, newData: string): void { + public setValue(value: PollVoteValue, candidate: AssignmentPollOption, newData: string): void { const vote = candidate.votes.find(v => v.value === value); if (vote) { vote.weight = parseInt(newData, 10); @@ -142,7 +142,7 @@ export class AssignmentPollDialogComponent { * @param candidate the pollOption * @returns the currently entered number or undefined if no number has been set */ - public getValue(value: PollVoteValue, candidate: PollOption): number | undefined { + public getValue(value: PollVoteValue, candidate: AssignmentPollOption): number | undefined { const val = candidate.votes.find(v => v.value === value); return val ? val.weight : undefined; } diff --git a/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.html b/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.html index d2cb6d8a9..8ef843193 100644 --- a/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.html +++ b/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.html @@ -64,7 +64,7 @@
- {{ getCandidateName(option) }} + {{ option.user.full_name }}
@@ -111,6 +111,7 @@ {{ key | translate }}:
+ {{ poll[key] | precisionPipe }} {{ pollService.getSpecialLabel(poll[key]) }}
diff --git a/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts b/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts index 978edcaa0..ff348b0df 100644 --- a/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts +++ b/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts @@ -3,17 +3,18 @@ import { MatDialog, MatSnackBar } from '@angular/material'; import { TranslateService } from '@ngx-translate/core'; -import { AssignmentPollDialogComponent } from './assignment-poll-dialog.component'; +import { AssignmentPollDialogComponent } from '../assignment-poll-dialog/assignment-poll-dialog.component'; import { AssignmentPollService } from '../../services/assignment-poll.service'; import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service'; import { MajorityMethod, CalculablePollKey } from 'app/core/ui-services/poll.service'; import { OperatorService } from 'app/core/core-services/operator.service'; -import { Poll } from 'app/shared/models/assignments/poll'; -import { PollOption } from 'app/shared/models/assignments/poll-option'; +import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll'; import { PromptService } from 'app/core/ui-services/prompt.service'; import { ViewAssignment } from '../../models/view-assignment'; import { BaseViewComponent } from 'app/site/base/base-view'; import { Title } from '@angular/platform-browser'; +import { ViewAssignmentPollOption } from '../../models/view-assignment-poll-option'; +import { ViewAssignmentPoll } from '../../models/view-assignment-poll'; /** * Component for a single assignment poll. Used in assignment detail view @@ -34,7 +35,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit * The poll represented in this component */ @Input() - public poll: Poll; + public poll: ViewAssignmentPoll; /** * The selected Majority method to display quorum calculations. Will be @@ -135,25 +136,10 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit * * TODO Print the ballots for this poll. */ - public printBallot(poll: Poll): void { + public printBallot(poll: AssignmentPoll): void { this.raiseError('Not yet implemented'); } - /** - * Fetches the name for a candidate from the assignment - * - * @param option Any poll option - * @returns the full_name for the candidate - */ - public getCandidateName(option: PollOption): string { - const user = this.assignment.candidates.find(candidate => candidate.id === option.candidate_id); - return user ? user.full_name : ''; - // TODO this.assignment.candidates may not contain every candidates' name (if deleted later) - // so we should rather use this.userRepo.getViewModel(option.id).full_name - // TODO is this name always available? - // TODO error handling - } - /** * Determines whether the candidate has reached the majority needed to pass * the quorum @@ -161,7 +147,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit * @param option * @returns true if the quorum is successfully met */ - public quorumReached(option: PollOption): boolean { + public quorumReached(option: ViewAssignmentPollOption): boolean { const yesValue = this.poll.pollmethod === 'votes' ? 'Votes' : 'Yes'; const amount = option.votes.find(v => v.value === yesValue).weight; const yesQuorum = this.pollService.yesQuorum(this.majorityChoice, this.poll, option); @@ -214,17 +200,17 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit * * @param option */ - public toggleElected(option: PollOption): void { + public toggleElected(option: ViewAssignmentPollOption): void { if (!this.operator.hasPerms('assignments.can_manage')) { return; } // TODO additional conditions: assignment not finished? - const candidate = this.assignment.assignment.assignment_related_users.find( - user => user.user_id === option.candidate_id + const viewAssignmentRelatedUser = this.assignment.assignmentRelatedUsers.find( + user => user.user_id === option.user_id ); - if (candidate) { - this.assignmentRepo.markElected(candidate, this.assignment, !option.is_elected); + if (viewAssignmentRelatedUser) { + this.assignmentRepo.markElected(viewAssignmentRelatedUser, this.assignment, !option.is_elected); } } } diff --git a/client/src/app/site/assignments/models/view-assignment-poll-option.ts b/client/src/app/site/assignments/models/view-assignment-poll-option.ts new file mode 100644 index 000000000..ba48b8a26 --- /dev/null +++ b/client/src/app/site/assignments/models/view-assignment-poll-option.ts @@ -0,0 +1,63 @@ +import { ViewUser } from 'app/site/users/models/view-user'; +import { BaseViewModel } from 'app/site/base/base-view-model'; +import { Updateable } from 'app/site/base/updateable'; +import { Identifiable } from 'app/shared/models/base/identifiable'; +import { PollVoteValue } from 'app/core/ui-services/poll.service'; +import { AssignmentPollOption } from 'app/shared/models/assignments/assignment-poll-option'; + +export class ViewAssignmentPollOption implements Identifiable, Updateable { + private _assignmentPollOption: AssignmentPollOption; + private _user: ViewUser; // This is the "candidate". We'll stay consistent wich user here... + + public get option(): AssignmentPollOption { + return this._assignmentPollOption; + } + + /** + * Note: "User" instead of "candidate" to be consistent. + */ + public get user(): ViewUser | null { + return this._user; + } + + public get id(): number { + return this.option.id; + } + + /** + * Note: "User" instead of "candidate" to be consistent. + */ + public get user_id(): number { + return this.option.candidate_id; + } + + public get is_elected(): boolean { + return this.option.is_elected; + } + + public get votes(): { + weight: number; + value: PollVoteValue; + }[] { + return this.option.votes; + } + + public get poll_id(): number { + return this.option.poll_id; + } + + public get weight(): number { + return this.option.weight; + } + + public constructor(assignmentPollOption: AssignmentPollOption, user?: ViewUser) { + this._assignmentPollOption = assignmentPollOption; + this._user = user; + } + + public updateDependencies(update: BaseViewModel): void { + if (update instanceof ViewUser && update.id === this.user_id) { + this._user = update; + } + } +} diff --git a/client/src/app/site/assignments/models/view-assignment-poll.ts b/client/src/app/site/assignments/models/view-assignment-poll.ts new file mode 100644 index 000000000..9c77105a9 --- /dev/null +++ b/client/src/app/site/assignments/models/view-assignment-poll.ts @@ -0,0 +1,71 @@ +import { BaseViewModel } from 'app/site/base/base-view-model'; +import { Updateable } from 'app/site/base/updateable'; +import { Identifiable } from 'app/shared/models/base/identifiable'; +import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll'; +import { AssignmentPollMethod } from '../services/assignment-poll.service'; +import { ViewAssignmentPollOption } from './view-assignment-poll-option'; + +export class ViewAssignmentPoll implements Identifiable, Updateable { + private _assignmentPoll: AssignmentPoll; + private _assignmentPollOptions: ViewAssignmentPollOption[]; + + public get poll(): AssignmentPoll { + return this._assignmentPoll; + } + + public get options(): ViewAssignmentPollOption[] { + return this._assignmentPollOptions; + } + + public get id(): number { + return this.poll.id; + } + + public get pollmethod(): AssignmentPollMethod { + return this.poll.pollmethod; + } + + public get description(): string { + return this.poll.description; + } + + public get published(): boolean { + return this.poll.published; + } + + public get votesvalid(): number { + return this.poll.votesvalid; + } + + public get votesinvalid(): number { + return this.poll.votesinvalid; + } + + public get votescast(): number { + return this.poll.votescast; + } + + public get has_votes(): boolean { + return this.poll.has_votes; + } + + public get assignment_id(): number { + return this.poll.assignment_id; + } + + /** + * storing the base values for percentage calculations, + * to avoid recalculating pollBases too often + * (the calculation iterates through all pollOptions in some use cases) + */ + public pollBase: number; + + public constructor(assignmentPoll: AssignmentPoll, assignmentPollOptions: ViewAssignmentPollOption[]) { + this._assignmentPoll = assignmentPoll; + this._assignmentPollOptions = assignmentPollOptions; + } + + public updateDependencies(update: BaseViewModel): void { + this.options.forEach(option => option.updateDependencies(update)); + } +} diff --git a/client/src/app/site/assignments/models/view-assignment-related-user.ts b/client/src/app/site/assignments/models/view-assignment-related-user.ts new file mode 100644 index 000000000..179b5b4e5 --- /dev/null +++ b/client/src/app/site/assignments/models/view-assignment-related-user.ts @@ -0,0 +1,49 @@ +import { AssignmentRelatedUser } from 'app/shared/models/assignments/assignment-related-user'; +import { ViewUser } from 'app/site/users/models/view-user'; +import { BaseViewModel } from 'app/site/base/base-view-model'; +import { Updateable } from 'app/site/base/updateable'; +import { Identifiable } from 'app/shared/models/base/identifiable'; + +export class ViewAssignmentRelatedUser implements Updateable, Identifiable { + private _assignmentRelatedUser: AssignmentRelatedUser; + private _user?: ViewUser; + + public get assignmentRelatedUser(): AssignmentRelatedUser { + return this._assignmentRelatedUser; + } + + public get user(): ViewUser { + return this._user; + } + + public get id(): number { + return this.assignmentRelatedUser.id; + } + + public get user_id(): number { + return this.assignmentRelatedUser.user_id; + } + + public get assignment_id(): number { + return this.assignmentRelatedUser.assignment_id; + } + + public get elected(): boolean { + return this.assignmentRelatedUser.elected; + } + + public get weight(): number { + return this.assignmentRelatedUser.weight; + } + + public constructor(assignmentRelatedUser: AssignmentRelatedUser, user?: ViewUser) { + this._assignmentRelatedUser = assignmentRelatedUser; + this._user = user; + } + + public updateDependencies(update: BaseViewModel): void { + if (update instanceof ViewUser && update.id === this.user_id) { + this._user = update; + } + } +} diff --git a/client/src/app/site/assignments/models/view-assignment.ts b/client/src/app/site/assignments/models/view-assignment.ts index 6ba86104d..fc6f7092f 100644 --- a/client/src/app/site/assignments/models/view-assignment.ts +++ b/client/src/app/site/assignments/models/view-assignment.ts @@ -6,7 +6,8 @@ import { ViewUser } from 'app/site/users/models/view-user'; import { ViewItem } from 'app/site/agenda/models/view-item'; import { ViewTag } from 'app/site/tags/models/view-tag'; import { BaseViewModel } from 'app/site/base/base-view-model'; -import { Poll } from 'app/shared/models/assignments/poll'; +import { ViewAssignmentRelatedUser } from './view-assignment-related-user'; +import { ViewAssignmentPoll } from './view-assignment-poll'; export interface AssignmentPhase { value: number; @@ -17,9 +18,10 @@ export class ViewAssignment extends BaseAgendaViewModel { public static COLLECTIONSTRING = Assignment.COLLECTIONSTRING; private _assignment: Assignment; - private _relatedUser: ViewUser[]; - private _agendaItem: ViewItem; - private _tags: ViewTag[]; + private _assignmentRelatedUsers: ViewAssignmentRelatedUser[]; + private _assignmentPolls: ViewAssignmentPoll[]; + private _agendaItem?: ViewItem; + private _tags?: ViewTag[]; public get id(): number { return this._assignment ? this._assignment.id : null; @@ -29,35 +31,39 @@ export class ViewAssignment extends BaseAgendaViewModel { return this._assignment; } + public get polls(): ViewAssignmentPoll[] { + return this._assignmentPolls; + } + public get title(): string { return this.assignment.title; } public get candidates(): ViewUser[] { - return this._relatedUser; + return this._assignmentRelatedUsers.map(aru => aru.user); } - public get agendaItem(): ViewItem { + public get assignmentRelatedUsers(): ViewAssignmentRelatedUser[] { + return this._assignmentRelatedUsers; + } + + public get agendaItem(): ViewItem | null { return this._agendaItem; } public get tags(): ViewTag[] { - return this._tags; + return this._tags || []; } /** * unknown where the identifier to the phase is get */ public get phase(): number { - return this.assignment ? this.assignment.phase : null; + return this.assignment.phase; } public get candidateAmount(): number { - return this.candidates ? this.candidates.length : 0; - } - - public get polls(): Poll[] { - return this.assignment ? this.assignment.polls : []; // TODO check + return this._assignmentRelatedUsers ? this._assignmentRelatedUsers.length : 0; } /** @@ -67,18 +73,37 @@ export class ViewAssignment extends BaseAgendaViewModel { public getAgendaTitle; public getAgendaTitleWithType; - public constructor(assignment: Assignment, relatedUser?: ViewUser[], agendaItem?: ViewItem, tags?: ViewTag[]) { + public constructor( + assignment: Assignment, + assignmentRelatedUsers: ViewAssignmentRelatedUser[], + assignmentPolls: ViewAssignmentPoll[], + agendaItem?: ViewItem, + tags?: ViewTag[] + ) { super(Assignment.COLLECTIONSTRING); - console.log('related user: ', relatedUser); - this._assignment = assignment; - this._relatedUser = relatedUser; + this._assignmentRelatedUsers = assignmentRelatedUsers; + this._assignmentPolls = assignmentPolls; this._agendaItem = agendaItem; this._tags = tags; } - public updateDependencies(update: BaseViewModel): void {} + public updateDependencies(update: BaseViewModel): void { + if (update instanceof ViewItem && update.id === this.assignment.agenda_item_id) { + this._agendaItem = update; + } else if (update instanceof ViewTag && this.assignment.tags_id.includes(update.id)) { + const tagIndex = this._tags.findIndex(_tag => _tag.id === update.id); + if (tagIndex < 0) { + this._tags.push(update); + } else { + this._tags[tagIndex] = update; + } + } else if (update instanceof ViewUser) { + this.assignmentRelatedUsers.forEach(aru => aru.updateDependencies(update)); + this.polls.forEach(poll => poll.updateDependencies(update)); + } + } public getAgendaItem(): ViewItem { return this.agendaItem; diff --git a/client/src/app/site/assignments/services/assignment-poll.service.ts b/client/src/app/site/assignments/services/assignment-poll.service.ts index 1e719e99f..86c3af68e 100644 --- a/client/src/app/site/assignments/services/assignment-poll.service.ts +++ b/client/src/app/site/assignments/services/assignment-poll.service.ts @@ -8,8 +8,8 @@ import { CalculablePollKey, PollVoteValue } from 'app/core/ui-services/poll.service'; -import { Poll } from 'app/shared/models/assignments/poll'; -import { PollOption } from 'app/shared/models/assignments/poll-option'; +import { ViewAssignmentPollOption } from '../models/view-assignment-poll-option'; +import { ViewAssignmentPoll } from '../models/view-assignment-poll'; type AssignmentPollValues = 'auto' | 'votes' | 'yesnoabstain' | 'yesno'; export type AssignmentPollMethod = 'yn' | 'yna' | 'votes'; @@ -69,15 +69,15 @@ export class AssignmentPollService extends PollService { * @param poll * @returns The amount of votes indicating the 100% base */ - public getBaseAmount(poll: Poll): number | null { + public getBaseAmount(poll: ViewAssignmentPoll): number | null { switch (this.percentBase) { case 'DISABLED': return null; case 'YES_NO': case 'YES_NO_ABSTAIN': if (poll.pollmethod === 'votes') { - const yes = poll.options.map(cand => { - const yesValue = cand.votes.find(v => v.value === 'Yes'); + const yes = poll.options.map(option => { + const yesValue = option.votes.find(v => v.value === 'Yes'); return yesValue ? yesValue.weight : -99; }); if (Math.min(...yes) < 0) { @@ -105,7 +105,7 @@ export class AssignmentPollService extends PollService { * @param value * @returns a percentage number with two digits, null if the value cannot be calculated */ - public getPercent(poll: Poll, option: PollOption, value: PollVoteValue): number | null { + public getPercent(poll: ViewAssignmentPoll, option: ViewAssignmentPollOption, value: PollVoteValue): number | null { const base = poll.pollmethod === 'votes' ? poll.pollBase : this.getOptionBaseAmount(poll, option); if (!base) { return null; @@ -125,7 +125,7 @@ export class AssignmentPollService extends PollService { * @returns true if the poll has no percentages, the poll option is a special value, * or if the calculations are disabled in the config */ - public isAbstractOption(poll: Poll, option: PollOption): boolean { + public isAbstractOption(poll: ViewAssignmentPoll, option: ViewAssignmentPollOption): boolean { if (!option.votes || !option.votes.length) { return true; } @@ -146,7 +146,7 @@ export class AssignmentPollService extends PollService { * TODO: Yes, No, etc. in an option will always return true. * Use {@link isAbstractOption} for these */ - public isAbstractValue(poll: Poll, value: CalculablePollKey): boolean { + public isAbstractValue(poll: ViewAssignmentPoll, value: CalculablePollKey): boolean { if (!poll.pollBase || !this.pollValues.includes(value)) { return true; } @@ -163,7 +163,7 @@ export class AssignmentPollService extends PollService { * * @returns an positive integer to be used as percentage base, or null */ - private getOptionBaseAmount(poll: Poll, option: PollOption): number | null { + private getOptionBaseAmount(poll: ViewAssignmentPoll, option: ViewAssignmentPollOption): number | null { if (poll.pollmethod === 'votes') { return null; } @@ -193,7 +193,11 @@ export class AssignmentPollService extends PollService { * @param option * @returns a positive integer number; may return null if quorum is not calculable */ - public yesQuorum(method: MajorityMethod, poll: Poll, option: PollOption): number | null { + public yesQuorum( + method: MajorityMethod, + poll: ViewAssignmentPoll, + option: ViewAssignmentPollOption + ): number | null { const baseAmount = poll.pollmethod === 'votes' ? poll.pollBase : this.getOptionBaseAmount(poll, option); return method.calc(baseAmount); } diff --git a/client/src/app/site/base/base-view-model.ts b/client/src/app/site/base/base-view-model.ts index 6484d6062..54849e52a 100644 --- a/client/src/app/site/base/base-view-model.ts +++ b/client/src/app/site/base/base-view-model.ts @@ -2,6 +2,7 @@ import { Displayable } from './displayable'; import { Identifiable } from '../../shared/models/base/identifiable'; import { Collection } from 'app/shared/models/base/collection'; import { BaseModel } from 'app/shared/models/base/base-model'; +import { Updateable } from './updateable'; export interface ViewModelConstructor { COLLECTIONSTRING: string; @@ -11,7 +12,7 @@ export interface ViewModelConstructor { /** * Base class for view models. alls view models should have titles. */ -export abstract class BaseViewModel implements Displayable, Identifiable, Collection { +export abstract class BaseViewModel implements Displayable, Identifiable, Collection, Updateable { /** * Force children to have an id. */ diff --git a/client/src/app/site/base/updateable.ts b/client/src/app/site/base/updateable.ts new file mode 100644 index 000000000..36a6d6162 --- /dev/null +++ b/client/src/app/site/base/updateable.ts @@ -0,0 +1,5 @@ +import { BaseViewModel } from './base-view-model'; + +export interface Updateable { + updateDependencies(update: BaseViewModel): void; +} diff --git a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts index 42c24e092..85ba5bdbd 100644 --- a/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts +++ b/client/src/app/site/motions/modules/motion-detail/components/motion-detail/motion-detail.component.ts @@ -707,13 +707,13 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit, this.updateWorkflowIdForCreateForm(); const component = this; - this.highlightedLineMatcher = new class implements ErrorStateMatcher { + this.highlightedLineMatcher = new (class implements ErrorStateMatcher { public isErrorState(control: FormControl): boolean { const value: string = control && control.value ? control.value + '' : ''; const maxLineNumber = component.repo.getLastLineNumber(component.motion, component.lineLength); return value.match(/[^\d]/) !== null || parseInt(value, 10) >= maxLineNumber; } - }(); + })(); // create the search motion form this.recommendationExtensionForm = this.formBuilder.group({