Merge pull request #6070 from tsiegleauq/stop-voting-publish-prompt

stop-voting shows prompt
This commit is contained in:
Emanuel Schütze 2021-05-26 18:13:00 +02:00 committed by GitHub
commit c60553e376
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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,38 +50,46 @@ 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.changeState(PollState.Created); 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);
}
} }
/** /**

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):