added vote per user table and progress for polls
added update for options after stopping a poll
This commit is contained in:
parent
604df9d48b
commit
682db96b7c
@ -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
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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[];
|
||||
}
|
||||
|
@ -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>
|
||||
<!-- 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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
@ -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 {
|
||||
|
@ -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]
|
||||
})
|
||||
|
@ -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));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@ -0,0 +1 @@
|
||||
<span>{{ this.poll.voted_id.length }} von {{ this.max }} haben abgestimmt.</span>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
@ -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 {}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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]),
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user