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
mat-stroked-button
[ngClass]="pollStateActions[poll.state].css"
(click)="changeState(poll.nextState)"
(click)="nextPollState()"
[disabled]="stateChangePending"
>
<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 { 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 { VotingService } from 'app/core/ui-services/voting.service';
import { VotingPrivacyWarningComponent } from 'app/shared/components/voting-privacy-warning/voting-privacy-warning.component';
@ -82,6 +83,7 @@ export class AssignmentPollComponent
translate: TranslateService,
dialog: MatDialog,
promptService: PromptService,
choiceService: ChoiceService,
repo: AssignmentPollRepositoryService,
pollDialog: AssignmentPollDialogService,
private pollService: AssignmentPollService,
@ -90,7 +92,7 @@ export class AssignmentPollComponent
private operator: OperatorService,
private votingService: VotingService
) {
super(titleService, matSnackBar, translate, dialog, promptService, repo, pollDialog);
super(titleService, matSnackBar, translate, dialog, promptService, choiceService, repo, pollDialog);
}
public ngOnInit(): void {

View File

@ -50,7 +50,7 @@
<button
mat-stroked-button
[ngClass]="pollStateActions[poll.state].css"
(click)="changeState(poll.nextState)"
(click)="nextPollState()"
[disabled]="stateChangePending"
>
<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 { 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 { VotingPrivacyWarningComponent } from 'app/shared/components/voting-privacy-warning/voting-privacy-warning.component';
import { infoDialogSettings } from 'app/shared/utils/dialog-settings';
@ -68,6 +69,7 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll, Motio
titleService: Title,
matSnackBar: MatSnackBar,
promptService: PromptService,
choiceService: ChoiceService,
pollDialog: MotionPollDialogService,
protected dialog: MatDialog,
protected pollRepo: MotionPollRepositoryService,
@ -76,7 +78,7 @@ export class MotionPollComponent extends BasePollComponent<ViewMotionPoll, Motio
private pdfService: MotionPollPdfService,
private operator: OperatorService
) {
super(titleService, matSnackBar, translate, dialog, promptService, pollRepo, pollDialog);
super(titleService, matSnackBar, translate, dialog, promptService, choiceService, pollRepo, pollDialog);
}
public openVotingWarning(): void {

View File

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

View File

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

View File

@ -164,9 +164,13 @@ class BasePollViewSet(ModelViewSet):
@transaction.atomic
def publish(self, request, pk):
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"})
# stop poll if needed
if poll.state == BasePoll.STATE_STARTED:
poll.stop()
poll.state = BasePoll.STATE_PUBLISHED
poll.save()
inform_changed_data(

View File

@ -1284,6 +1284,29 @@ class PublishMotionPoll(TestCase):
self.assertHttpStatusVerbose(response, status.HTTP_400_BAD_REQUEST)
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):
def setUp(self):