added vote per user table and progress for polls

added update for options after stopping a poll
This commit is contained in:
Joshua Sangmeister 2020-01-16 17:22:12 +01:00 committed by FinnStutzenstein
parent 604df9d48b
commit 682db96b7c
21 changed files with 244 additions and 85 deletions

View File

@ -9,6 +9,7 @@ import { ViewModelStoreService } from 'app/core/core-services/view-model-store.s
import { RelationDefinition } from 'app/core/definitions/relations';
import { VotingService } from 'app/core/ui-services/voting.service';
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
import { ViewMotion } from 'app/site/motions/models/view-motion';
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
import { MotionPollTitleInformation, ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
import { BasePollRepositoryService } from 'app/site/polls/services/base-poll-repository.service';
@ -35,6 +36,12 @@ const MotionPollRelations: RelationDefinition[] = [
ownIdKey: 'options_id',
ownKey: 'options',
foreignViewModel: ViewMotionOption
},
{
type: 'M2O',
ownIdKey: 'motion_id',
ownKey: 'motion',
foreignViewModel: ViewMotion
}
];

View File

@ -35,7 +35,8 @@ export class VotingService {
* checks whether the operator can vote on the given poll
*/
public canVote(poll: ViewBasePoll): boolean {
return !this.getVotePermissionError(poll);
const error = this.getVotePermissionError(poll);
return !error;
}
/**

View File

@ -7,10 +7,11 @@ import { AssignmentPollDetailComponent } from './components/assignment-poll-deta
import { AssignmentPollVoteComponent } from './components/assignment-poll-vote/assignment-poll-vote.component';
import { AssignmentPollComponent } from './components/assignment-poll/assignment-poll.component';
import { AssignmentsRoutingModule } from './assignments-routing.module';
import { PollsModule } from '../polls/polls.module';
import { SharedModule } from '../../shared/shared.module';
@NgModule({
imports: [CommonModule, AssignmentsRoutingModule, SharedModule],
imports: [CommonModule, AssignmentsRoutingModule, SharedModule, PollsModule],
declarations: [
AssignmentDetailComponent,
AssignmentListComponent,

View File

@ -38,30 +38,13 @@
<div>{{ 'Majority method' | translate }}: {{ poll.majorityMethodVerbose | translate }}</div>
<div>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</div>
</div>
<div *ngIf="poll.state === 2">
<os-poll-progress [poll]="poll"></os-poll-progress>
</div>
<div *ngIf="poll.state === 3 || poll.state === 4">
<h2 translate>Result</h2>
<div
*ngIf="poll.type === 'named'"
style="display: grid; grid-template-columns: auto repeat({{ poll.options.length }}, max-content);"
>
<!-- top left cell is empty -->
<div></div>
<!-- header (the assignment related users) -->
<ng-container *ngFor="let option of poll.options">
<div *ngIf="option.user">{{ option.user.full_name }}</div>
<div *ngIf="!option.user">{{ 'Unknown user' | translate }}</div>
</ng-container>
<!-- rows -->
<ng-container *ngFor="let obj of votesByUser | keyvalue">
<div *ngIf="obj.value.user">{{ obj.value.user.full_name }}</div>
<div *ngIf="!obj.value.user">{{ 'Unknown user' | translate }}</div>
<ng-container *ngFor="let option of poll.options">
<div>{{ obj.value.votes[option.user_id] }}</div>
</ng-container>
</ng-container>
</div>
<div class="chart-wrapper"></div>
<mat-table [dataSource]="poll.tableData">
<ng-container matColumnDef="user" sticky>
@ -88,9 +71,10 @@
<mat-cell *matCellDef="let row"></mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="columnDefinition"></mat-header-row>
<mat-row *matRowDef="let row; columns: columnDefinition"></mat-row>
<mat-header-row *matHeaderRowDef="columnDefinitionOverview"></mat-header-row>
<mat-row *matRowDef="let row; columns: columnDefinitionOverview"></mat-row>
</mat-table>
<os-charts
*ngIf="chartDataSubject.value"
[type]="chartType"
@ -98,6 +82,31 @@
[showLegend]="true"
[data]="chartDataSubject"
></os-charts>
<ng-container *ngIf="poll.type === 'named' && votesDataSource.data">
<input matInput [(ngModel)]="votesDataSource.filter" placeholder="Filter"/>
<mat-table [dataSource]="votesDataSource">
<ng-container matColumnDef="users" sticky>
<mat-header-cell *matHeaderCellDef>{{ "User" | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row">
<div *ngIf="row.user">{{ row.user.getFullName() }}</div>
<div *ngIf="!row.user">{{ "Unknown user" | translate }}</div>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'votes-' + option.user_id" *ngFor="let option of poll.options" sticky>
<mat-header-cell *matHeaderCellDef>
<div *ngIf="option.user">{{ option.user.getFullName() }}</div>
<div *ngIf="!option.user">{{ "Unknown user" | translate }}</div>
</mat-header-cell>
<mat-cell *matCellDef="let row">
{{ row.votes[option.user_id] }}
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="columnDefinitionPerName"></mat-header-row>
<mat-row *matRowDef="let row; columns: columnDefinitionPerName"></mat-row>
</mat-table>
</ng-container>
</div>
</ng-container>
</ng-template>
@ -106,7 +115,7 @@
<mat-menu #pollDetailMenu="matMenu">
<os-projector-button [menuItem]="true" [object]="poll" *osPerms="'core.can_manage_projector'"></os-projector-button>
<button mat-menu-item *ngIf="poll && poll.type === 'named'" (click)="pseudoanonymizePoll()">
<mat-icon>questionmark</mat-icon>
<mat-icon>polymer</mat-icon>
<span translate>Pseudoanonymize</span>
</button>
<mat-divider></mat-divider>

View File

@ -12,10 +12,8 @@ import { PromptService } from 'app/core/ui-services/prompt.service';
import { ChartType } from 'app/shared/components/charts/charts.component';
import { AssignmentPollMethods } from 'app/shared/models/assignments/assignment-poll';
import { BasePollDetailComponent } from 'app/site/polls/components/base-poll-detail.component';
import { ViewUser } from 'app/site/users/models/view-user';
import { AssignmentPollDialogService } from '../../services/assignment-poll-dialog.service';
import { ViewAssignmentPoll } from '../../models/view-assignment-poll';
import { ViewAssignmentVote } from '../../models/view-assignment-vote';
@Component({
selector: 'os-assignment-poll-detail',
@ -27,20 +25,20 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
public candidatesLabels: string[] = [];
public votesByUser: { [key: number]: { user: ViewUser; votes: { [key: number]: ViewAssignmentVote } } };
public get chartType(): ChartType {
return 'horizontalBar';
}
public get columnDefinition(): string[] {
public get columnDefinitionOverview(): string[] {
const columns = ['user', 'yes', 'no', 'quorum'];
if ((<ViewAssignmentPoll>this.poll).pollmethod === AssignmentPollMethods.YNA) {
if (this.poll.pollmethod === AssignmentPollMethods.YNA) {
columns.splice(3, 0, 'abstain');
}
return columns;
}
public columnDefinitionPerName: string[];
public constructor(
title: Title,
translate: TranslateService,
@ -55,27 +53,32 @@ export class AssignmentPollDetailComponent extends BasePollDetailComponent<ViewA
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog);
}
public onPollLoaded(): void {
const votes = {};
public onPollWithOptionsLoaded(): void {
this.columnDefinitionPerName = ['users'].concat(this.poll.options.map(option => 'votes-' + option.user_id));
setTimeout(() => {
for (const option of this.poll.options) {
for (const vote of option.votes) {
if (!votes[vote.user_id]) {
votes[vote.user_id] = {
user: vote.user,
votes: {}
};
}
votes[vote.user_id].votes[option.user_id] =
this.poll.pollmethod === AssignmentPollMethods.Votes ? vote.weight : vote.valueVerbose;
const votes = {};
let i = -1;
for (const option of this.poll.options) {
for (const vote of option.votes) {
// if poll was pseudoanonymized, use a negative index to not interfere with
// possible named votes (although this should never happen)
const userId = vote.user_id || i--;
if (!votes[userId]) {
votes[userId] = {
user: vote.user,
votes: {}
};
}
votes[userId].votes[option.user_id] =
this.poll.pollmethod === AssignmentPollMethods.Votes ? vote.weight : vote.valueVerbose;
}
console.log(votes, this.poll, this.poll.options);
this.votesByUser = votes;
this.candidatesLabels = this.poll.initChartLabels();
this.isReady = true;
});
}
this.setVotesData(Object.values(votes));
this.candidatesLabels = this.poll.initChartLabels();
this.isReady = true;
}
protected hasPerms(): boolean {

View File

@ -4,6 +4,7 @@ import { PollColor } from 'app/shared/models/poll/base-poll';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { ViewMotionOption } from 'app/site/motions/models/view-motion-option';
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
import { ViewMotion } from './view-motion';
export interface MotionPollTitleInformation {
title: string;
@ -72,5 +73,6 @@ export class ViewMotionPoll extends ViewBasePoll<MotionPoll> implements MotionPo
}
export interface ViewMotionPoll extends MotionPoll {
motion: ViewMotion;
options: ViewMotionOption[];
}

View File

@ -1,6 +1,6 @@
<os-head-bar [goBack]="true" [nav]="false">
<div class="title-slot">
<h2 *ngIf="motion">{{ 'Motion' | translate }} {{ motion.id }}</h2>
<h2 *ngIf="poll">{{ 'Motion' | translate }} {{ poll.motion.id }}</h2>
</div>
<div class="menu-slot" *osPerms="'agenda.can_manage'; or: 'agenda.can_see_list_of_speakers'">
@ -51,17 +51,26 @@
<mat-row *matRowDef="let row; columns: columnDefinition"></mat-row>
</mat-table>
<!-- Named table -->
<!-- The table was created in another PR -->
<div class="named-result-table" *ngIf="poll.type === 'named'">
<h3>{{ 'Singe votes' | translate }}</h3>
<div *ngFor="let vote of poll.options[0].votes">
<div *ngIf="vote.user">{{ vote.user.full_name }}</div>
<div *ngIf="!vote.user">{{ 'Unknown user' | translate }}</div>
<div>{{ vote.valueVerbose }}</div>
</div>
</div>
<!-- Named table: only show if votes are present -->
<ng-container *ngIf="poll.type === 'named' && votesDataSource.data">
<input matInput [(ngModel)]="votesDataSource.filter" placeholder="Filter"/>
<mat-table [dataSource]="votesDataSource">
<ng-container matColumnDef="key" sticky>
<mat-header-cell *matHeaderCellDef>{{ "User" | translate }}</mat-header-cell>
<mat-cell *matCellDef="let vote">
<div *ngIf="vote.user">{{ vote.user.getFullName() }}</div>
<div *ngIf="!vote.user">{{ 'Unknown user' | translate }}</div>
</mat-cell>
</ng-container>
<ng-container matColumnDef="value" sticky>
<mat-header-cell *matHeaderCellDef>{{ "Vote" | translate }}</mat-header-cell>
<mat-cell *matCellDef="let vote">{{ vote.valueVerbose }}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="columnDefinition"></mat-header-row>
<mat-row *matRowDef="let vote; columns: columnDefinition"></mat-row>
</mat-table>
</ng-container>
</div>
</div>
@ -76,6 +85,10 @@
<div>{{ 'Majority method' | translate }}: {{ poll.majorityMethodVerbose | translate }}</div>
<div>{{ '100% base' | translate }}: {{ poll.percentBaseVerbose | translate }}</div>
</div>
<div *ngIf="poll.state === 2">
<os-poll-progress [poll]="poll"></os-poll-progress>
</div>
</ng-container>
</ng-template>
@ -87,7 +100,7 @@
<span translate>Edit</span>
</button>
<button mat-menu-item *ngIf="poll && poll.type === 'named'" (click)="pseudoanonymizePoll()">
<mat-icon>questionmark</mat-icon>
<mat-icon>polymer</mat-icon>
<span translate>Pseudoanonymize</span>
</button>
<mat-divider></mat-divider>

View File

@ -7,7 +7,6 @@ import { TranslateService } from '@ngx-translate/core';
import { OperatorService } from 'app/core/core-services/operator.service';
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
import { MotionRepositoryService } from 'app/core/repositories/motions/motion-repository.service';
import { GroupRepositoryService } from 'app/core/repositories/users/group-repository.service';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { ChartType } from 'app/shared/components/charts/charts.component';
@ -45,14 +44,13 @@ export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotio
prompt: PromptService,
pollDialog: MotionPollDialogService,
private operator: OperatorService,
private router: Router,
private motionRepo: MotionRepositoryService
private router: Router
) {
super(title, translate, matSnackbar, repo, route, groupRepo, prompt, pollDialog);
}
protected onPollLoaded(): void {
this.motion = this.motionRepo.getViewModel((<ViewMotionPoll>this.poll).motion_id);
protected onPollWithOptionsLoaded(): void {
this.setVotesData(this.poll.options[0].votes);
}
public openDialog(): void {
@ -61,7 +59,7 @@ export class MotionPollDetailComponent extends BasePollDetailComponent<ViewMotio
}
protected onDeleted(): void {
this.router.navigate(['motions', (<ViewMotionPoll>this.poll).motion_id]);
this.router.navigate(['motions', this.poll.motion_id]);
}
protected hasPerms(): boolean {

View File

@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { SharedModule } from 'app/shared/shared.module';
import { PollsModule } from 'app/site/polls/polls.module';
import { MotionPollDetailComponent } from './motion-poll-detail/motion-poll-detail.component';
import { MotionPollListComponent } from './motion-poll-list/motion-poll-list.component';
import { MotionPollRoutingModule } from './motion-poll-routing.module';
@ -9,7 +10,7 @@ import { MotionPollVoteComponent } from './motion-poll-vote/motion-poll-vote.com
import { MotionPollComponent } from './motion-poll/motion-poll.component';
@NgModule({
imports: [CommonModule, SharedModule, MotionPollRoutingModule],
imports: [CommonModule, SharedModule, MotionPollRoutingModule, PollsModule],
exports: [MotionPollComponent],
declarations: [MotionPollComponent, MotionPollDetailComponent, MotionPollListComponent, MotionPollVoteComponent]
})

View File

@ -1,5 +1,5 @@
import { OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material';
import { MatSnackBar, MatTableDataSource } from '@angular/material';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
@ -15,9 +15,14 @@ import { ChartData, ChartType } from 'app/shared/components/charts/charts.compon
import { PollState, PollType } from 'app/shared/models/poll/base-poll';
import { BaseViewComponent } from 'app/site/base/base-view';
import { ViewGroup } from 'app/site/users/models/view-group';
import { ViewUser } from 'app/site/users/models/view-user';
import { BasePollRepositoryService } from '../services/base-poll-repository.service';
import { ViewBasePoll } from '../models/view-base-poll';
export interface BaseVoteData {
user?: ViewUser;
}
export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends BaseViewComponent implements OnInit {
/**
* All the groups of users.
@ -55,6 +60,9 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
*/
public chartDataSubject: BehaviorSubject<ChartData> = new BehaviorSubject(null);
// The datasource for the votes-per-user table
public votesDataSource: MatTableDataSource<BaseVoteData> = new MatTableDataSource();
/**
* Constructor
*
@ -81,6 +89,7 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
protected pollDialog: BasePollDialogService<V>
) {
super(title, translate, matSnackbar);
this.votesDataSource.filterPredicate = this.dataSourceFilterPredicate;
}
/**
@ -100,7 +109,7 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
const text = 'Do you really want to delete the selected poll?';
if (await this.promptDialog.open(title, text)) {
this.repo.delete(this.poll).then(() => this.onDeleted());
this.repo.delete(this.poll).then(() => this.onDeleted(), this.raiseError);
}
}
@ -109,7 +118,7 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
const text = 'Do you really want to pseudoanonymize the selected poll?';
if (await this.promptDialog.open(title, text)) {
await this.repo.pseudoanonymize(this.poll);
this.repo.pseudoanonymize(this.poll).then(() => this.onPollLoaded(), this.raiseError); // votes have changed, but not the poll, so the components have to be informed about the update
}
}
@ -129,10 +138,35 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
*/
protected onPollLoaded(): void {}
protected onPollWithOptionsLoaded(): void {}
protected onStateChanged(): void {}
protected abstract hasPerms(): boolean;
// custom filter for the data source: only search in usernames
protected dataSourceFilterPredicate(data: BaseVoteData, filter: string): boolean {
return (
data.user &&
data.user
.getFullName()
.trim()
.toLowerCase()
.indexOf(filter.trim().toLowerCase()) !== -1
);
}
/**
* sets the votes data only if the poll wasn't pseudoanonymized
*/
protected setVotesData(data: BaseVoteData[]): void {
if (data.every(voteDate => !voteDate.user)) {
this.votesDataSource.data = null;
} else {
this.votesDataSource.data = data;
}
}
/**
* This checks, if the poll has votes.
*/
@ -155,6 +189,15 @@ export abstract class BasePollDetailComponent<V extends ViewBasePoll> extends Ba
this.updateBreadcrumbs();
this.checkData();
this.onPollLoaded();
// wait for options to be loaded
(function waitForOptions(): void {
if (!this.poll.options || !this.poll.options.length) {
setTimeout(waitForOptions.bind(this), 1);
} else {
this.onPollWithOptionsLoaded();
}
}.call(this));
}
})
);

