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.
This commit is contained in:
FinnStutzenstein 2019-04-12 15:11:12 +02:00
parent 2f9328c2d2
commit 9cfc0bbd42
31 changed files with 436 additions and 190 deletions

View File

@ -87,6 +87,7 @@ matrix:
install:
- npm install
script:
- npm list --depth=0 || cat --help
- npm run prettify-check
- language: node_js

View File

@ -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",

View File

@ -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<void> {
public async deletePoll(poll: ViewAssignmentPoll): Promise<void> {
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<Poll>, originalPoll: Poll): Promise<void> {
const data: Poll = Object.assign(originalPoll, poll);
public async updatePoll(poll: Partial<AssignmentPoll>, originalPoll: ViewAssignmentPoll): Promise<void> {
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<Poll>, originalPoll: Poll): Promise<void> {
public async updateVotes(poll: Partial<AssignmentPoll>, originalPoll: ViewAssignmentPoll): Promise<void> {
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<void> {
const data = { user: user.user_id };
public async markElected(
assignmentRelatedUser: ViewAssignmentRelatedUser,
assignment: ViewAssignment,
elected: boolean
): Promise<void> {
const data = { user: assignmentRelatedUser.user_id };
if (elected) {
await this.httpService.post(this.restPath + assignment.id + this.markElectedPath, data);
} else {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,8 +9,7 @@
<!-- Title -->
<div class="title-slot">
<h2 *ngIf="assignment && !newAssignment">
<span translate>Election</span>
<span>&nbsp;</span> <span *ngIf="!editAssignment">{{ assignment.title }}</span>
<span *ngIf="!editAssignment">{{ assignment.getTitle() }}</span>
<span *ngIf="editAssignment">{{ assignmentForm.get('title').value }}</span>
</h2>
<h2 *ngIf="newAssignment" translate>New election</h2>
@ -47,7 +46,7 @@
<!-- Title -->
<div class="title on-transition-fade" *ngIf="assignment && !editAssignment">
<div class="title-line">
<h1>{{ assignment.title }}</h1>
<h1>{{ assignment.getTitle() }}</h1>
</div>
</div>
<ng-container *ngIf="vp.isMobile; then mobileView; else desktopView"></ng-container>
@ -131,7 +130,7 @@
<div *ngIf="assignment && assignment.candidates">
<!-- TODO: Sorting -->
<div *ngFor="let candidate of assignment.candidates">
<span>{{ candidate.username }}</span>
<span>{{ candidate.full_name }}</span>
<button mat-button *ngIf="hasPerms('addOthers')" (click)="removeUser(candidate)">Remove</button>
</div>
</div>

View File

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

View File

@ -38,7 +38,7 @@
<!-- name column -->
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef mat-sort-header>Title</mat-header-cell>
<mat-cell *matCellDef="let assignment">{{ assignment.getTitle() }}</mat-cell>
<mat-cell *matCellDef="let assignment">{{ assignment.getListTitle() }}</mat-cell>
</ng-container>
<!-- pahse column-->
<ng-container matColumnDef="phase">

View File

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

View File

@ -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<ViewAssignmen
*/
public async deleteSelected(): Promise<void> {
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);
}

View File

@ -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<AssignmentPollDialogComponent>,
@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;
}

View File

@ -64,7 +64,7 @@
</div>
<!-- candidate Name -->
<div>
{{ getCandidateName(option) }}
{{ option.user.full_name }}
</div>
<!-- Votes -->
<div *ngIf="poll.published && poll.has_votes">
@ -111,6 +111,7 @@
<span>{{ key | translate }}</span>:
</div>
<div>
{{ poll[key] | precisionPipe }}
{{ pollService.getSpecialLabel(poll[key]) }}
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<T extends BaseViewModel> {
COLLECTIONSTRING: string;
@ -11,7 +12,7 @@ export interface ViewModelConstructor<T extends BaseViewModel> {
/**
* 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.
*/

View File

@ -0,0 +1,5 @@
import { BaseViewModel } from './base-view-model';
export interface Updateable {
updateDependencies(update: BaseViewModel): void;
}

View File

@ -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({