stop-voting shows prompt

Stop voting shows options to either simply stop, publish directly or
abort. Was done using choice service.
Alter vote repo to simply choose with voting state to adress rather than
calculate the next state

Add server-side option to publish a poll in the started state
This commit is contained in:
Sean 2021-05-26 17:53:46 +02:00 committed by Emanuel Schütze
parent f4c237a18e
commit 6dc5c3bfa9
8 changed files with 74 additions and 38 deletions

View File

@ -48,7 +48,7 @@
<button <button
mat-stroked-button mat-stroked-button
[ngClass]="pollStateActions[poll.state].css" [ngClass]="pollStateActions[poll.state].css"
(click)="changeState(poll.nextState)" (click)="nextPollState()"
[disabled]="stateChangePending" [disabled]="stateChangePending"
> >
<mat-icon> {{ pollStateActions[poll.state].icon }}</mat-icon> <mat-icon> {{ pollStateActions[poll.state].icon }}</mat-icon>

View File

@ -8,6 +8,7 @@ import { TranslateService } from '@ngx-translate/core';
import { OperatorService } from 'app/core/core-services/operator.service'; import { OperatorService } from 'app/core/core-services/operator.service';
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service'; import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
import { ChoiceService } from 'app/core/ui-services/choice.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { VotingService } from 'app/core/ui-services/voting.service'; import { VotingService } from 'app/core/ui-services/voting.service';
import { VotingPrivacyWarningComponent } from 'app/shared/components/voting-privacy-warning/voting-privacy-warning.component'; import { VotingPrivacyWarningComponent } from 'app/shared/components/voting-privacy-warning/voting-privacy-warning.component';
@ -82,6 +83,7 @@ export class AssignmentPollComponent
translate: TranslateService, translate: TranslateService,
dialog: MatDialog, dialog: MatDialog,
promptService: PromptService, promptService: PromptService,
choiceService: ChoiceService,
repo: AssignmentPollRepositoryService, repo: AssignmentPollRepositoryService,
pollDialog: AssignmentPollDialogService, pollDialog: AssignmentPollDialogService,
private pollService: AssignmentPollService, private pollService: AssignmentPollService,
@ -90,7 +92,7 @@ export class AssignmentPollComponent
private operator: OperatorService, private operator: OperatorService,
private votingService: VotingService private votingService: VotingService
) { ) {
super(titleService, matSnackBar, translate, dialog, promptService, repo, pollDialog); super(titleService, matSnackBar, translate, dialog, promptService, choiceService, repo, pollDialog);
} }
public ngOnInit(): void { public ngOnInit(): void {

View File

@ -50,7 +50,7 @@
<button <button
mat-stroked-button mat-stroked-button
[ngClass]="pollStateActions[poll.state].css" [ngClass]="pollStateActions[poll.state].css"
(click)="changeState(poll.nextState)" (click)="nextPollState()"
[disabled]="stateChangePending" [disabled]="stateChangePending"
> >
<mat-icon> {{ pollStateActions[poll.state].icon }}</mat-icon> <mat-icon> {{ pollStateActions[poll.state].icon }}</mat-icon>

View File

@ -7,6 +7,7 @@ import { TranslateService } from '@ngx-translate/core';
import { OperatorService, Permission } from 'app/core/core-services/operator.service'; import { OperatorService, Permission } from 'app/core/core-services/operator.service';
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service'; import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
import { ChoiceService } from 'app/core/ui-services/choice.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { VotingPrivacyWarningComponent } from 'app/shared/components/voting-privacy-warning/voting-privacy-warning.component'; import { VotingPrivacyWarningComponent } from 'app/shared/components/voting-privacy-warning/voting-privacy-warning.component';
import { infoDialogSettings } from 'app/shared/utils/dialog-settings'; import { infoDialogSettings } from 'app/shared/utils/dialog-settings';
@ -68,6 +69,7 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll, Motio
titleService: Title, titleService: Title,
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
promptService: PromptService, promptService: PromptService,
choiceService: ChoiceService,
pollDialog: MotionPollDialogService, pollDialog: MotionPollDialogService,
protected dialog: MatDialog, protected dialog: MatDialog,
protected pollRepo: MotionPollRepositoryService, protected pollRepo: MotionPollRepositoryService,
@ -76,7 +78,7 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll, Motio
private pdfService: MotionPollPdfService, private pdfService: MotionPollPdfService,
private operator: OperatorService private operator: OperatorService
) { ) {
super(titleService, matSnackBar, translate, dialog, promptService, pollRepo, pollDialog); super(titleService, matSnackBar, translate, dialog, promptService, choiceService, pollRepo, pollDialog);
} }
public openVotingWarning(): void { public openVotingWarning(): void {

View File

@ -6,13 +6,14 @@ import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service'; import { BasePollDialogService } from 'app/core/ui-services/base-poll-dialog.service';
import { ChoiceService } from 'app/core/ui-services/choice.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { ChartData } from 'app/shared/components/charts/charts.component'; import { ChartData } from 'app/shared/components/charts/charts.component';
import { PollState, PollType } from 'app/shared/models/poll/base-poll'; import { PollState, PollType } from 'app/shared/models/poll/base-poll';
import { BaseViewComponentDirective } from 'app/site/base/base-view'; import { BaseViewComponentDirective } from 'app/site/base/base-view';
import { BasePollRepositoryService } from '../services/base-poll-repository.service'; import { BasePollRepositoryService } from '../services/base-poll-repository.service';
import { PollService } from '../services/poll.service'; import { PollService } from '../services/poll.service';
import { ViewBasePoll } from '../models/view-base-poll'; import { PollStateChangeActionVerbose, ViewBasePoll } from '../models/view-base-poll';
export abstract class BasePollComponent< export abstract class BasePollComponent<
V extends ViewBasePoll, V extends ViewBasePoll,
@ -49,39 +50,47 @@ export abstract class BasePollComponent<
protected translate: TranslateService, protected translate: TranslateService,
protected dialog: MatDialog, protected dialog: MatDialog,
protected promptService: PromptService, protected promptService: PromptService,
protected choiceService: ChoiceService,
protected repo: BasePollRepositoryService, protected repo: BasePollRepositoryService,
protected pollDialog: BasePollDialogService<V, S> protected pollDialog: BasePollDialogService<V, S>
) { ) {
super(titleService, translate, matSnackBar); super(titleService, translate, matSnackBar);
} }
public async changeState(key: PollState): Promise<void> { public async nextPollState(): Promise<void> {
if (key === PollState.Created) { const currentState: PollState = this._poll.state;
const title = this.translate.instant('Are you sure you want to reset this vote?'); if (currentState === PollState.Created || currentState === PollState.Finished) {
const content = this.translate.instant('All votes will be lost.'); await this.changeState(this._poll.nextState);
if (await this.promptService.open(title, content)) { } else if (currentState === PollState.Started) {
this.stateChangePending = true; const title = this.translate.instant('Are you sure you want to stop this voting?');
this.repo const actions = [this.translate.instant('Stop'), this.translate.instant('Stop & publish')];
.resetPoll(this._poll) const choice = await this.choiceService.open(title, null, false, actions);
.catch(this.raiseError)
.finally(() => { if (choice?.action === 'Stop') {
this.stateChangePending = false; await this.changeState(PollState.Finished);
}); } else if (choice?.action === 'Stop & publish') {
await this.changeState(PollState.Published);
} }
} else {
this.stateChangePending = true;
this.repo
.changePollState(this._poll)
.catch(this.raiseError)
.finally(() => {
this.stateChangePending = false;
});
} }
} }
public resetState(): void { private async changeState(targetState: PollState): Promise<void> {
this.stateChangePending = true;
this.repo
.changePollState(this._poll, targetState)
.catch(this.raiseError)
.finally(() => {
this.stateChangePending = false;
});
}
public async resetState(): Promise<void> {
const title = this.translate.instant('Are you sure you want to reset this vote?');
const content = this.translate.instant('All votes will be lost.');
if (await this.promptService.open(title, content)) {
this.changeState(PollState.Created); this.changeState(PollState.Created);
} }
}
/** /**
* Handler for the 'delete poll' button * Handler for the 'delete poll' button

View File

@ -56,17 +56,17 @@ export abstract class BasePollRepositoryService<
return viewModel; return viewModel;
} }
public changePollState(poll: BasePoll): Promise<void> { public changePollState(poll: BasePoll, targetState: PollState): Promise<void> {
const path = this.restPath(poll); const path = this.restPath(poll);
switch (poll.state) { switch (targetState) {
case PollState.Created: case PollState.Created:
return this.http.post(`${path}/start/`); return this.http.post(`${path}/reset/`);
case PollState.Started: case PollState.Started:
return this.http.post(`${path}/stop/`); return this.http.post(`${path}/start/`);
case PollState.Finished: case PollState.Finished:
return this.http.post(`${path}/publish/`); return this.http.post(`${path}/stop/`);
case PollState.Published: case PollState.Published:
return this.resetPoll(poll); return this.http.post(`${path}/publish/`);
} }
} }
@ -74,10 +74,6 @@ export abstract class BasePollRepositoryService<
return `/rest/${poll.collectionString}/${poll.id}`; return `/rest/${poll.collectionString}/${poll.id}`;
} }
public resetPoll(poll: BasePoll): Promise<void> {
return this.http.post(`${this.restPath(poll)}/reset/`);
}
public pseudoanonymize(poll: BasePoll): Promise<void> { public pseudoanonymize(poll: BasePoll): Promise<void> {
return this.http.post(`${this.restPath(poll)}/pseudoanonymize/`); return this.http.post(`${this.restPath(poll)}/pseudoanonymize/`);
} }

View File

@ -164,9 +164,13 @@ class BasePollViewSet(ModelViewSet):
@transaction.atomic @transaction.atomic
def publish(self, request, pk): def publish(self, request, pk):
poll = self.get_locked_object() poll = self.get_locked_object()
if poll.state != BasePoll.STATE_FINISHED: if poll.state not in (BasePoll.STATE_STARTED, BasePoll.STATE_FINISHED):
raise ValidationError({"detail": "Wrong poll state"}) raise ValidationError({"detail": "Wrong poll state"})
# stop poll if needed
if poll.state == BasePoll.STATE_STARTED:
poll.stop()
poll.state = BasePoll.STATE_PUBLISHED poll.state = BasePoll.STATE_PUBLISHED
poll.save() poll.save()
inform_changed_data( inform_changed_data(

View File

@ -1284,6 +1284,29 @@ class PublishMotionPoll(TestCase):
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST) self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
self.assertEqual(MotionPoll.objects.get().state, MotionPoll.STATE_CREATED) self.assertEqual(MotionPoll.objects.get().state, MotionPoll.STATE_CREATED)
def test_publish_from_started(self):
self.poll.state = MotionPoll.STATE_STARTED
self.poll.save()
response = self.client.post(reverse("motionpoll-publish", args=[self.poll.pk]))
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
self.assertEqual(MotionPoll.objects.get().state, MotionPoll.STATE_PUBLISHED)
def test_publish_from_started_with_entitled_users(self):
self.poll.state = MotionPoll.STATE_STARTED
self.poll.save()
admin = get_user_model().objects.get(username="admin")
admin.is_present = True
admin.save()
self.poll.groups.add(GROUP_ADMIN_PK)
response = self.client.post(reverse("motionpoll-publish", args=[self.poll.pk]))
self.assertHttpStatusVerbose(response, status.HTTP_200_OK)
poll = MotionPoll.objects.get()
self.assertEqual(poll.state, MotionPoll.STATE_PUBLISHED)
self.assertEqual(
poll.entitled_users_at_stop,
[{"user_id": admin.id, "voted": False, "vote_delegated_to_id": None}],
)
class PseudoanonymizeMotionPoll(TestCase): class PseudoanonymizeMotionPoll(TestCase):
def setUp(self): def setUp(self):