View File

@ -0,0 +1 @@
<span>{{ this.poll.voted_id.length }} von {{ this.max }} haben abgestimmt.</span>

View File

@ -0,0 +1,26 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { PollProgressComponent } from './poll-progress.component';
describe('PollProgressComponent', () => {
let component: PollProgressComponent;
let fixture: ComponentFixture<PollProgressComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PollProgressComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,44 @@
import { Component, Input, OnInit } from '@angular/core';
import { MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { map } from 'rxjs/operators';
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
import { BaseViewComponent } from 'app/site/base/base-view';
import { ViewBasePoll } from 'app/site/polls/models/view-base-poll';
@Component({
selector: 'os-poll-progress',
templateUrl: './poll-progress.component.html',
styleUrls: ['./poll-progress.component.scss']
})
export class PollProgressComponent extends BaseViewComponent implements OnInit {
@Input()
public poll: ViewBasePoll;
public max: number;
public constructor(
title: Title,
protected translate: TranslateService,
snackbar: MatSnackBar,
private userRepo: UserRepositoryService
) {
super(title, translate, snackbar);
}
/**
* OnInit.
* Sets the observable for groups.
*/
public ngOnInit(): void {
this.userRepo
.getViewModelListObservable()
.pipe(map(users => users.filter(user => this.poll.groups_id.intersect(user.groups_id).length)))
.subscribe(users => {
this.max = users.length;
});
}
}

View File

@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { PollListComponent } from './components/poll-list/poll-list.component';
import { PollProgressComponent } from './components/poll-progress/poll-progress.component';
import { PollsRoutingModule } from './polls-routing.module';
import { SharedModule } from '../../shared/shared.module';
@ -11,6 +12,7 @@ import { SharedModule } from '../../shared/shared.module';
*/
@NgModule({
imports: [CommonModule, PollsRoutingModule, SharedModule],
declarations: [PollListComponent]
exports: [PollProgressComponent],
declarations: [PollListComponent, PollProgressComponent]
})
export class PollsModule {}

View File

@ -2,10 +2,12 @@ import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
import { HasViewModelListObservable } from 'app/core/definitions/has-view-model-list-observable';
import { AssignmentPollRepositoryService } from 'app/core/repositories/assignments/assignment-poll-repository.service';
import { MotionPollRepositoryService } from 'app/core/repositories/motions/motion-poll-repository.service';
import { ViewAssignmentPoll } from 'app/site/assignments/models/view-assignment-poll';
import { BaseViewModel } from 'app/site/base/base-view-model';
import { ViewMotionPoll } from 'app/site/motions/models/view-motion-poll';
import { ViewBasePoll } from '../models/view-base-poll';
@ -21,7 +23,8 @@ export class PollListObservableService implements HasViewModelListObservable<Vie
public constructor(
motionPollRepo: MotionPollRepositoryService,
assignmentPollRepo: AssignmentPollRepositoryService
assignmentPollRepo: AssignmentPollRepositoryService,
private mapper: CollectionStringMapperService
) {
motionPollRepo
.getViewModelListObservable()
@ -41,4 +44,8 @@ export class PollListObservableService implements HasViewModelListObservable<Vie
public getViewModelListObservable(): Observable<ViewBasePoll[]> {
return this.viewPollListSubject.asObservable();
}
public getObservableFromViewModel(poll: ViewBasePoll): Observable<BaseViewModel> {
return this.mapper.getRepository(poll.collectionString).getViewModelObservable(poll.id);
}
}

View File

@ -94,7 +94,6 @@ class BasePollViewSet(ModelViewSet):
return super().update(request, *args, **kwargs)
def handle_request_with_votes(self, request, poll):
print(poll, poll.type, BasePoll.TYPE_ANALOG)
if poll.type != BasePoll.TYPE_ANALOG:
raise ValidationError(
{"detail": "You cannot enter votes for a non-analog poll."}
@ -142,6 +141,7 @@ class BasePollViewSet(ModelViewSet):
poll.state = BasePoll.STATE_FINISHED
poll.save()
inform_changed_data(poll.get_votes())
inform_changed_data(poll.get_options())
return Response()
@detail_route(methods=["POST"])
@ -233,7 +233,9 @@ class BasePollViewSet(ModelViewSet):
if poll.state != BasePoll.STATE_STARTED:
raise ValidationError("You can only vote on a started poll.")
if not request.user.is_present or not in_some_groups(
request.user.id, poll.groups.all(), exact=True
request.user.id,
list(poll.groups.values_list("pk", flat=True)),
exact=True,
):
self.permission_denied(request)

View File

@ -7,7 +7,6 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ImproperlyConfigured
from django.db.models import Model
from django.db.models.query import QuerySet
from .cache import element_cache
@ -112,9 +111,8 @@ async def async_has_perm(user_id: int, perm: str) -> bool:
return has_perm
def in_some_groups(
user_id: int, groups: Union[List[int], QuerySet], exact: bool = False
) -> bool:
# async code doesn't work well with QuerySets, so we have to give a list of ints for groups
def in_some_groups(user_id: int, groups: List[int], exact: bool = False) -> bool:
"""
Checks that user is in at least one given group. Groups can be given as a list
of ids or a QuerySet.
@ -134,7 +132,7 @@ def in_some_groups(
async def async_in_some_groups(
user_id: int, groups: Union[List[int], QuerySet], exact: bool = False
user_id: int, groups: List[int], exact: bool = False
) -> bool:
"""
Checks that user is in at least one given group. Groups can be given as a list
@ -143,9 +141,6 @@ async def async_in_some_groups(
user_id 0 means anonymous user.
"""
if isinstance(groups, QuerySet):
groups = [group.pk for group in groups]
if not user_id and not await async_anonymous_is_enabled():
in_some_groups = False
elif not user_id:

View File

@ -382,7 +382,6 @@ class ManageSpeaker(TestCase):
self.assertEqual(response.status_code, 403)
def test_remove_someone_else(self):
print(self.user)
speaker = Speaker.objects.add(self.user, self.list_of_speakers)
response = self.client.delete(
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk]),

View File

@ -708,6 +708,7 @@ class VoteAssignmentPollBaseTestClass(TestCase):
self.admin.save()
self.poll.groups.add(GROUP_ADMIN_PK)
self.poll.create_options()
inform_changed_data(self.poll)
def create_poll(self):
# has to be implemented by subclasses

View File

@ -70,6 +70,7 @@ class TestCreation(TestCase):
"is_directory": True,
"mediafile": self.file,
},
format="multipart",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertFalse(Mediafile.objects.exists())
@ -79,6 +80,7 @@ class TestCreation(TestCase):
response = self.client.post(
reverse("mediafile-list"),
{"title": "test_title_vai8oDogohheideedie4", "mediafile": file},
format="multipart",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
mediafile = Mediafile.objects.get()
@ -90,6 +92,7 @@ class TestCreation(TestCase):
response = self.client.post(
reverse("mediafile-list"),
{"title": "test_title_Zeicheipeequie3ohfid", "mediafile": file1},
format="multipart",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
mediafile = Mediafile.objects.get()
@ -98,6 +101,7 @@ class TestCreation(TestCase):
response = self.client.post(
reverse("mediafile-list"),
{"title": "test_title_aiChaetohs0quicee9eb", "mediafile": file2},
format="multipart",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(Mediafile.objects.count(), 1)