Merge pull request #4594 from FinnStutzenstein/modelStructureForAssignments

Background structure for assignments
This commit is contained in:
Finn Stutzenstein 2019-04-15 10:43:54 +02:00 committed by GitHub
commit 259afa7f88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 436 additions and 190 deletions

View File

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

View File

@ -79,7 +79,7 @@
"karma-jasmine": "~2.0.1", "karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.0", "karma-jasmine-html-reporter": "^1.4.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^1.16.4", "prettier": "^1.17.0",
"protractor": "^5.4.2", "protractor": "^5.4.2",
"source-map-explorer": "^1.7.0", "source-map-explorer": "^1.7.0",
"terser": "3.16.1", "terser": "3.16.1",

View File

@ -3,14 +3,14 @@ import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Assignment } from 'app/shared/models/assignments/assignment'; 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 { BaseAgendaContentObjectRepository } from '../base-agenda-content-object-repository';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service'; import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
import { DataSendService } from 'app/core/core-services/data-send.service'; import { DataSendService } from 'app/core/core-services/data-send.service';
import { DataStoreService } from '../../core-services/data-store.service'; import { DataStoreService } from '../../core-services/data-store.service';
import { HttpService } from 'app/core/core-services/http.service'; import { HttpService } from 'app/core/core-services/http.service';
import { Item } from 'app/shared/models/agenda/item'; 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 { Tag } from 'app/shared/models/core/tag';
import { User } from 'app/shared/models/users/user'; import { User } from 'app/shared/models/users/user';
import { ViewAssignment } from 'app/site/assignments/models/view-assignment'; 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 { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewTag } from 'app/site/tags/models/view-tag'; import { ViewTag } from 'app/site/tags/models/view-tag';
import { ViewUser } from 'app/site/users/models/view-user'; 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. * Repository Service for Assignments.
@ -69,17 +72,43 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
}; };
public createViewModel(assignment: Assignment): ViewAssignment { 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 agendaItem = this.viewModelStoreService.get(ViewItem, assignment.agenda_item_id);
const tags = this.viewModelStoreService.getMany(ViewTag, assignment.tags_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.getVerboseName = this.getVerboseName;
viewAssignment.getAgendaTitle = () => this.getAgendaTitle(viewAssignment); viewAssignment.getAgendaTitle = () => this.getAgendaTitle(viewAssignment);
viewAssignment.getAgendaTitleWithType = () => this.getAgendaTitleWithType(viewAssignment); viewAssignment.getAgendaTitleWithType = () => this.getAgendaTitleWithType(viewAssignment);
return 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 * Adds another user as a candidate
* *
@ -127,7 +156,7 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
* *
* @param id id of the poll to delete * @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}/`); await this.httpService.delete(`${this.restPollPath}${poll.id}/`);
} }
@ -139,8 +168,8 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
* *
* TODO: check if votes is untouched * TODO: check if votes is untouched
*/ */
public async updatePoll(poll: Partial<Poll>, originalPoll: Poll): Promise<void> { public async updatePoll(poll: Partial<AssignmentPoll>, originalPoll: ViewAssignmentPoll): Promise<void> {
const data: Poll = Object.assign(originalPoll, poll); const data: AssignmentPoll = Object.assign(originalPoll.poll, poll);
await this.httpService.patch(`${this.restPollPath}${originalPoll.id}/`, data); await this.httpService.patch(`${this.restPollPath}${originalPoll.id}/`, data);
} }
@ -151,7 +180,7 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
* @param poll the updated Poll * @param poll the updated Poll
* @param originalPoll the original 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); poll.options.sort((a, b) => a.weight - b.weight);
const votes = poll.options.map(option => { const votes = poll.options.map(option => {
switch (poll.pollmethod) { switch (poll.pollmethod) {
@ -185,12 +214,16 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito
/** /**
* change the 'elected' state of an election candidate * change the 'elected' state of an election candidate
* *
* @param user * @param assignmentRelatedUser
* @param assignment * @param assignment
* @param elected true if the candidate is to be elected, false if unelected * @param elected true if the candidate is to be elected, false if unelected
*/ */
public async markElected(user: AssignmentUser, assignment: ViewAssignment, elected: boolean): Promise<void> { public async markElected(
const data = { user: user.user_id }; assignmentRelatedUser: ViewAssignmentRelatedUser,
assignment: ViewAssignment,
elected: boolean
): Promise<void> {
const data = { user: assignmentRelatedUser.user_id };
if (elected) { if (elected) {
await this.httpService.post(this.restPath + assignment.id + this.markElectedPath, data); await this.httpService.post(this.restPath + assignment.id + this.markElectedPath, data);
} else { } else {

View File

@ -7,12 +7,12 @@ import { PollVoteValue } from 'app/core/ui-services/poll.service';
* part of the 'polls-options'-array in poll * part of the 'polls-options'-array in poll
* @ignore * @ignore
*/ */
export class PollOption extends Deserializer { export class AssignmentPollOption extends Deserializer {
public id: number; // The AssignmentUser id of the candidate public id: number; // The AssignmentUser id of the candidate
public candidate_id: number; // the User id of the candidate public candidate_id: number; // the User id of the candidate
public is_elected: boolean; public is_elected: boolean;
public votes: { public votes: {
weight: number; // TODO arrives as string? weight: number; // represented as a string because it's a decimal field
value: PollVoteValue; value: PollVoteValue;
}[]; }[];
public poll_id: number; public poll_id: number;
@ -24,21 +24,12 @@ export class PollOption extends Deserializer {
* @param input * @param input
*/ */
public constructor(input?: any) { public constructor(input?: any) {
// cast stringify numbers if (input && input.votes) {
if (typeof input === 'object') { input.votes.forEach(vote => {
Object.keys(input).forEach(key => { if (vote.weight) {
if (typeof input[key] === 'string') { vote.weight = parseFloat(vote.weight);
input[key] = parseInt(input[key], 10);
} }
}); });
if (input.votes) {
input.votes = input.votes.map(vote => {
return {
value: vote.value,
weight: parseInt(vote.weight, 10)
};
});
}
} }
super(input); super(input);
} }

View File

@ -1,44 +1,37 @@
import { AssignmentPollMethod } from 'app/site/assignments/services/assignment-poll.service'; import { AssignmentPollMethod } from 'app/site/assignments/services/assignment-poll.service';
import { Deserializer } from '../base/deserializer'; import { Deserializer } from '../base/deserializer';
import { PollOption } from './poll-option'; import { AssignmentPollOption } from './assignment-poll-option';
/** /**
* Content of the 'polls' property of assignments * Content of the 'polls' property of assignments
* @ignore * @ignore
*/ */
export class Poll extends Deserializer { export class AssignmentPoll extends Deserializer {
private static DECIMAL_FIELDS = ['votesvalid', 'votesinvalid', 'votescast'];
public id: number; public id: number;
public pollmethod: AssignmentPollMethod; public pollmethod: AssignmentPollMethod;
public description: string; public description: string;
public published: boolean; public published: boolean;
public options: PollOption[]; public options: AssignmentPollOption[];
public votesvalid: number; public votesvalid: number;
public votesinvalid: number; public votesinvalid: number;
public votescast: number; public votescast: number;
public has_votes: boolean; public has_votes: boolean;
public assignment_id: number; 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' * Needs to be completely optional because assignment has (yet) the optional parameter 'polls'
* @param input * @param input
*/ */
public constructor(input?: any) { public constructor(input?: any) {
// cast stringify numbers // cast stringify numbers
if (typeof input === 'object') { if (input) {
const numberifyKeys = ['id', 'votesvalid', 'votesinvalid', 'votescast', 'assignment_id']; AssignmentPoll.DECIMAL_FIELDS.forEach(field => {
if (input[field] && typeof input[field] === 'string') {
for (const key of Object.keys(input)) { input[field] = parseFloat(input[field]);
if (numberifyKeys.includes(key) && typeof input[key] === 'string') {
input[key] = parseInt(input[key], 10);
} }
} });
} }
super(input); super(input);
} }
@ -47,7 +40,7 @@ export class Poll extends Deserializer {
Object.assign(this, input); Object.assign(this, input);
this.options = []; this.options = [];
if (input.options instanceof Array) { 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 { BaseModel } from '../base/base-model';
import { AssignmentRelatedUser } from './assignment-related-user';
import { AssignmentPoll } from './assignment-poll';
/** /**
* Representation of an assignment. * Representation of an assignment.
@ -8,14 +8,15 @@ import { BaseModel } from '../base/base-model';
*/ */
export class Assignment extends BaseModel<Assignment> { export class Assignment extends BaseModel<Assignment> {
public static COLLECTIONSTRING = 'assignments/assignment'; public static COLLECTIONSTRING = 'assignments/assignment';
public id: number; public id: number;
public title: string; public title: string;
public description: string; public description: string;
public open_posts: number; public open_posts: number;
public phase: number; // see Openslides constants public phase: number; // see Openslides constants
public assignment_related_users: AssignmentUser[]; public assignment_related_users: AssignmentRelatedUser[];
public poll_description_default: number; public poll_description_default: number;
public polls: Poll[]; public polls: AssignmentPoll[];
public agenda_item_id: number; public agenda_item_id: number;
public tags_id: number[]; public tags_id: number[];
@ -25,25 +26,18 @@ export class Assignment extends BaseModel<Assignment> {
public get candidates_id(): number[] { public get candidates_id(): number[] {
return this.assignment_related_users return this.assignment_related_users
.sort((a: AssignmentUser, b: AssignmentUser) => { .sort((a: AssignmentRelatedUser, b: AssignmentRelatedUser) => {
return a.weight - b.weight; return a.weight - b.weight;
}) })
.map((candidate: AssignmentUser) => candidate.user_id); .map((candidate: AssignmentRelatedUser) => candidate.user_id);
} }
public deserialize(input: any): void { public deserialize(input: any): void {
Object.assign(this, input); 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 = []; this.polls = [];
if (input.polls instanceof Array) { 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 { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { CommonModule } from '@angular/common'; import { CommonModule, DecimalPipe } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { OwlDateTimeModule, OwlNativeDateTimeModule } from 'ng-pick-datetime'; 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 { SlideContainerComponent } from './components/slide-container/slide-container.component';
import { CountdownTimeComponent } from './components/contdown-time/countdown-time.component'; import { CountdownTimeComponent } from './components/contdown-time/countdown-time.component';
import { MediaUploadContentComponent } from './components/media-upload-content/media-upload-content.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. * Share Module for all "dumb" components and pipes.
@ -201,7 +202,8 @@ import { MediaUploadContentComponent } from './components/media-upload-content/m
OwlDateTimeModule, OwlDateTimeModule,
OwlNativeDateTimeModule, OwlNativeDateTimeModule,
CountdownTimeComponent, CountdownTimeComponent,
MediaUploadContentComponent MediaUploadContentComponent,
PrecisionPipe
], ],
declarations: [ declarations: [
PermsDirective, PermsDirective,
@ -228,7 +230,8 @@ import { MediaUploadContentComponent } from './components/media-upload-content/m
ProjectorComponent, ProjectorComponent,
SlideContainerComponent, SlideContainerComponent,
CountdownTimeComponent, CountdownTimeComponent,
MediaUploadContentComponent MediaUploadContentComponent,
PrecisionPipe
], ],
providers: [ providers: [
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter }, { provide: DateAdapter, useClass: OpenSlidesDateAdapter },
@ -236,7 +239,8 @@ import { MediaUploadContentComponent } from './components/media-upload-content/m
SortingListComponent, SortingListComponent,
SortingTreeComponent, SortingTreeComponent,
OsSortFilterBarComponent, OsSortFilterBarComponent,
OsSortBottomSheetComponent OsSortBottomSheetComponent,
DecimalPipe
], ],
entryComponents: [OsSortBottomSheetComponent, C4DialogComponent] entryComponents: [OsSortBottomSheetComponent, C4DialogComponent]
}) })

View File

@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { AssignmentDetailComponent } from './components/assignment-detail/assignment-detail.component'; 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 = [ const routes: Routes = [
{ path: '', component: AssignmentListComponent, pathMatch: 'full' }, { path: '', component: AssignmentListComponent, pathMatch: 'full' },

View File

@ -2,9 +2,9 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { AssignmentDetailComponent } from './components/assignment-detail/assignment-detail.component'; 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 { 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 { AssignmentsRoutingModule } from './assignments-routing.module';
import { SharedModule } from '../../shared/shared.module'; import { SharedModule } from '../../shared/shared.module';

View File

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

View File

@ -1,4 +1,3 @@
import { BehaviorSubject } from 'rxjs';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { MatSnackBar, MatSelectChange } from '@angular/material'; import { MatSnackBar, MatSelectChange } from '@angular/material';
@ -6,6 +5,7 @@ import { Router, ActivatedRoute } from '@angular/router';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject } from 'rxjs';
import { Assignment } from 'app/shared/models/assignments/assignment'; import { Assignment } from 'app/shared/models/assignments/assignment';
import { AssignmentPollService } from '../../services/assignment-poll.service'; 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 { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service'; import { LocalPermissionsService } from 'app/site/motions/services/local-permissions.service';
import { OperatorService } from 'app/core/core-services/operator.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 { TagRepositoryService } from 'app/core/repositories/tags/tag-repository.service';
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service'; import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
import { ViewAssignment, AssignmentPhase } from '../../models/view-assignment'; 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 { ViewportService } from 'app/core/ui-services/viewport.service';
import { ViewTag } from 'app/site/tags/models/view-tag'; import { ViewTag } from 'app/site/tags/models/view-tag';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
import { PromptService } from 'app/core/ui-services/prompt.service';
/** /**
* Component for the assignment detail view * Component for the assignment detail view
@ -87,7 +88,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
*/ */
public set assignment(assignment: ViewAssignment) { public set assignment(assignment: ViewAssignment) {
this._assignment = assignment; this._assignment = assignment;
if (this.assignment.polls && this.assignment.polls.length) { if (this.assignment.polls.length) {
this.assignment.polls.forEach(poll => { this.assignment.polls.forEach(poll => {
poll.pollBase = this.pollService.getBaseAmount(poll); poll.pollBase = this.pollService.getBaseAmount(poll);
}); });
@ -148,6 +149,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
* @param pollService * @param pollService
* @param agendaRepo * @param agendaRepo
* @param tagRepo * @param tagRepo
* @param promptService
*/ */
public constructor( public constructor(
title: Title, title: Title,
@ -164,7 +166,8 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
private constants: ConstantsService, private constants: ConstantsService,
public pollService: AssignmentPollService, public pollService: AssignmentPollService,
private agendaRepo: ItemRepositoryService, private agendaRepo: ItemRepositoryService,
private tagRepo: TagRepositoryService private tagRepo: TagRepositoryService,
private promptService: PromptService
) { ) {
super(title, translate, matSnackBar); super(title, translate, matSnackBar);
/* Server side constants for phases */ /* Server side constants for phases */
@ -342,7 +345,7 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
} else { } else {
this.newAssignment = true; this.newAssignment = true;
// TODO set defaults? // TODO set defaults?
this.assignment = new ViewAssignment(new Assignment()); this.assignment = new ViewAssignment(new Assignment(), [], []);
this.patchForm(this.assignment); this.patchForm(this.assignment);
this.setEditMode(true); this.setEditMode(true);
} }
@ -350,9 +353,14 @@ export class AssignmentDetailComponent extends BaseViewComponent implements OnIn
/** /**
* Handler for deleting the assignment * 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 * 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 * Assemble a meaningful label for the poll
* TODO (currently e.g. 'Ballot 10 (unublished)') * 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 pubState = poll.published ? this.translate.instant('published') : this.translate.instant('unpublished');
const title = this.translate.instant('Ballot'); const title = this.translate.instant('Ballot');
return `${title} ${index + 1} (${pubState})`; return `${title} ${index + 1} (${pubState})`;

View File

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

View File

@ -1,7 +1,7 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AssignmentListComponent } from './assignment-list.component'; import { AssignmentListComponent } from './assignment-list.component';
import { E2EImportsModule } from '../../../../e2e-imports.module'; import { E2EImportsModule } from '../../../../../e2e-imports.module';
describe('AssignmentListComponent', () => { describe('AssignmentListComponent', () => {
let component: AssignmentListComponent; let component: AssignmentListComponent;

View File

@ -1,18 +1,19 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar } from '@angular/material';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { Assignment } from 'app/shared/models/assignments/assignment'; import { Assignment } from 'app/shared/models/assignments/assignment';
import { AssignmentFilterListService } from '../services/assignment-filter.service'; import { AssignmentFilterListService } from '../../services/assignment-filter.service';
import { AssignmentSortListService } from '../services/assignment-sort-list.service'; import { AssignmentSortListService } from '../../services/assignment-sort-list.service';
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.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 { OperatorService } from 'app/core/core-services/operator.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { StorageService } from 'app/core/core-services/storage.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 * List view for the assignments
@ -97,7 +98,7 @@ export class AssignmentListComponent extends ListViewBaseComponent<ViewAssignmen
*/ */
public async deleteSelected(): Promise<void> { public async deleteSelected(): Promise<void> {
const title = this.translate.instant('Are you sure you want to delete all selected elections?'); 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) { for (const assignment of this.selectedRows) {
await this.repo.delete(assignment); 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 { AssignmentPollService } from '../../services/assignment-poll.service';
import { CalculablePollKey, PollVoteValue } from 'app/core/ui-services/poll.service'; import { CalculablePollKey, PollVoteValue } from 'app/core/ui-services/poll.service';
import { Poll } from 'app/shared/models/assignments/poll'; import { AssignmentPoll } from 'app/shared/models/assignments/assignment-poll';
import { PollOption } from 'app/shared/models/assignments/poll-option';
import { ViewUser } from 'app/site/users/models/view-user'; 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) * Vote entries included once for summary (e.g. total votes cast)
@ -40,7 +40,7 @@ export class AssignmentPollDialogComponent {
*/ */
public constructor( public constructor(
public dialogRef: MatDialogRef<AssignmentPollDialogComponent>, 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 matSnackBar: MatSnackBar,
private translate: TranslateService, private translate: TranslateService,
private pollService: AssignmentPollService private pollService: AssignmentPollService
@ -123,7 +123,7 @@ export class AssignmentPollDialogComponent {
* @param candidate the candidate for whom to update the value * @param candidate the candidate for whom to update the value
* @param newData the new 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); const vote = candidate.votes.find(v => v.value === value);
if (vote) { if (vote) {
vote.weight = parseInt(newData, 10); vote.weight = parseInt(newData, 10);
@ -142,7 +142,7 @@ export class AssignmentPollDialogComponent {
* @param candidate the pollOption * @param candidate the pollOption
* @returns the currently entered number or undefined if no number has been set * @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); const val = candidate.votes.find(v => v.value === value);
return val ? val.weight : undefined; return val ? val.weight : undefined;
} }

View File

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

View File

@ -3,17 +3,18 @@ import { MatDialog, MatSnackBar } from '@angular/material';
import { TranslateService } from '@ngx-translate/core'; 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 { AssignmentPollService } from '../../services/assignment-poll.service';
import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service'; import { AssignmentRepositoryService } from 'app/core/repositories/assignments/assignment-repository.service';
import { MajorityMethod, CalculablePollKey } from 'app/core/ui-services/poll.service'; import { MajorityMethod, CalculablePollKey } from 'app/core/ui-services/poll.service';
import { OperatorService } from 'app/core/core-services/operator.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 { PollOption } from 'app/shared/models/assignments/poll-option';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { ViewAssignment } from '../../models/view-assignment'; import { ViewAssignment } from '../../models/view-assignment';
import { BaseViewComponent } from 'app/site/base/base-view'; import { BaseViewComponent } from 'app/site/base/base-view';
import { Title } from '@angular/platform-browser'; 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 * 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 * The poll represented in this component
*/ */
@Input() @Input()
public poll: Poll; public poll: ViewAssignmentPoll;
/** /**
* The selected Majority method to display quorum calculations. Will be * 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. * TODO Print the ballots for this poll.
*/ */
public printBallot(poll: Poll): void { public printBallot(poll: AssignmentPoll): void {
this.raiseError('Not yet implemented'); 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 * Determines whether the candidate has reached the majority needed to pass
* the quorum * the quorum
@ -161,7 +147,7 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
* @param option * @param option
* @returns true if the quorum is successfully met * @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 yesValue = this.poll.pollmethod === 'votes' ? 'Votes' : 'Yes';
const amount = option.votes.find(v => v.value === yesValue).weight; const amount = option.votes.find(v => v.value === yesValue).weight;
const yesQuorum = this.pollService.yesQuorum(this.majorityChoice, this.poll, option); const yesQuorum = this.pollService.yesQuorum(this.majorityChoice, this.poll, option);
@ -214,17 +200,17 @@ export class AssignmentPollComponent extends BaseViewComponent implements OnInit
* *
* @param option * @param option
*/ */
public toggleElected(option: PollOption): void { public toggleElected(option: ViewAssignmentPollOption): void {
if (!this.operator.hasPerms('assignments.can_manage')) { if (!this.operator.hasPerms('assignments.can_manage')) {
return; return;
} }
// TODO additional conditions: assignment not finished? // TODO additional conditions: assignment not finished?
const candidate = this.assignment.assignment.assignment_related_users.find( const viewAssignmentRelatedUser = this.assignment.assignmentRelatedUsers.find(
user => user.user_id === option.candidate_id user => user.user_id === option.user_id
); );
if (candidate) { if (viewAssignmentRelatedUser) {
this.assignmentRepo.markElected(candidate, this.assignment, !option.is_elected); 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 { ViewItem } from 'app/site/agenda/models/view-item';
import { ViewTag } from 'app/site/tags/models/view-tag'; import { ViewTag } from 'app/site/tags/models/view-tag';
import { BaseViewModel } from 'app/site/base/base-view-model'; 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 { export interface AssignmentPhase {
value: number; value: number;
@ -17,9 +18,10 @@ export class ViewAssignment extends BaseAgendaViewModel {
public static COLLECTIONSTRING = Assignment.COLLECTIONSTRING; public static COLLECTIONSTRING = Assignment.COLLECTIONSTRING;
private _assignment: Assignment; private _assignment: Assignment;
private _relatedUser: ViewUser[]; private _assignmentRelatedUsers: ViewAssignmentRelatedUser[];
private _agendaItem: ViewItem; private _assignmentPolls: ViewAssignmentPoll[];
private _tags: ViewTag[]; private _agendaItem?: ViewItem;
private _tags?: ViewTag[];
public get id(): number { public get id(): number {
return this._assignment ? this._assignment.id : null; return this._assignment ? this._assignment.id : null;
@ -29,35 +31,39 @@ export class ViewAssignment extends BaseAgendaViewModel {
return this._assignment; return this._assignment;
} }
public get polls(): ViewAssignmentPoll[] {
return this._assignmentPolls;
}
public get title(): string { public get title(): string {
return this.assignment.title; return this.assignment.title;
} }
public get candidates(): ViewUser[] { 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; return this._agendaItem;
} }
public get tags(): ViewTag[] { public get tags(): ViewTag[] {
return this._tags; return this._tags || [];
} }
/** /**
* unknown where the identifier to the phase is get * unknown where the identifier to the phase is get
*/ */
public get phase(): number { public get phase(): number {
return this.assignment ? this.assignment.phase : null; return this.assignment.phase;
} }
public get candidateAmount(): number { public get candidateAmount(): number {
return this.candidates ? this.candidates.length : 0; return this._assignmentRelatedUsers ? this._assignmentRelatedUsers.length : 0;
}
public get polls(): Poll[] {
return this.assignment ? this.assignment.polls : []; // TODO check
} }
/** /**
@ -67,18 +73,37 @@ export class ViewAssignment extends BaseAgendaViewModel {
public getAgendaTitle; public getAgendaTitle;
public getAgendaTitleWithType; 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); super(Assignment.COLLECTIONSTRING);
console.log('related user: ', relatedUser);
this._assignment = assignment; this._assignment = assignment;
this._relatedUser = relatedUser; this._assignmentRelatedUsers = assignmentRelatedUsers;
this._assignmentPolls = assignmentPolls;
this._agendaItem = agendaItem; this._agendaItem = agendaItem;
this._tags = tags; 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 { public getAgendaItem(): ViewItem {
return this.agendaItem; return this.agendaItem;

View File

@ -8,8 +8,8 @@ import {
CalculablePollKey, CalculablePollKey,
PollVoteValue PollVoteValue
} from 'app/core/ui-services/poll.service'; } from 'app/core/ui-services/poll.service';
import { Poll } from 'app/shared/models/assignments/poll'; import { ViewAssignmentPollOption } from '../models/view-assignment-poll-option';
import { PollOption } from 'app/shared/models/assignments/poll-option'; import { ViewAssignmentPoll } from '../models/view-assignment-poll';
type AssignmentPollValues = 'auto' | 'votes' | 'yesnoabstain' | 'yesno'; type AssignmentPollValues = 'auto' | 'votes' | 'yesnoabstain' | 'yesno';
export type AssignmentPollMethod = 'yn' | 'yna' | 'votes'; export type AssignmentPollMethod = 'yn' | 'yna' | 'votes';
@ -69,15 +69,15 @@ export class AssignmentPollService extends PollService {
* @param poll * @param poll
* @returns The amount of votes indicating the 100% base * @returns The amount of votes indicating the 100% base
*/ */
public getBaseAmount(poll: Poll): number | null { public getBaseAmount(poll: ViewAssignmentPoll): number | null {
switch (this.percentBase) { switch (this.percentBase) {
case 'DISABLED': case 'DISABLED':
return null; return null;
case 'YES_NO': case 'YES_NO':
case 'YES_NO_ABSTAIN': case 'YES_NO_ABSTAIN':
if (poll.pollmethod === 'votes') { if (poll.pollmethod === 'votes') {
const yes = poll.options.map(cand => { const yes = poll.options.map(option => {
const yesValue = cand.votes.find(v => v.value === 'Yes'); const yesValue = option.votes.find(v => v.value === 'Yes');
return yesValue ? yesValue.weight : -99; return yesValue ? yesValue.weight : -99;
}); });
if (Math.min(...yes) < 0) { if (Math.min(...yes) < 0) {
@ -105,7 +105,7 @@ export class AssignmentPollService extends PollService {
* @param value * @param value
* @returns a percentage number with two digits, null if the value cannot be calculated * @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); const base = poll.pollmethod === 'votes' ? poll.pollBase : this.getOptionBaseAmount(poll, option);
if (!base) { if (!base) {
return null; 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, * @returns true if the poll has no percentages, the poll option is a special value,
* or if the calculations are disabled in the config * 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) { if (!option.votes || !option.votes.length) {
return true; return true;
} }
@ -146,7 +146,7 @@ export class AssignmentPollService extends PollService {
* TODO: Yes, No, etc. in an option will always return true. * TODO: Yes, No, etc. in an option will always return true.
* Use {@link isAbstractOption} for these * 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)) { if (!poll.pollBase || !this.pollValues.includes(value)) {
return true; return true;
} }
@ -163,7 +163,7 @@ export class AssignmentPollService extends PollService {
* *
* @returns an positive integer to be used as percentage base, or null * @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') { if (poll.pollmethod === 'votes') {
return null; return null;
} }
@ -193,7 +193,11 @@ export class AssignmentPollService extends PollService {
* @param option * @param option
* @returns a positive integer number; may return null if quorum is not calculable * @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); const baseAmount = poll.pollmethod === 'votes' ? poll.pollBase : this.getOptionBaseAmount(poll, option);
return method.calc(baseAmount); return method.calc(baseAmount);
} }

View File

@ -2,6 +2,7 @@ import { Displayable } from './displayable';
import { Identifiable } from '../../shared/models/base/identifiable'; import { Identifiable } from '../../shared/models/base/identifiable';
import { Collection } from 'app/shared/models/base/collection'; import { Collection } from 'app/shared/models/base/collection';
import { BaseModel } from 'app/shared/models/base/base-model'; import { BaseModel } from 'app/shared/models/base/base-model';
import { Updateable } from './updateable';
export interface ViewModelConstructor<T extends BaseViewModel> { export interface ViewModelConstructor<T extends BaseViewModel> {
COLLECTIONSTRING: string; COLLECTIONSTRING: string;
@ -11,7 +12,7 @@ export interface ViewModelConstructor<T extends BaseViewModel> {
/** /**
* Base class for view models. alls view models should have titles. * 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. * 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(); this.updateWorkflowIdForCreateForm();
const component = this; const component = this;
this.highlightedLineMatcher = new class implements ErrorStateMatcher { this.highlightedLineMatcher = new (class implements ErrorStateMatcher {
public isErrorState(control: FormControl): boolean { public isErrorState(control: FormControl): boolean {
const value: string = control && control.value ? control.value + '' : ''; const value: string = control && control.value ? control.value + '' : '';
const maxLineNumber = component.repo.getLastLineNumber(component.motion, component.lineLength); const maxLineNumber = component.repo.getLastLineNumber(component.motion, component.lineLength);
return value.match(/[^\d]/) !== null || parseInt(value, 10) >= maxLineNumber; return value.match(/[^\d]/) !== null || parseInt(value, 10) >= maxLineNumber;
} }
}(); })();
// create the search motion form // create the search motion form
this.recommendationExtensionForm = this.formBuilder.group({ this.recommendationExtensionForm = this.formBuilder.group({