diff --git a/client/src/app/core/repositories/assignments/assignment-repository.service.ts b/client/src/app/core/repositories/assignments/assignment-repository.service.ts index db13ec64b..d055eb8ab 100644 --- a/client/src/app/core/repositories/assignments/assignment-repository.service.ts +++ b/client/src/app/core/repositories/assignments/assignment-repository.service.ts @@ -10,6 +10,7 @@ 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 { Tag } from 'app/shared/models/core/tag'; import { User } from 'app/shared/models/users/user'; import { ViewAssignment } from 'app/site/assignments/models/view-assignment'; @@ -17,7 +18,6 @@ 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 { Poll } from 'app/shared/models/assignments/poll'; /** * Repository Service for Assignments. @@ -28,14 +28,22 @@ import { Poll } from 'app/shared/models/assignments/poll'; providedIn: 'root' }) export class AssignmentRepositoryService extends BaseAgendaContentObjectRepository { + private readonly restPath = '/rest/assignments/assignment/'; + private readonly restPollPath = '/rest/assignments/poll/'; + private readonly candidatureOtherPath = '/candidature_other/'; + private readonly candidatureSelfPath = '/candidature_self/'; + private readonly createPollPath = '/create_poll/'; + private readonly markElectedPath = '/mark_elected/'; + /** * Constructor for the Assignment Repository. * - * @param DS The DataStore - * @param mapperService Maps collection strings to classes - * @param viewModelStoreService - * @param translate - * @param httpService + * @param DS DataStore access + * @param dataSend Sending data + * @param mapperService Map models to object + * @param viewModelStoreService Access view models + * @param translate Translate string + * @param httpService make HTTP Requests */ public constructor( DS: DataStoreService, @@ -76,55 +84,42 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito * Adds another user as a candidate * * @param userId User id of a candidate - * @param assignment + * @param assignment The assignment to add the candidate to */ - public async addCandidate(userId: number, assignment: ViewAssignment): Promise { - const restPath = `/rest/assignments/assignment/${assignment.id}/candidature_other/`; + public async changeCandidate(userId: number, assignment: ViewAssignment): Promise { const data = { user: userId }; - await this.httpService.post(restPath, data); - } - - /** - * Removes an user from the list of candidates for an assignment - * - * @param user note: AssignmentUser, not a ViewUser - * @param assignment - */ - public async deleteCandidate(user: AssignmentUser, assignment: ViewAssignment): Promise { - const restPath = `/rest/assignments/assignment/${assignment.id}/candidature_other/`; - const data = { user: user.id }; - await this.httpService.delete(restPath, data); + if (assignment.candidates.some(candidate => candidate.id === userId)) { + await this.httpService.delete(this.restPath + assignment.id + this.candidatureOtherPath, data); + } else { + await this.httpService.post(this.restPath + assignment.id + this.candidatureOtherPath, data); + } } /** * Add the operator as candidate to the assignment * - * @param assignment + * @param assignment The assignment to add the candidate to */ public async addSelf(assignment: ViewAssignment): Promise { - const restPath = `/rest/assignments/assignment/${assignment.id}/candidature_self/`; - await this.httpService.post(restPath); + await this.httpService.post(this.restPath + assignment.id + this.candidatureSelfPath); } /** * Removes the current user (operator) from the list of candidates for an assignment * - * @param assignment + * @param assignment The assignment to remove ourself from */ public async deleteSelf(assignment: ViewAssignment): Promise { - const restPath = `/rest/assignments/assignment/${assignment.id}/candidature_self/`; - await this.httpService.delete(restPath); + await this.httpService.delete(this.restPath + assignment.id + this.candidatureSelfPath); } /** * Creates a new Poll to a given assignment * - * @param assignment + * @param assignment The assignment to add the poll to */ public async addPoll(assignment: ViewAssignment): Promise { - const restPath = `/rest/assignments/assignment/${assignment.id}/create_poll/`; - await this.httpService.post(restPath); - // TODO set phase, too, if phase was 0. Should be done server side? + await this.httpService.post(this.restPath + assignment.id + this.createPollPath); } /** @@ -133,8 +128,7 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito * @param id id of the poll to delete */ public async deletePoll(poll: Poll): Promise { - const restPath = `/rest/assignments/poll/${poll.id}/`; - await this.httpService.delete(restPath); + await this.httpService.delete(`${this.restPollPath}${poll.id}/`); } /** @@ -143,12 +137,11 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito * @param poll the (partial) data to update * @param originalPoll the poll to update * - * TODO check if votes is untouched + * TODO: check if votes is untouched */ public async updatePoll(poll: Partial, originalPoll: Poll): Promise { - const restPath = `/rest/assignments/poll/${originalPoll.id}/`; const data: Poll = Object.assign(originalPoll, poll); - await this.httpService.patch(restPath, data); + await this.httpService.patch(`${this.restPollPath}${originalPoll.id}/`, data); } /** @@ -186,8 +179,7 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito votesno: null, votesvalid: poll.votesvalid || null }; - const restPath = `/rest/assignments/poll/${originalPoll.id}/`; - await this.httpService.put(restPath, data); + await this.httpService.put(`${this.restPollPath}${originalPoll.id}/`, data); } /** @@ -198,12 +190,11 @@ export class AssignmentRepositoryService extends BaseAgendaContentObjectReposito * @param elected true if the candidate is to be elected, false if unelected */ public async markElected(user: AssignmentUser, assignment: ViewAssignment, elected: boolean): Promise { - const restPath = `/rest/assignments/assignment/${assignment.id}/mark_elected/`; const data = { user: user.user_id }; if (elected) { - await this.httpService.post(restPath, data); + await this.httpService.post(this.restPath + assignment.id + this.markElectedPath, data); } else { - await this.httpService.delete(restPath, data); + await this.httpService.delete(this.restPath + assignment.id + this.markElectedPath, data); } } diff --git a/client/src/app/core/ui-services/poll.service.ts b/client/src/app/core/ui-services/poll.service.ts index 951c0561c..185e81e72 100644 --- a/client/src/app/core/ui-services/poll.service.ts +++ b/client/src/app/core/ui-services/poll.service.ts @@ -6,7 +6,6 @@ import { _ } from 'app/core/translate/translation-marker'; * The possible keys of a poll object that represent numbers. * TODO Should be 'key of MotionPoll if type of key is number' * TODO: normalize MotionPoll model and other poll models - * TODO: reuse more motion-poll-service stuff */ export type CalculablePollKey = 'votesvalid' | 'votesinvalid' | 'votescast' | 'yes' | 'no' | 'abstain'; @@ -102,9 +101,9 @@ export abstract class PollService { /** * Gets an icon for a Poll Key * - * @param key + * @param key yes, no, abstain or something like that * @returns a string for material-icons to represent the icon for - * this key(e.g. yes: positiv sign, no: negative sign) + * this key(e.g. yes: positive sign, no: negative sign) */ public getIcon(key: CalculablePollKey): string { switch (key) { @@ -128,6 +127,7 @@ export abstract class PollService { /** * Gets a label for a poll Key * + * @param key yes, no, abstain or something like that * @returns A short descriptive name for the poll keys */ public getLabel(key: CalculablePollKey | PollVoteValue): string { @@ -151,11 +151,11 @@ export abstract class PollService { /** * retrieve special labels for a poll value - * - * @param value - * @returns the label for a non-positive value, according to * {@link specialPollVotes}. Positive values will return as string * representation of themselves + * + * @param value check value for special numbers + * @returns the label for a non-positive value, according to */ public getSpecialLabel(value: number): string { if (value >= 0) { @@ -166,10 +166,9 @@ export abstract class PollService { } /** - * Get the progressbar class for a decision key - * - * @param key + * Get the progress bar class for a decision key * + * @param key a calculable poll key (like yes or no) * @returns a css class designing a progress bar in a color, or an empty string */ public getProgressBarColor(key: CalculablePollKey | PollVoteValue): string { diff --git a/client/src/app/shared/models/assignments/poll-option.ts b/client/src/app/shared/models/assignments/poll-option.ts index 8f7e1b038..2b2aca993 100644 --- a/client/src/app/shared/models/assignments/poll-option.ts +++ b/client/src/app/shared/models/assignments/poll-option.ts @@ -32,10 +32,10 @@ export class PollOption extends Deserializer { } }); if (input.votes) { - input.votes = input.votes.map(v => { + input.votes = input.votes.map(vote => { return { - value: v.value, - weight: parseInt(v.weight, 10) + value: vote.value, + weight: parseInt(vote.weight, 10) }; }); } diff --git a/client/src/app/shared/models/assignments/poll.ts b/client/src/app/shared/models/assignments/poll.ts index b68a4f127..a27aacc32 100644 --- a/client/src/app/shared/models/assignments/poll.ts +++ b/client/src/app/shared/models/assignments/poll.ts @@ -33,11 +33,12 @@ export class Poll extends Deserializer { // cast stringify numbers if (typeof input === 'object') { const numberifyKeys = ['id', 'votesvalid', 'votesinvalid', 'votescast', 'assignment_id']; - Object.keys(input).forEach(key => { + + for (const key of Object.keys(input)) { if (numberifyKeys.includes(key) && typeof input[key] === 'string') { input[key] = parseInt(input[key], 10); } - }); + } } super(input); } diff --git a/client/src/app/site/assignments/assignment-list/assignment-list.component.html b/client/src/app/site/assignments/assignment-list/assignment-list.component.html index d834cbcf3..c704a36ff 100644 --- a/client/src/app/site/assignments/assignment-list/assignment-list.component.html +++ b/client/src/app/site/assignments/assignment-list/assignment-list.component.html @@ -1,4 +1,8 @@ - +

Elections

@@ -91,6 +95,10 @@ done_all Select all + diff --git a/client/src/app/site/assignments/components/assignment-poll/assignment-poll-dialog.component.ts b/client/src/app/site/assignments/components/assignment-poll/assignment-poll-dialog.component.ts index dbc89a387..21829f280 100644 --- a/client/src/app/site/assignments/components/assignment-poll/assignment-poll-dialog.component.ts +++ b/client/src/app/site/assignments/components/assignment-poll/assignment-poll-dialog.component.ts @@ -69,7 +69,7 @@ export class AssignmentPollDialogComponent { /** * Validates candidates input (every candidate has their options filled in), - * submits and closes the dialog if successfull, else displays an error popup. + * submits and closes the dialog if successful, else displays an error popup. * TODO better validation */ public submit(): void { @@ -123,14 +123,14 @@ 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: number): void { + public setValue(value: PollVoteValue, candidate: PollOption, newData: string): void { const vote = candidate.votes.find(v => v.value === value); if (vote) { - vote.weight = +newData; + vote.weight = parseInt(newData, 10); } else { candidate.votes.push({ value: value, - weight: +newData + 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 { + public getValue(value: PollVoteValue, candidate: PollOption): number | undefined { const val = candidate.votes.find(v => v.value === value); return val ? val.weight : undefined; } @@ -151,10 +151,10 @@ export class AssignmentPollDialogComponent { * Retrieves a per-poll value * * @param value - * @returns integer or null + * @returns integer or undefined */ - public getSumValue(value: summaryPollKeys): number | null { - return this.data.poll[value] || null; + public getSumValue(value: summaryPollKeys): number | undefined { + return this.data.poll[value] || undefined; } /** @@ -164,6 +164,6 @@ export class AssignmentPollDialogComponent { * @param weight */ public setSumValue(value: summaryPollKeys, weight: string): void { - this.data.poll[value] = +weight; + this.data.poll[value] = parseInt(weight, 10); } } diff --git a/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts b/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts index 4009d338f..978edcaa0 100644 --- a/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts +++ b/client/src/app/site/assignments/components/assignment-poll/assignment-poll.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, Input } from '@angular/core'; -import { MatDialog } from '@angular/material'; +import { MatDialog, MatSnackBar } from '@angular/material'; import { TranslateService } from '@ngx-translate/core'; @@ -12,17 +12,18 @@ import { Poll } from 'app/shared/models/assignments/poll'; import { PollOption } from 'app/shared/models/assignments/poll-option'; 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'; /** * Component for a single assignment poll. Used in assignment detail view - * TODO DOCU */ @Component({ selector: 'os-assignment-poll', templateUrl: './assignment-poll.component.html', styleUrls: ['./assignment-poll.component.scss'] }) -export class AssignmentPollComponent implements OnInit { +export class AssignmentPollComponent extends BaseViewComponent implements OnInit { /** * The related assignment (used for metainfos, e.g. related user names) */ @@ -52,6 +53,8 @@ export class AssignmentPollComponent implements OnInit { } /** + * Gets the voting options + * * @returns all used (not undefined) option-independent values that are * used in this poll (e.g.) */ @@ -60,6 +63,8 @@ export class AssignmentPollComponent implements OnInit { } /** + * Gets the translated poll method name + * * TODO: check/improve text here * * @returns a name for the poll method this poll is set to (which is determined @@ -92,15 +97,20 @@ export class AssignmentPollComponent implements OnInit { * @param promptService Prompts for confirmation dialogs */ public constructor( + titleService: Title, + matSnackBar: MatSnackBar, public pollService: AssignmentPollService, private operator: OperatorService, private assignmentRepo: AssignmentRepositoryService, public translate: TranslateService, public dialog: MatDialog, private promptService: PromptService - ) {} + ) { + super(titleService, translate, matSnackBar); + } /** + * Gets the currently selected majority choice option from the repo */ public ngOnInit(): void { this.majorityChoice = @@ -116,17 +126,17 @@ export class AssignmentPollComponent implements OnInit { public async onDeletePoll(): Promise { const title = this.translate.instant('Are you sure you want to delete this poll?'); if (await this.promptService.open(title, null)) { - await this.assignmentRepo.deletePoll(this.poll); + await this.assignmentRepo.deletePoll(this.poll).then(null, this.raiseError); } - // TODO error handling } /** + * Print the PDF of this poll with the corresponding options and numbers + * * TODO Print the ballots for this poll. */ public printBallot(poll: Poll): void { - this.promptService.open('TODO', 'TODO'); - // TODO Print ballot not implemented + this.raiseError('Not yet implemented'); } /** @@ -136,7 +146,7 @@ export class AssignmentPollComponent implements OnInit { * @returns the full_name for the candidate */ public getCandidateName(option: PollOption): string { - const user = this.assignment.candidates.find(c => c.id === option.candidate_id); + 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 @@ -161,7 +171,6 @@ export class AssignmentPollComponent implements OnInit { /** * Opens the {@link AssignmentPollDialogComponent} dialog and then updates the votes, if the dialog * closes successfully (validation is done there) - * */ public enterVotes(): void { // TODO deep copy of this.poll (JSON parse is ugly workaround) @@ -174,13 +183,12 @@ export class AssignmentPollComponent implements OnInit { data: data, maxHeight: '90vh', minWidth: '300px', - maxWidth: '80vh', + maxWidth: '80vw', disableClose: true }); dialogRef.afterClosed().subscribe(result => { if (result) { - this.assignmentRepo.updateVotes(result, this.poll); - // TODO error handling + this.assignmentRepo.updateVotes(result, this.poll).then(null, this.raiseError); } }); } @@ -188,6 +196,7 @@ export class AssignmentPollComponent implements OnInit { /** * Updates the majority method for this poll * + * @param method the selected majority method */ public setMajority(method: MajorityMethod): void { this.majorityChoice = method; @@ -209,9 +218,10 @@ export class AssignmentPollComponent implements OnInit { if (!this.operator.hasPerms('assignments.can_manage')) { return; } + // TODO additional conditions: assignment not finished? const candidate = this.assignment.assignment.assignment_related_users.find( - u => u.user_id === option.candidate_id + user => user.user_id === option.candidate_id ); if (candidate) { this.assignmentRepo.markElected(candidate, this.assignment, !option.is_elected); diff --git a/client/src/app/site/assignments/models/view-assignment.ts b/client/src/app/site/assignments/models/view-assignment.ts index bd53409fe..6ba86104d 100644 --- a/client/src/app/site/assignments/models/view-assignment.ts +++ b/client/src/app/site/assignments/models/view-assignment.ts @@ -69,6 +69,9 @@ export class ViewAssignment extends BaseAgendaViewModel { public constructor(assignment: Assignment, relatedUser?: ViewUser[], agendaItem?: ViewItem, tags?: ViewTag[]) { super(Assignment.COLLECTIONSTRING); + + console.log('related user: ', relatedUser); + this._assignment = assignment; this._relatedUser = relatedUser; this._agendaItem = agendaItem; diff --git a/client/src/app/site/assignments/services/assignment-poll.service.ts b/client/src/app/site/assignments/services/assignment-poll.service.ts index 09f7dc4f5..1e719e99f 100644 --- a/client/src/app/site/assignments/services/assignment-poll.service.ts +++ b/client/src/app/site/assignments/services/assignment-poll.service.ts @@ -16,7 +16,7 @@ export type AssignmentPollMethod = 'yn' | 'yna' | 'votes'; type AssignmentPercentBase = 'YES_NO_ABSTAIN' | 'YES_NO' | 'VALID' | 'CAST' | 'DISABLED'; /** - * Service class for motion polls. + * Service class for assignment polls. */ @Injectable({ providedIn: 'root' @@ -46,6 +46,7 @@ export class AssignmentPollService extends PollService { /** * Constructor. Subscribes to the configuration values needed + * * @param config ConfigService */ public constructor(config: ConfigService) { @@ -59,12 +60,11 @@ export class AssignmentPollService extends PollService { config .get('assignments_poll_100_percent_base') .subscribe(base => (this.percentBase = base)); - // assignments_add_candidates_to_list_of_speakers boolean } /** * Get the base amount for the 100% calculations. Note that some poll methods - * (e.g. yes/no/abstain may have a diffferent percentage base and will return null here) + * (e.g. yes/no/abstain may have a different percentage base and will return null here) * * @param poll * @returns The amount of votes indicating the 100% base @@ -120,6 +120,8 @@ export class AssignmentPollService extends PollService { /** * Check if the option in a poll is abstract (percentages should not be calculated) * + * @param poll + * @param option * @returns true if the poll has no percentages, the poll option is a special value, * or if the calculations are disabled in the config */