pro/contra speakers and notes for point of order speakers
Enhance Pro Contra UI/UX by Sean
This commit is contained in:
parent
4f35770769
commit
c9c90cd4a3
@ -1 +1 @@
|
|||||||
Subproject commit f17ad9db22e87c1b642cc5796ed6d07cd4e0f95b
|
Subproject commit d3e251aa0ca1b01b2fed3c0f2b4f5e2db87f5684
|
@ -117,6 +117,13 @@ export class OperatorService implements OnAfterAppsLoaded {
|
|||||||
return this._viewUser;
|
return this._viewUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns 0 for the anonymous
|
||||||
|
*/
|
||||||
|
public get userId(): number {
|
||||||
|
return this.user?.id || 0;
|
||||||
|
}
|
||||||
|
|
||||||
public get isAnonymous(): boolean {
|
public get isAnonymous(): boolean {
|
||||||
return !this.user || this.user.id === 0;
|
return !this.user || this.user.id === 0;
|
||||||
}
|
}
|
||||||
|
@ -161,10 +161,11 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit
|
|||||||
public async createSpeaker(
|
public async createSpeaker(
|
||||||
listOfSpeakers: ViewListOfSpeakers,
|
listOfSpeakers: ViewListOfSpeakers,
|
||||||
userId: number,
|
userId: number,
|
||||||
pointOfOrder?: boolean
|
pointOfOrder?: boolean,
|
||||||
|
note?: string
|
||||||
): Promise<Identifiable> {
|
): Promise<Identifiable> {
|
||||||
const restUrl = this.getRestUrl(listOfSpeakers.id, 'manage_speaker');
|
const restUrl = this.getRestUrl(listOfSpeakers.id, 'manage_speaker');
|
||||||
return await this.httpService.post<Identifiable>(restUrl, { user: userId, point_of_order: pointOfOrder });
|
return await this.httpService.post<Identifiable>(restUrl, { user: userId, point_of_order: pointOfOrder, note });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -222,15 +223,14 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks all speakers for a given user
|
* Toggles the mark for a given speaker.
|
||||||
*
|
|
||||||
* @param userId {@link User} id of the user
|
|
||||||
* @param marked determine if the user should be marked or not
|
|
||||||
* @param listOfSpeakers the target list of speakers
|
|
||||||
*/
|
*/
|
||||||
public async markSpeaker(listOfSpeakers: ViewListOfSpeakers, speaker: ViewSpeaker, marked: boolean): Promise<void> {
|
public async toggleMarked(speaker: ViewSpeaker): Promise<void> {
|
||||||
const restUrl = this.getRestUrl(listOfSpeakers.id, 'manage_speaker');
|
await this.httpService.put(`/rest/agenda/speaker/${speaker.id}/`, { marked: !speaker.marked });
|
||||||
await this.httpService.patch(restUrl, { user: speaker.user.id, marked: marked });
|
}
|
||||||
|
|
||||||
|
public async setProContraSpeech(speaker: ViewSpeaker, proSpeech: boolean | null): Promise<void> {
|
||||||
|
await this.httpService.put(`/rest/agenda/speaker/${speaker.id}/`, { pro_speech: proSpeech });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -20,7 +20,9 @@
|
|||||||
<div class="finished-speaker-grid">
|
<div class="finished-speaker-grid">
|
||||||
<div class="number">{{ number + 1 }}.</div>
|
<div class="number">{{ number + 1 }}.</div>
|
||||||
<div class="name">{{ speaker.getTitle() }}</div>
|
<div class="name">{{ speaker.getTitle() }}</div>
|
||||||
<div class="point-of-order" *ngIf="speaker.point_of_order"><mat-icon color="warn">warning</mat-icon></div>
|
<div class="point-of-order" *ngIf="speaker.point_of_order">
|
||||||
|
<mat-icon color="warn">warning</mat-icon>
|
||||||
|
</div>
|
||||||
<div class="time">
|
<div class="time">
|
||||||
{{ durationString(speaker) }} ({{ 'Start time' | translate }}: {{ startTimeToString(speaker) }})
|
{{ durationString(speaker) }} ({{ 'Start time' | translate }}: {{ startTimeToString(speaker) }})
|
||||||
</div>
|
</div>
|
||||||
@ -49,10 +51,24 @@
|
|||||||
<mat-icon>mic</mat-icon>
|
<mat-icon>mic</mat-icon>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="name">{{ activeSpeaker.getListTitle() }}</span>
|
<span class="name">
|
||||||
|
{{ activeSpeaker.getListTitle() }}
|
||||||
|
<div class="active-speaker-subtitle">
|
||||||
|
<i *ngIf="activeSpeaker.pro_speech === true">
|
||||||
|
{{ 'Pro speech' | translate }}
|
||||||
|
</i>
|
||||||
|
|
||||||
|
<i *ngIf="activeSpeaker.pro_speech === false">
|
||||||
|
{{ 'Contra speech' | translate }}
|
||||||
|
</i>
|
||||||
|
|
||||||
|
<i *ngIf="activeSpeaker.marked">
|
||||||
|
{{ 'Contribution' | translate }}
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
<span class="suffix">
|
<span class="suffix">
|
||||||
|
|
||||||
<!-- point of order visible for everyone -->
|
<!-- point of order visible for everyone -->
|
||||||
<button
|
<button
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
@ -84,7 +100,56 @@
|
|||||||
>
|
>
|
||||||
<!-- implicit speaker references into the component using ng-template slot -->
|
<!-- implicit speaker references into the component using ng-template slot -->
|
||||||
<ng-template let-speaker>
|
<ng-template let-speaker>
|
||||||
<span *osPerms="'agenda.can_manage_list_of_speakers'; and: !speaker.point_of_order">
|
<div class="single-speaker-line">
|
||||||
|
<div class="speaker-name">
|
||||||
|
<!-- Speaker name -->
|
||||||
|
<div>
|
||||||
|
{{ speaker.getTitle() }}
|
||||||
|
</div>
|
||||||
|
<!-- Extra line -->
|
||||||
|
<ng-container>
|
||||||
|
<!-- Pro Contra Mark -->
|
||||||
|
<ng-container *ngIf="speaker.pro_speech === true">
|
||||||
|
<mat-icon inline class="inline-icon-text-align green-text"> add_circle </mat-icon>
|
||||||
|
<i class="user-subtitle">
|
||||||
|
{{ 'Pro speech' | translate }}
|
||||||
|
</i>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="speaker.pro_speech === false">
|
||||||
|
<mat-icon inline class="inline-icon-text-align red-warning-text">
|
||||||
|
remove_circle
|
||||||
|
</mat-icon>
|
||||||
|
<i class="user-subtitle">
|
||||||
|
{{ 'Contra speech' | translate }}
|
||||||
|
</i>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="speaker.marked">
|
||||||
|
<mat-icon inline class="inline-icon-text-align icon">star</mat-icon>
|
||||||
|
<i class="user-subtitle">
|
||||||
|
{{ 'Contribution' | translate }}
|
||||||
|
</i>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- Point Of Order and note -->
|
||||||
|
<ng-container>
|
||||||
|
<mat-icon
|
||||||
|
inline
|
||||||
|
color="warn"
|
||||||
|
class="inline-icon-text-align"
|
||||||
|
*ngIf="speaker.point_of_order"
|
||||||
|
>warning</mat-icon
|
||||||
|
>
|
||||||
|
<i *ngIf="showSpeakersOrderNote" class="red-warning-text">
|
||||||
|
{{ speaker.note }}
|
||||||
|
</i>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
<div class="speaker-controls one-line">
|
||||||
|
<!-- Extra stuff: Spoken Count, Gender, 1st Contribution -->
|
||||||
|
<span *osPerms="permission.agendaCanManageListOfSpeakers; and: !speaker.point_of_order">
|
||||||
<!-- Speaker count -->
|
<!-- Speaker count -->
|
||||||
<span *ngIf="hasSpokenCount(speaker)" class="red-warning-text speaker-warning">
|
<span *ngIf="hasSpokenCount(speaker)" class="red-warning-text speaker-warning">
|
||||||
{{ hasSpokenCount(speaker) + 1 }}. <span>{{ 'contribution' | translate }}</span>
|
{{ hasSpokenCount(speaker) + 1 }}. <span>{{ 'contribution' | translate }}</span>
|
||||||
@ -102,52 +167,69 @@
|
|||||||
<span *ngIf="speaker.gender">({{ speaker.gender | translate }})</span>
|
<span *ngIf="speaker.gender">({{ speaker.gender | translate }})</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Start, start and delete buttons -->
|
<!-- For normal users -->
|
||||||
<span>
|
<span *osPerms="permission.agendaCanManageListOfSpeakers; complement: true">
|
||||||
|
<ng-container *ngIf="speakerIsOperator(speaker)">
|
||||||
|
<!-- pro -->
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
(click)="onProContraButtons(speaker, true)"
|
||||||
|
matTooltip="{{ 'Pro speech' | translate }}"
|
||||||
|
*ngIf="enableProContraSpeech && !speaker.point_of_order"
|
||||||
|
>
|
||||||
|
<mat-icon class="user-subtitle" *ngIf="speaker.pro_speech !== true"> add_circle_outline </mat-icon>
|
||||||
|
<mat-icon class="green-text" *ngIf="speaker.pro_speech === true">
|
||||||
|
add_circle
|
||||||
|
</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- contra -->
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
(click)="onProContraButtons(speaker, false)"
|
||||||
|
matTooltip="{{ 'Contra speech' | translate }}"
|
||||||
|
*ngIf="enableProContraSpeech && !speaker.point_of_order"
|
||||||
|
>
|
||||||
|
<mat-icon class="user-subtitle" *ngIf="speaker.pro_speech !== false"> remove_circle_outline </mat-icon>
|
||||||
|
<mat-icon class="red-warning-text" *ngIf="speaker.pro_speech === false">
|
||||||
|
remove_circle
|
||||||
|
</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- mark -->
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
(click)="onMarkButton(speaker)"
|
||||||
|
matTooltip="{{ 'Contribution' | translate }}"
|
||||||
|
*ngIf="canMark(speaker) && !speaker.point_of_order"
|
||||||
|
>
|
||||||
|
<mat-icon *ngIf="speaker.marked">star</mat-icon>
|
||||||
|
<mat-icon class="user-subtitle" *ngIf="!speaker.marked">star_border</mat-icon> </button>
|
||||||
|
</ng-container>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Start and more button -->
|
||||||
|
<span *osPerms="permission.agendaCanManageListOfSpeakers">
|
||||||
<!-- start button -->
|
<!-- start button -->
|
||||||
<button
|
<button
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
matTooltip="{{ 'Begin speech' | translate }}"
|
matTooltip="{{ 'Begin speech' | translate }}"
|
||||||
(click)="onStartButton(speaker)"
|
(click)="onStartButton(speaker)"
|
||||||
*osPerms="'agenda.can_manage_list_of_speakers'"
|
|
||||||
>
|
>
|
||||||
<mat-icon>play_arrow</mat-icon>
|
<mat-icon>play_arrow</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- star button -->
|
<!-- more menu button -->
|
||||||
<button
|
<button
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
matTooltip="{{ 'Mark speaker' | translate }}"
|
[matMenuTriggerFor]="manageSpeakerMenu"
|
||||||
(click)="onMarkButton(speaker)"
|
[matMenuTriggerData]="{ speaker: speaker }"
|
||||||
*osPerms="'agenda.can_manage_list_of_speakers'; and: !speaker.point_of_order"
|
|
||||||
>
|
>
|
||||||
<mat-icon>{{ speaker.marked ? 'star' : 'star_border' }}</mat-icon>
|
<mat-icon>more_vert</mat-icon>
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- point of order visible for everyone -->
|
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
matTooltip="{{ 'Point of order' | translate }}"
|
|
||||||
*ngIf="speaker.point_of_order"
|
|
||||||
>
|
|
||||||
<mat-icon color="warn"> warning </mat-icon>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- delete button -->
|
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
matTooltip="{{ 'Remove' | translate }}"
|
|
||||||
(click)="removeSpeaker(speaker)"
|
|
||||||
*osPerms="'agenda.can_manage_list_of_speakers'"
|
|
||||||
>
|
|
||||||
<mat-icon>close</mat-icon>
|
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
<!-- For thouse without LOS -->
|
</div>
|
||||||
<span *osPerms="'agenda.can_manage_list_of_speakers'; complement: true">
|
|
||||||
<mat-icon *ngIf="speaker.marked"> star </mat-icon>
|
|
||||||
</span>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</os-sorting-list>
|
</os-sorting-list>
|
||||||
</div>
|
</div>
|
||||||
@ -188,24 +270,63 @@
|
|||||||
<mat-icon>add</mat-icon>
|
<mat-icon>add</mat-icon>
|
||||||
<span>{{ 'Add me' | translate }}</span>
|
<span>{{ 'Add me' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button *ngIf="isOpInWaitlist()" mat-stroked-button [disabled]="closed" (click)="removeSpeaker()">
|
||||||
*ngIf="isOpInWaitlist()"
|
|
||||||
mat-stroked-button
|
|
||||||
[disabled]="closed"
|
|
||||||
(click)="removeSpeaker()"
|
|
||||||
>
|
|
||||||
<mat-icon>remove</mat-icon>
|
<mat-icon>remove</mat-icon>
|
||||||
<span>{{ 'Remove me' | translate }}</span>
|
<span>{{ 'Remove me' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Point Of order -->
|
<!-- Point Of order -->
|
||||||
<button mat-stroked-button color="warn" (click)="addPointOfOrder()" *ngIf="showPointOfOrders && !isOpInWaitlist(true)">
|
<button
|
||||||
|
mat-stroked-button
|
||||||
|
color="warn"
|
||||||
|
(click)="addPointOfOrder()"
|
||||||
|
*ngIf="showPointOfOrders && !isOpInWaitlist(true)"
|
||||||
|
>
|
||||||
<mat-icon>warning</mat-icon>
|
<mat-icon>warning</mat-icon>
|
||||||
<span>{{ 'Point of order' | translate }}</span>
|
<span>{{ 'Point of order' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button mat-stroked-button color="warn" (click)="removePointOfOrder()" *ngIf="showPointOfOrders && isOpInWaitlist(true)">
|
<button
|
||||||
|
mat-stroked-button
|
||||||
|
color="warn"
|
||||||
|
(click)="removePointOfOrder()"
|
||||||
|
*ngIf="showPointOfOrders && isOpInWaitlist(true)"
|
||||||
|
>
|
||||||
<mat-icon>remove</mat-icon>
|
<mat-icon>remove</mat-icon>
|
||||||
<span>{{ 'Withdraw point of order' | translate }}</span>
|
<span>{{ 'Withdraw point of order' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|
||||||
|
<!-- admin menu for managing single speakers -->
|
||||||
|
<mat-menu #manageSpeakerMenu>
|
||||||
|
<ng-template let-speaker="speaker" matMenuContent>
|
||||||
|
<ng-container *osPerms="permission.agendaCanManageListOfSpeakers">
|
||||||
|
<!-- pro button -->
|
||||||
|
<button mat-menu-item (click)="onProContraButtons(speaker, true)" *ngIf="enableProContraSpeech">
|
||||||
|
<mat-icon *ngIf="speaker.pro_speech !== true"> add_circle_outline </mat-icon>
|
||||||
|
<mat-icon class="green-text" *ngIf="speaker.pro_speech === true"> add_circle </mat-icon>
|
||||||
|
<span>{{ 'Pro speech' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- contra button -->
|
||||||
|
<button mat-menu-item (click)="onProContraButtons(speaker, false)" *ngIf="enableProContraSpeech">
|
||||||
|
<mat-icon *ngIf="speaker.pro_speech !== false"> remove_circle_outline </mat-icon>
|
||||||
|
<mat-icon class="red-warning-text" *ngIf="speaker.pro_speech === false"> remove_circle </mat-icon>
|
||||||
|
<span>{{ 'Contra speech' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- star button -->
|
||||||
|
<button mat-menu-item (click)="onMarkButton(speaker)">
|
||||||
|
<mat-icon>{{ speaker.marked ? 'star' : 'star_border' }}</mat-icon>
|
||||||
|
<span>{{ 'Contribution' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<!-- remove speaker from list -->
|
||||||
|
<button mat-menu-item (click)="removeSpeaker(speaker)">
|
||||||
|
<mat-icon color="warn">delete</mat-icon>
|
||||||
|
<span>{{ 'Remove' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</ng-template>
|
||||||
|
</mat-menu>
|
||||||
|
@ -83,6 +83,7 @@
|
|||||||
height: 50px;
|
height: 50px;
|
||||||
margin: 50px 25px 20px 25px;
|
margin: 50px 25px 20px 25px;
|
||||||
box-shadow: 0px 3px 10px 0px rgba(0, 0, 0, 0.25);
|
box-shadow: 0px 3px 10px 0px rgba(0, 0, 0, 0.25);
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
.prefix {
|
.prefix {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
@ -99,6 +100,13 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
.active-speaker-subtitle {
|
||||||
|
* {
|
||||||
|
font-weight: initial;
|
||||||
|
font-size: 95%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.suffix {
|
.suffix {
|
||||||
@ -111,6 +119,34 @@
|
|||||||
|
|
||||||
.waiting-list {
|
.waiting-list {
|
||||||
padding: 10px 25px 0 25px;
|
padding: 10px 25px 0 25px;
|
||||||
|
|
||||||
|
.single-speaker-line {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto min-content;
|
||||||
|
grid-column-gap: 0.5em;
|
||||||
|
|
||||||
|
.speaker-name {
|
||||||
|
margin: auto 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speaker-controls {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.speaker-warning {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-self-buttons {
|
||||||
|
margin: 10px 20px;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-new-speaker-form {
|
.search-new-speaker-form {
|
||||||
@ -127,17 +163,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-self-buttons {
|
|
||||||
margin: 10px 20px;
|
|
||||||
display: inline-flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
button {
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.speaker-warning {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormControl, FormGroup } from '@angular/forms';
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
@ -22,10 +23,13 @@ import { ConfigService } from 'app/core/ui-services/config.service';
|
|||||||
import { DurationService } from 'app/core/ui-services/duration.service';
|
import { DurationService } from 'app/core/ui-services/duration.service';
|
||||||
import { PromptService } from 'app/core/ui-services/prompt.service';
|
import { PromptService } from 'app/core/ui-services/prompt.service';
|
||||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||||
|
import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||||
|
import { infoDialogSettings } from 'app/shared/utils/dialog-settings';
|
||||||
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
|
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
|
||||||
import { SpeakerState, ViewSpeaker } from 'app/site/agenda/models/view-speaker';
|
import { SpeakerState, ViewSpeaker } from 'app/site/agenda/models/view-speaker';
|
||||||
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
||||||
import { ViewUser } from 'app/site/users/models/view-user';
|
import { ViewUser } from 'app/site/users/models/view-user';
|
||||||
|
import { PointOfOrderDialogComponent } from '../point-of-order-dialog/point-of-order-dialog.component';
|
||||||
import { Selectable } from '../selectable';
|
import { Selectable } from '../selectable';
|
||||||
import { SortingListComponent } from '../sorting-list/sorting-list.component';
|
import { SortingListComponent } from '../sorting-list/sorting-list.component';
|
||||||
|
|
||||||
@ -63,6 +67,9 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
|||||||
}
|
}
|
||||||
|
|
||||||
private pointOfOrderEnabled: boolean;
|
private pointOfOrderEnabled: boolean;
|
||||||
|
public enableProContraSpeech: boolean;
|
||||||
|
private canSetMarkSelf: boolean;
|
||||||
|
private noteForAll: boolean;
|
||||||
|
|
||||||
public get title(): string {
|
public get title(): string {
|
||||||
return this.viewListOfSpeakers?.getTitle();
|
return this.viewListOfSpeakers?.getTitle();
|
||||||
@ -80,6 +87,10 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
|||||||
return !this.config.instant('agenda_present_speakers_only') || this.operator.user.is_present;
|
return !this.config.instant('agenda_present_speakers_only') || this.operator.user.is_present;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get showSpeakersOrderNote(): boolean {
|
||||||
|
return this.noteForAll || this.opCanManage;
|
||||||
|
}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
public set speakers(los: ViewListOfSpeakers) {
|
public set speakers(los: ViewListOfSpeakers) {
|
||||||
this.setListOfSpeakers(los);
|
this.setListOfSpeakers(los);
|
||||||
@ -113,7 +124,8 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
|||||||
private userRepository: UserRepositoryService,
|
private userRepository: UserRepositoryService,
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
private viewport: ViewportService,
|
private viewport: ViewportService,
|
||||||
private cd: ChangeDetectorRef
|
private cd: ChangeDetectorRef,
|
||||||
|
private dialog: MatDialog
|
||||||
) {
|
) {
|
||||||
super(title, translate, snackBar);
|
super(title, translate, snackBar);
|
||||||
this.addSpeakerForm = new FormGroup({ user_id: new FormControl() });
|
this.addSpeakerForm = new FormGroup({ user_id: new FormControl() });
|
||||||
@ -150,6 +162,15 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
|||||||
// observe point of order settings
|
// observe point of order settings
|
||||||
this.config.get<boolean>('agenda_enable_point_of_order_speakers').subscribe(enabled => {
|
this.config.get<boolean>('agenda_enable_point_of_order_speakers').subscribe(enabled => {
|
||||||
this.pointOfOrderEnabled = enabled;
|
this.pointOfOrderEnabled = enabled;
|
||||||
|
}),
|
||||||
|
this.config.get<boolean>('agenda_list_of_speakers_enable_pro_contra_speech').subscribe(enabled => {
|
||||||
|
this.enableProContraSpeech = enabled;
|
||||||
|
}),
|
||||||
|
this.config.get<boolean>('agenda_list_of_speakers_can_set_mark_self').subscribe(canMark => {
|
||||||
|
this.canSetMarkSelf = canMark;
|
||||||
|
}),
|
||||||
|
this.config.get<boolean>('agenda_list_of_speakers_speaker_note_for_everyone').subscribe(noteForAll => {
|
||||||
|
this.noteForAll = noteForAll;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -209,15 +230,20 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async addPointOfOrder(): Promise<void> {
|
public async addPointOfOrder(): Promise<void> {
|
||||||
const title = this.translate.instant('Are you sure you want to submit a point of order?');
|
const dialogRef = this.dialog.open<PointOfOrderDialogComponent, ViewListOfSpeakers, Promise<Identifiable>>(
|
||||||
if (await this.promptService.open(title)) {
|
PointOfOrderDialogComponent,
|
||||||
|
{
|
||||||
|
data: this.viewListOfSpeakers,
|
||||||
|
...infoDialogSettings
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.listOfSpeakersRepo.createSpeaker(this.viewListOfSpeakers, undefined, true);
|
await dialogRef.afterClosed().toPromise();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.raiseError(e);
|
this.raiseError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public removePointOfOrder(): void {
|
public removePointOfOrder(): void {
|
||||||
this.listOfSpeakersRepo.deleteSpeaker(this.viewListOfSpeakers, undefined, true).catch(this.raiseError);
|
this.listOfSpeakersRepo.deleteSpeaker(this.viewListOfSpeakers, undefined, true).catch(this.raiseError);
|
||||||
@ -235,7 +261,7 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
|||||||
/**
|
/**
|
||||||
* Click on the mic button to mark a speaker as speaking
|
* Click on the mic button to mark a speaker as speaking
|
||||||
*
|
*
|
||||||
* @param speaker the speaker marked in the list
|
* @param speaker the speaker selected in the list
|
||||||
*/
|
*/
|
||||||
public async onStartButton(speaker: ViewSpeaker): Promise<void> {
|
public async onStartButton(speaker: ViewSpeaker): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@ -258,13 +284,26 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public speakerIsOperator(speaker: ViewSpeaker): boolean {
|
||||||
|
return speaker.user_id === this.operator.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public canMark(speaker: ViewSpeaker): boolean {
|
||||||
|
return this.opCanManage || (this.canSetMarkSelf && this.speakerIsOperator(speaker));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Click on the star button. Toggles the marked attribute.
|
* Click on the star button. Toggles the marked attribute.
|
||||||
*
|
*
|
||||||
* @param speaker The speaker clicked on.
|
* @param speaker The speaker clicked on.
|
||||||
*/
|
*/
|
||||||
public onMarkButton(speaker: ViewSpeaker): void {
|
public onMarkButton(speaker: ViewSpeaker): void {
|
||||||
this.listOfSpeakersRepo.markSpeaker(this.viewListOfSpeakers, speaker, !speaker.marked).catch(this.raiseError);
|
this.listOfSpeakersRepo.toggleMarked(speaker).catch(this.raiseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onProContraButtons(speaker: ViewSpeaker, isPro: boolean): void {
|
||||||
|
const value = speaker.pro_speech === isPro ? null : isPro;
|
||||||
|
this.listOfSpeakersRepo.setProContraSpeech(speaker, value).catch(this.raiseError);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
<h2 mat-dialog-title>
|
||||||
|
<span>{{ 'Are you sure you want to submit a point of order?' | translate }}</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<mat-dialog-content>
|
||||||
|
{{ 'Request' | translate }}
|
||||||
|
<form class="edit-form" [formGroup]="editForm">
|
||||||
|
<mat-form-field>
|
||||||
|
<input matInput osAutofocus [maxLength]="MAX_LENGTH" formControlName="note" />
|
||||||
|
</mat-form-field>
|
||||||
|
</form>
|
||||||
|
{{ editForm.value.note.length }} / {{ MAX_LENGTH }}
|
||||||
|
</mat-dialog-content>
|
||||||
|
<mat-dialog-actions>
|
||||||
|
<button mat-button osAutofocus (click)="onOk()" color="accent" [disabled]="!this.editForm.valid">
|
||||||
|
{{ 'OK' | translate }}
|
||||||
|
</button>
|
||||||
|
<button mat-button (click)="onCancel()">{{ 'Cancel' | translate }}</button>
|
||||||
|
</mat-dialog-actions>
|
@ -0,0 +1,6 @@
|
|||||||
|
.edit-form {
|
||||||
|
width: 100%;
|
||||||
|
.mat-form-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
import { Component, Inject } from '@angular/core';
|
||||||
|
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
|
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
|
|
||||||
|
import { ListOfSpeakersRepositoryService } from 'app/core/repositories/agenda/list-of-speakers-repository.service';
|
||||||
|
import { Identifiable } from 'app/shared/models/base/identifiable';
|
||||||
|
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'os-point-of-order-dialog',
|
||||||
|
templateUrl: './point-of-order-dialog.component.html',
|
||||||
|
styleUrls: ['./point-of-order-dialog.component.scss']
|
||||||
|
})
|
||||||
|
export class PointOfOrderDialogComponent {
|
||||||
|
public editForm: FormGroup;
|
||||||
|
|
||||||
|
public readonly MAX_LENGTH = 80;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
public dialogRef: MatDialogRef<PointOfOrderDialogComponent, Promise<Identifiable>>,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public listOfSpeakers: ViewListOfSpeakers,
|
||||||
|
private fb: FormBuilder,
|
||||||
|
private repo: ListOfSpeakersRepositoryService
|
||||||
|
) {
|
||||||
|
this.editForm = this.fb.group({
|
||||||
|
note: ['', [Validators.required, Validators.maxLength(this.MAX_LENGTH)]]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onOk(): void {
|
||||||
|
if (!this.editForm.valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const note = this.editForm.value.note;
|
||||||
|
this.dialogRef.close(this.repo.createSpeaker(this.listOfSpeakers, undefined, true, note));
|
||||||
|
}
|
||||||
|
|
||||||
|
public onCancel(): void {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
}
|
@ -9,18 +9,22 @@
|
|||||||
(click)="onItemClick($event, i)"
|
(click)="onItemClick($event, i)"
|
||||||
(cdkDragStarted)="dragStarted(i)"
|
(cdkDragStarted)="dragStarted(i)"
|
||||||
>
|
>
|
||||||
<div class="section-one" cdkDragHandle *ngIf="enable">
|
<!-- drag handle -->
|
||||||
|
<div class="drag-indicator" cdkDragHandle *ngIf="enable">
|
||||||
<mat-icon>drag_indicator</mat-icon>
|
<mat-icon>drag_indicator</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-two">
|
|
||||||
<!-- {number}. {item.getTitle()} -->
|
<!-- Count number -->
|
||||||
<span *ngIf="count">{{ i + 1 }}. </span>
|
<div class="count" *ngIf="count">
|
||||||
<span>{{ item?.getTitle() }}</span>
|
<span>{{ i + 1 }}.</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-three">
|
|
||||||
<!-- Extra controls slot using implicit template references -->
|
<!-- Content -->
|
||||||
|
<div class="content">
|
||||||
<ng-template [ngTemplateOutlet]="templateRef" [ngTemplateOutletContext]="{ $implicit: item }"></ng-template>
|
<ng-template [ngTemplateOutlet]="templateRef" [ngTemplateOutletContext]="{ $implicit: item }"></ng-template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Drag prev -->
|
||||||
<div class="line" *cdkDragPreview>
|
<div class="line" *cdkDragPreview>
|
||||||
<div class="spacer.left-10" *ngIf="multiSelectedIndex.length > 0">
|
<div class="spacer.left-10" *ngIf="multiSelectedIndex.length > 0">
|
||||||
{{ multiSelectedIndex.length }} <span>{{ 'items selected' | translate }}</span>
|
{{ multiSelectedIndex.length }} <span>{{ 'items selected' | translate }}</span>
|
||||||
|
@ -9,31 +9,26 @@
|
|||||||
min-height: 50px;
|
min-height: 50px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
|
||||||
.section-one {
|
.drag-indicator {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
padding: 0 10px;
|
|
||||||
line-height: 0px;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
line-height: 0px;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
color: slategrey;
|
color: slategrey;
|
||||||
cursor: move;
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-two {
|
.count {
|
||||||
|
display: table-cell;
|
||||||
|
vertical-align: middle;
|
||||||
|
min-width: 2em;
|
||||||
|
padding-left: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-left: 20px;
|
|
||||||
|
|
||||||
span + span {
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-three {
|
|
||||||
display: table-cell;
|
|
||||||
padding-right: 10px;
|
|
||||||
width: auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,11 @@ export class Speaker extends BaseModel<Speaker> {
|
|||||||
public id: number;
|
public id: number;
|
||||||
public user_id: number;
|
public user_id: number;
|
||||||
public weight: number;
|
public weight: number;
|
||||||
|
public note?: string;
|
||||||
public marked: boolean;
|
public marked: boolean;
|
||||||
public item_id: number;
|
public item_id: number;
|
||||||
public point_of_order: boolean;
|
public point_of_order: boolean;
|
||||||
|
public pro_speech?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ISO datetime string to indicate the begin time of the speech. Empty if
|
* ISO datetime string to indicate the begin time of the speech. Empty if
|
||||||
|
@ -134,6 +134,7 @@ import { ApplauseBarDisplayComponent } from './components/applause-bar-display/a
|
|||||||
import { ProgressComponent } from './components/progress/progress.component';
|
import { ProgressComponent } from './components/progress/progress.component';
|
||||||
import { NgParticlesModule } from 'ng-particles';
|
import { NgParticlesModule } from 'ng-particles';
|
||||||
import { ApplauseParticleDisplayComponent } from './components/applause-particle-display/applause-particle-display.component';
|
import { ApplauseParticleDisplayComponent } from './components/applause-particle-display/applause-particle-display.component';
|
||||||
|
import { PointOfOrderDialogComponent } from './components/point-of-order-dialog/point-of-order-dialog.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Share Module for all "dumb" components and pipes.
|
* Share Module for all "dumb" components and pipes.
|
||||||
@ -305,7 +306,8 @@ import { ApplauseParticleDisplayComponent } from './components/applause-particle
|
|||||||
AssignmentPollDetailContentComponent,
|
AssignmentPollDetailContentComponent,
|
||||||
JitsiComponent,
|
JitsiComponent,
|
||||||
VideoPlayerComponent,
|
VideoPlayerComponent,
|
||||||
ListOfSpeakersContentComponent
|
ListOfSpeakersContentComponent,
|
||||||
|
PointOfOrderDialogComponent
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
PermsDirective,
|
PermsDirective,
|
||||||
@ -372,7 +374,8 @@ import { ApplauseParticleDisplayComponent } from './components/applause-particle
|
|||||||
ListOfSpeakersContentComponent,
|
ListOfSpeakersContentComponent,
|
||||||
ApplauseBarDisplayComponent,
|
ApplauseBarDisplayComponent,
|
||||||
ProgressComponent,
|
ProgressComponent,
|
||||||
ApplauseParticleDisplayComponent
|
ApplauseParticleDisplayComponent,
|
||||||
|
PointOfOrderDialogComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
@ -155,6 +155,9 @@
|
|||||||
>
|
>
|
||||||
<!-- implicit item references into the component using ng-template slot -->
|
<!-- implicit item references into the component using ng-template slot -->
|
||||||
<ng-template let-item>
|
<ng-template let-item>
|
||||||
|
<div class="single-candidate-line">
|
||||||
|
<span *ngIf="hasPerms('manage')"> {{ item.getTitle() }} </span>
|
||||||
|
|
||||||
<span *ngIf="hasPerms('manage')">
|
<span *ngIf="hasPerms('manage')">
|
||||||
<button
|
<button
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
@ -164,6 +167,7 @@
|
|||||||
<mat-icon>clear</mat-icon>
|
<mat-icon>clear</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</os-sorting-list>
|
</os-sorting-list>
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,6 +40,16 @@
|
|||||||
|
|
||||||
.candidates-list {
|
.candidates-list {
|
||||||
padding: 10px 25px 0 25px;
|
padding: 10px 25px 0 25px;
|
||||||
|
|
||||||
|
.single-candidate-line {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto min-content;
|
||||||
|
grid-column-gap: 0.5em;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin: auto 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-candidates {
|
.add-candidates {
|
||||||
|
@ -15,9 +15,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<os-sorting-list (sortEvent)="onListUpdate($event)" [input]="motionObservable" #sorter>
|
<os-sorting-list (sortEvent)="onListUpdate($event)" [input]="motionObservable" #sorter>
|
||||||
<ng-template let-motion>
|
<ng-template let-motion>
|
||||||
|
<div class="single-motion-line">
|
||||||
|
<span>
|
||||||
|
{{ motion.getTitle() }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
<mat-basic-chip class="bluegrey" disableRipple matTooltip="{{ 'Sequential number' | translate }}">
|
<mat-basic-chip class="bluegrey" disableRipple matTooltip="{{ 'Sequential number' | translate }}">
|
||||||
{{ motion.id }}
|
{{ motion.id }}
|
||||||
</mat-basic-chip>
|
</mat-basic-chip>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</os-sorting-list>
|
</os-sorting-list>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
.single-motion-line {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto min-content;
|
||||||
|
grid-column-gap: 0.5em;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin: auto 0;
|
||||||
|
}
|
||||||
|
}
|
@ -8,5 +8,11 @@
|
|||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<mat-card class="os-form-card">
|
<mat-card class="os-form-card">
|
||||||
<!-- The sorting component -->
|
<!-- The sorting component -->
|
||||||
<os-sorting-list (sortEvent)="onSortingChange($event)" [live]="true" [input]="comments" #sorter> </os-sorting-list>
|
<os-sorting-list (sortEvent)="onSortingChange($event)" [live]="true" [input]="comments" #sorter>
|
||||||
|
<ng-template let-comment>
|
||||||
|
<span>
|
||||||
|
{{ comment.getTitle() }}
|
||||||
|
</span>
|
||||||
|
</ng-template>
|
||||||
|
</os-sorting-list>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
@ -27,9 +27,22 @@
|
|||||||
>
|
>
|
||||||
<!-- implicit user references into the component using ng-template slot -->
|
<!-- implicit user references into the component using ng-template slot -->
|
||||||
<ng-template let-user>
|
<ng-template let-user>
|
||||||
<button type="button" mat-icon-button matTooltip="{{ 'Remove' | translate }}" (click)="onRemove(user)">
|
<div class="single-submitter-line">
|
||||||
|
<span class="ellipsis-overflow">
|
||||||
|
{{ user.getTitle() }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
mat-icon-button
|
||||||
|
matTooltip="{{ 'Remove' | translate }}"
|
||||||
|
(click)="onRemove(user)"
|
||||||
|
>
|
||||||
<mat-icon>close</mat-icon>
|
<mat-icon>close</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</os-sorting-list>
|
</os-sorting-list>
|
||||||
|
|
||||||
|
@ -20,6 +20,17 @@ h4 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.single-submitter-line {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto min-content;
|
||||||
|
grid-column-gap: 0.5em;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin: auto 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.user .mat-chip {
|
.user .mat-chip {
|
||||||
border-radius: 16px !important;
|
border-radius: 16px !important;
|
||||||
padding: 5px 15px !important;
|
padding: 5px 15px !important;
|
||||||
|
@ -2,6 +2,7 @@ export interface SlideSpeaker {
|
|||||||
user: string;
|
user: string;
|
||||||
marked: boolean;
|
marked: boolean;
|
||||||
point_of_order: boolean;
|
point_of_order: boolean;
|
||||||
|
pro_speech?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CommonListOfSpeakersSlideData {
|
export interface CommonListOfSpeakersSlideData {
|
||||||
|
@ -17,7 +17,9 @@
|
|||||||
<div *ngFor="let speaker of data.data.finished">
|
<div *ngFor="let speaker of data.data.finished">
|
||||||
{{ speaker.user }}
|
{{ speaker.user }}
|
||||||
<mat-icon *ngIf="speaker.marked">star</mat-icon>
|
<mat-icon *ngIf="speaker.marked">star</mat-icon>
|
||||||
<mat-icon *ngIf="speaker.point_of_order" color="warn"> warning</mat-icon>
|
<mat-icon *ngIf="speaker.pro_speech === true" class="green-text">add_circle</mat-icon>
|
||||||
|
<mat-icon *ngIf="speaker.pro_speech === false" class="red-warning-text">remove_circle</mat-icon>
|
||||||
|
<mat-icon *ngIf="speaker.point_of_order" color="warn">warning</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -26,7 +28,9 @@
|
|||||||
<mat-icon class="micicon">mic</mat-icon>
|
<mat-icon class="micicon">mic</mat-icon>
|
||||||
{{ data.data.current.user }}
|
{{ data.data.current.user }}
|
||||||
<mat-icon *ngIf="data.data.current.marked">star</mat-icon>
|
<mat-icon *ngIf="data.data.current.marked">star</mat-icon>
|
||||||
<mat-icon *ngIf="data.data.current.point_of_order" color="warn"> warning</mat-icon>
|
<mat-icon *ngIf="data.data.current.pro_speech === true" class="green-text">add_circle</mat-icon>
|
||||||
|
<mat-icon *ngIf="data.data.current.pro_speech === false" class="red-warning-text">remove_circle</mat-icon>
|
||||||
|
<mat-icon *ngIf="data.data.current.point_of_order" color="warn">warning</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Next speakers -->
|
<!-- Next speakers -->
|
||||||
@ -35,7 +39,9 @@
|
|||||||
<li *ngFor="let speaker of data.data.waiting">
|
<li *ngFor="let speaker of data.data.waiting">
|
||||||
{{ speaker.user }}
|
{{ speaker.user }}
|
||||||
<mat-icon *ngIf="speaker.marked">star</mat-icon>
|
<mat-icon *ngIf="speaker.marked">star</mat-icon>
|
||||||
<mat-icon *ngIf="speaker.point_of_order" color="warn"> warning</mat-icon>
|
<mat-icon *ngIf="speaker.pro_speech === true" class="green-text">add_circle</mat-icon>
|
||||||
|
<mat-icon *ngIf="speaker.pro_speech === false" class="red-warning-text">remove_circle</mat-icon>
|
||||||
|
<mat-icon *ngIf="speaker.point_of_order" color="warn">warning</mat-icon>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,6 +10,8 @@
|
|||||||
<li *ngFor="let speaker of nextSpeakers" class="one-line">
|
<li *ngFor="let speaker of nextSpeakers" class="one-line">
|
||||||
{{ speaker.user }}
|
{{ speaker.user }}
|
||||||
<mat-icon *ngIf="speaker.marked">star</mat-icon>
|
<mat-icon *ngIf="speaker.marked">star</mat-icon>
|
||||||
|
<mat-icon *ngIf="speaker.pro_speech===true">thumb_up</mat-icon>
|
||||||
|
<mat-icon *ngIf="speaker.pro_speech===false">thumb_down</mat-icon>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
@ -173,15 +173,27 @@ b,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.red-warning-text {
|
.red-warning-text {
|
||||||
color: red;
|
color: red !important;
|
||||||
.mat-icon {
|
.mat-icon {
|
||||||
color: red !important;
|
color: red !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.green-text {
|
.green-text {
|
||||||
// TODO better name/theming
|
color: #5a5 !important;
|
||||||
color: #5a5;
|
}
|
||||||
|
|
||||||
|
.icon-as-block.mat-icon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can better align icons with text. Use like:
|
||||||
|
* <mat-icon inline class="inline-icon-text-align"> ...
|
||||||
|
* <span> ...
|
||||||
|
*/
|
||||||
|
.inline-icon-text-align.mat-icon {
|
||||||
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-text-distance {
|
.icon-text-distance {
|
||||||
|
@ -17,7 +17,7 @@ class AgendaAppConfig(AppConfig):
|
|||||||
listen_to_related_object_post_delete,
|
listen_to_related_object_post_delete,
|
||||||
listen_to_related_object_post_save,
|
listen_to_related_object_post_save,
|
||||||
)
|
)
|
||||||
from .views import ItemViewSet, ListOfSpeakersViewSet
|
from .views import ItemViewSet, ListOfSpeakersViewSet, SpeakerViewSet
|
||||||
|
|
||||||
# Connect signals.
|
# Connect signals.
|
||||||
post_save.connect(
|
post_save.connect(
|
||||||
@ -38,6 +38,10 @@ class AgendaAppConfig(AppConfig):
|
|||||||
self.get_model("ListOfSpeakers").get_collection_string(),
|
self.get_model("ListOfSpeakers").get_collection_string(),
|
||||||
ListOfSpeakersViewSet,
|
ListOfSpeakersViewSet,
|
||||||
)
|
)
|
||||||
|
router.register(
|
||||||
|
self.get_model("Speaker").get_collection_string(),
|
||||||
|
SpeakerViewSet,
|
||||||
|
)
|
||||||
|
|
||||||
def get_config_variables(self):
|
def get_config_variables(self):
|
||||||
from .config_variables import get_config_variables
|
from .config_variables import get_config_variables
|
||||||
|
@ -220,3 +220,33 @@ def get_config_variables():
|
|||||||
group="Agenda",
|
group="Agenda",
|
||||||
subgroup="List of speakers",
|
subgroup="List of speakers",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
yield ConfigVariable(
|
||||||
|
name="agenda_list_of_speakers_enable_pro_contra_speech",
|
||||||
|
default_value=False,
|
||||||
|
input_type="boolean",
|
||||||
|
label="Enable pro/contra buttons for speakers",
|
||||||
|
weight=240,
|
||||||
|
group="Agenda",
|
||||||
|
subgroup="List of speakers",
|
||||||
|
)
|
||||||
|
|
||||||
|
yield ConfigVariable(
|
||||||
|
name="agenda_list_of_speakers_can_set_mark_self",
|
||||||
|
default_value=False,
|
||||||
|
input_type="boolean",
|
||||||
|
label="Speakers can set the mark by themselfs",
|
||||||
|
weight=245,
|
||||||
|
group="Agenda",
|
||||||
|
subgroup="List of speakers",
|
||||||
|
)
|
||||||
|
|
||||||
|
yield ConfigVariable(
|
||||||
|
name="agenda_list_of_speakers_speaker_note_for_everyone",
|
||||||
|
default_value=False,
|
||||||
|
input_type="boolean",
|
||||||
|
label="Everyone can see the note of a speaker (instead of admins and the speaker only)",
|
||||||
|
weight=250,
|
||||||
|
group="Agenda",
|
||||||
|
subgroup="List of speakers",
|
||||||
|
)
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 2.2.20 on 2021-04-19 10:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("agenda", "0012_list_of_speakers_closed_default"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="speaker",
|
||||||
|
name="note",
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="speaker",
|
||||||
|
name="pro_speech",
|
||||||
|
field=models.BooleanField(blank=True, default=None, null=True),
|
||||||
|
),
|
||||||
|
]
|
@ -421,7 +421,14 @@ class SpeakerManager(models.Manager):
|
|||||||
Manager for Speaker model. Provides a customized add method.
|
Manager for Speaker model. Provides a customized add method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def add(self, user, list_of_speakers, skip_autoupdate=False, point_of_order=False):
|
def add(
|
||||||
|
self,
|
||||||
|
user,
|
||||||
|
list_of_speakers,
|
||||||
|
skip_autoupdate=False,
|
||||||
|
point_of_order=False,
|
||||||
|
note=None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Customized manager method to prevent anonymous users to be on the
|
Customized manager method to prevent anonymous users to be on the
|
||||||
list of speakers and that someone is twice on one list (off coming
|
list of speakers and that someone is twice on one list (off coming
|
||||||
@ -450,6 +457,7 @@ class SpeakerManager(models.Manager):
|
|||||||
user=user,
|
user=user,
|
||||||
weight=weight,
|
weight=weight,
|
||||||
point_of_order=point_of_order,
|
point_of_order=point_of_order,
|
||||||
|
note=note,
|
||||||
)
|
)
|
||||||
speaker.save(
|
speaker.save(
|
||||||
force_insert=True,
|
force_insert=True,
|
||||||
@ -534,6 +542,8 @@ class Speaker(RESTModelMixin, models.Model):
|
|||||||
The sort order of the list of speakers. None, if he has already spoken.
|
The sort order of the list of speakers. None, if he has already spoken.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
note = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
|
||||||
marked = models.BooleanField(default=False)
|
marked = models.BooleanField(default=False)
|
||||||
"""
|
"""
|
||||||
Marks a speaker.
|
Marks a speaker.
|
||||||
@ -544,6 +554,8 @@ class Speaker(RESTModelMixin, models.Model):
|
|||||||
Identifies the speaker as someone with a point of order
|
Identifies the speaker as someone with a point of order
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
pro_speech = models.BooleanField(null=True, blank=True, default=None)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
default_permissions = ()
|
default_permissions = ()
|
||||||
permissions = (("can_be_speaker", "Can put oneself on the list of speakers"),)
|
permissions = (("can_be_speaker", "Can put oneself on the list of speakers"),)
|
||||||
|
@ -16,8 +16,19 @@ class SpeakerSerializer(ModelSerializer):
|
|||||||
"begin_time",
|
"begin_time",
|
||||||
"end_time",
|
"end_time",
|
||||||
"weight",
|
"weight",
|
||||||
|
"note",
|
||||||
"marked",
|
"marked",
|
||||||
"point_of_order",
|
"point_of_order",
|
||||||
|
"pro_speech",
|
||||||
|
)
|
||||||
|
read_only_fields = (
|
||||||
|
"id",
|
||||||
|
"user",
|
||||||
|
"begin_time",
|
||||||
|
"end_time",
|
||||||
|
"weight",
|
||||||
|
"note",
|
||||||
|
"point_of_order",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import jsonschema
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
|
from django.http.request import QueryDict
|
||||||
|
|
||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.utils.autoupdate import inform_changed_data
|
from openslides.utils.autoupdate import inform_changed_data
|
||||||
@ -299,12 +300,11 @@ class ListOfSpeakersViewSet(UpdateModelMixin, TreeSortMixin, GenericViewSet):
|
|||||||
"""
|
"""
|
||||||
Special view endpoint to add users to the list of speakers or remove
|
Special view endpoint to add users to the list of speakers or remove
|
||||||
them. Send POST {'user': <user_id>} to add a new speaker.
|
them. Send POST {'user': <user_id>} to add a new speaker.
|
||||||
Send POST {'user': <user_id>, 'point_of_order': True } to add a point
|
Send POST {'user': <user_id>, 'point_of_order': True, 'note': <optional string> }
|
||||||
of order to the list of speakers.
|
to add a pointof order to the list of speakers.
|
||||||
Omit data to add yourself. Send DELETE {'speaker': <speaker_id>} or
|
Omit data to add yourself. Send DELETE {'speaker': <speaker_id>} or
|
||||||
DELETE {'speaker': [<speaker_id>, <speaker_id>, ...]} to remove one or
|
DELETE {'speaker': [<speaker_id>, <speaker_id>, ...]} to remove one or
|
||||||
more speakers from the list of speakers. Omit data to remove yourself.
|
more speakers from the list of speakers. Omit data to remove yourself.
|
||||||
Send PATCH {'user': <user_id>, 'marked': <bool>} to mark the speaker.
|
|
||||||
|
|
||||||
Checks also whether the requesting user can do this. He needs at
|
Checks also whether the requesting user can do this. He needs at
|
||||||
least the permissions 'agenda.can_see_list_of_speakers' (see
|
least the permissions 'agenda.can_see_list_of_speakers' (see
|
||||||
@ -323,6 +323,10 @@ class ListOfSpeakersViewSet(UpdateModelMixin, TreeSortMixin, GenericViewSet):
|
|||||||
if not isinstance(point_of_order, bool):
|
if not isinstance(point_of_order, bool):
|
||||||
raise ValidationError({"detail": "point_of_order has to be a bool."})
|
raise ValidationError({"detail": "point_of_order has to be a bool."})
|
||||||
|
|
||||||
|
note = request.data.get("note")
|
||||||
|
if note is not None and not isinstance(note, str):
|
||||||
|
raise ValidationError({"detail": "note must be a string"})
|
||||||
|
|
||||||
# Check permissions and other conditions. Get user instance.
|
# Check permissions and other conditions. Get user instance.
|
||||||
if user_id is None:
|
if user_id is None:
|
||||||
# Add oneself
|
# Add oneself
|
||||||
@ -352,7 +356,7 @@ class ListOfSpeakersViewSet(UpdateModelMixin, TreeSortMixin, GenericViewSet):
|
|||||||
# list of coming speakers.
|
# list of coming speakers.
|
||||||
try:
|
try:
|
||||||
speaker = Speaker.objects.add(
|
speaker = Speaker.objects.add(
|
||||||
user, list_of_speakers, point_of_order=point_of_order
|
user, list_of_speakers, point_of_order=point_of_order, note=note
|
||||||
)
|
)
|
||||||
except OpenSlidesError as e:
|
except OpenSlidesError as e:
|
||||||
raise ValidationError({"detail": str(e)})
|
raise ValidationError({"detail": str(e)})
|
||||||
@ -361,37 +365,7 @@ class ListOfSpeakersViewSet(UpdateModelMixin, TreeSortMixin, GenericViewSet):
|
|||||||
# to see users may not have it but can get it now.
|
# to see users may not have it but can get it now.
|
||||||
inform_changed_data(user, disable_history=True)
|
inform_changed_data(user, disable_history=True)
|
||||||
|
|
||||||
# Set 'marked' for the speaker
|
elif request.method == "DELETE":
|
||||||
elif request.method == "PATCH":
|
|
||||||
# Check permissions
|
|
||||||
if not has_perm(self.request.user, "agenda.can_manage_list_of_speakers"):
|
|
||||||
self.permission_denied(request)
|
|
||||||
|
|
||||||
# Retrieve user_id
|
|
||||||
user_id = request.data.get("user")
|
|
||||||
try:
|
|
||||||
user = get_user_model().objects.get(pk=int(user_id))
|
|
||||||
except (ValueError, get_user_model().DoesNotExist):
|
|
||||||
raise ValidationError({"detail": "User does not exist."})
|
|
||||||
|
|
||||||
marked = request.data.get("marked")
|
|
||||||
if not isinstance(marked, bool):
|
|
||||||
raise ValidationError({"detail": "Marked has to be a bool."})
|
|
||||||
|
|
||||||
queryset = Speaker.objects.filter(
|
|
||||||
list_of_speakers=list_of_speakers, user=user, begin_time=None
|
|
||||||
)
|
|
||||||
|
|
||||||
if not queryset.exists():
|
|
||||||
raise ValidationError(
|
|
||||||
{"detail": "The user is not in the list of speakers."}
|
|
||||||
)
|
|
||||||
for speaker in queryset.all():
|
|
||||||
speaker.marked = marked
|
|
||||||
speaker.save()
|
|
||||||
|
|
||||||
else:
|
|
||||||
# request.method == 'DELETE'
|
|
||||||
speaker_ids = request.data.get("speaker")
|
speaker_ids = request.data.get("speaker")
|
||||||
|
|
||||||
# Check permissions and other conditions. Get speaker instance.
|
# Check permissions and other conditions. Get speaker instance.
|
||||||
@ -439,6 +413,8 @@ class ListOfSpeakersViewSet(UpdateModelMixin, TreeSortMixin, GenericViewSet):
|
|||||||
# send autoupdate if speakers are deleted
|
# send autoupdate if speakers are deleted
|
||||||
if deleted_some_speakers:
|
if deleted_some_speakers:
|
||||||
inform_changed_data(list_of_speakers)
|
inform_changed_data(list_of_speakers)
|
||||||
|
else:
|
||||||
|
raise ValidationError({"detail": "Invalid method"})
|
||||||
|
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
@ -576,3 +552,42 @@ class ListOfSpeakersViewSet(UpdateModelMixin, TreeSortMixin, GenericViewSet):
|
|||||||
Speaker.objects.all().delete()
|
Speaker.objects.all().delete()
|
||||||
inform_changed_data(ListOfSpeakers.objects.all())
|
inform_changed_data(ListOfSpeakers.objects.all())
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
|
|
||||||
|
class SpeakerViewSet(UpdateModelMixin, GenericViewSet):
|
||||||
|
queryset = Speaker.objects.all()
|
||||||
|
|
||||||
|
def check_view_permissions(self):
|
||||||
|
"""
|
||||||
|
Returns True if the user has required permissions.
|
||||||
|
"""
|
||||||
|
return has_perm(self.request.user, "agenda.can_see_list_of_speakers")
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
# This is a hack to make request.data mutable. Otherwise fields can not be deleted.
|
||||||
|
if isinstance(request.data, QueryDict):
|
||||||
|
request.data._mutable = True
|
||||||
|
|
||||||
|
if (
|
||||||
|
"pro_speech" in request.data
|
||||||
|
and not config["agenda_list_of_speakers_enable_pro_contra_speech"]
|
||||||
|
):
|
||||||
|
raise ValidationError({"detail": "pro/contra speech is not enabled"})
|
||||||
|
|
||||||
|
if not has_perm(request.user, "agenda.can_manage_list_of_speakers"):
|
||||||
|
# if no manage perms, only the speaker user itself can update the speaker.
|
||||||
|
speaker = self.get_object()
|
||||||
|
if speaker.user_id != request.user.id:
|
||||||
|
self.permission_denied(request)
|
||||||
|
|
||||||
|
whitelist = ["pro_speech"] # was checked above
|
||||||
|
if config["agenda_list_of_speakers_can_set_mark_self"]:
|
||||||
|
whitelist.append("marked")
|
||||||
|
|
||||||
|
for key in request.data.keys():
|
||||||
|
if key not in whitelist:
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": f"You are not allowed to set {key}"}
|
||||||
|
)
|
||||||
|
|
||||||
|
return super().update(request, *args, **kwargs)
|
||||||
|
@ -13,6 +13,7 @@ from django.utils.timezone import now
|
|||||||
from django.views import static
|
from django.views import static
|
||||||
from django.views.generic.base import View
|
from django.views.generic.base import View
|
||||||
|
|
||||||
|
from openslides.agenda.models import ListOfSpeakers
|
||||||
from openslides.utils.utils import split_element_id
|
from openslides.utils.utils import split_element_id
|
||||||
|
|
||||||
from .. import __license__ as license, __url__ as url, __version__ as version
|
from .. import __license__ as license, __url__ as url, __version__ as version
|
||||||
@ -425,6 +426,7 @@ class ConfigViewSet(ModelViewSet):
|
|||||||
# Validate and change value.
|
# Validate and change value.
|
||||||
try:
|
try:
|
||||||
config[key] = value
|
config[key] = value
|
||||||
|
self.autoupdate_for_key(key)
|
||||||
except ConfigNotFound:
|
except ConfigNotFound:
|
||||||
raise Http404
|
raise Http404
|
||||||
except ConfigError as err:
|
except ConfigError as err:
|
||||||
@ -461,6 +463,7 @@ class ConfigViewSet(ModelViewSet):
|
|||||||
for entry in request.data:
|
for entry in request.data:
|
||||||
try:
|
try:
|
||||||
config[entry["key"]] = entry["value"]
|
config[entry["key"]] = entry["value"]
|
||||||
|
self.autoupdate_for_key(entry["key"])
|
||||||
except ConfigError as err:
|
except ConfigError as err:
|
||||||
errors[entry["key"]] = str(err)
|
errors[entry["key"]] = str(err)
|
||||||
|
|
||||||
@ -484,9 +487,14 @@ class ConfigViewSet(ModelViewSet):
|
|||||||
and config[key] != config_variable.default_value
|
and config[key] != config_variable.default_value
|
||||||
):
|
):
|
||||||
config[key] = config_variable.default_value
|
config[key] = config_variable.default_value
|
||||||
|
self.autoupdate_for_key(key)
|
||||||
|
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
|
def autoupdate_for_key(self, key):
|
||||||
|
if key == "agenda_list_of_speakers_speaker_note_for_everyone":
|
||||||
|
inform_changed_data(ListOfSpeakers.objects.all())
|
||||||
|
|
||||||
|
|
||||||
class ProjectorMessageViewSet(ModelViewSet):
|
class ProjectorMessageViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
|
@ -529,27 +529,6 @@ class ManageSpeaker(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
def test_mark_speaker(self):
|
|
||||||
Speaker.objects.add(self.user, self.list_of_speakers)
|
|
||||||
response = self.client.patch(
|
|
||||||
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk]),
|
|
||||||
{"user": self.user.pk, "marked": True},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertTrue(Speaker.objects.get().marked)
|
|
||||||
|
|
||||||
def test_mark_speaker_non_admin(self):
|
|
||||||
self.make_admin_delegate()
|
|
||||||
Speaker.objects.add(self.user, self.list_of_speakers)
|
|
||||||
|
|
||||||
response = self.client.patch(
|
|
||||||
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk]),
|
|
||||||
{"user": self.user.pk},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 403)
|
|
||||||
|
|
||||||
# re-add last speaker
|
# re-add last speaker
|
||||||
def util_add_user_as_last_speaker(self):
|
def util_add_user_as_last_speaker(self):
|
||||||
speaker = Speaker.objects.add(self.user, self.list_of_speakers)
|
speaker = Speaker.objects.add(self.user, self.list_of_speakers)
|
||||||
@ -649,6 +628,109 @@ class ManageSpeaker(TestCase):
|
|||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateSpeaker(TestCase):
|
||||||
|
def advancedSetUp(self):
|
||||||
|
self.list_of_speakers = Topic.objects.create(
|
||||||
|
title="test_title_aZaedij4gohn5eeQu8fe"
|
||||||
|
).list_of_speakers
|
||||||
|
self.user, _ = self.create_user()
|
||||||
|
self.admin = get_user_model().objects.get(username="admin")
|
||||||
|
|
||||||
|
def test_mark_speaker(self):
|
||||||
|
speaker = Speaker.objects.add(self.user, self.list_of_speakers)
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("speaker-detail", args=[speaker.pk]),
|
||||||
|
{"marked": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(Speaker.objects.get().marked)
|
||||||
|
|
||||||
|
def test_mark_speaker_non_admin(self):
|
||||||
|
self.make_admin_delegate()
|
||||||
|
speaker = Speaker.objects.add(self.user, self.list_of_speakers)
|
||||||
|
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("speaker-detail", args=[speaker.pk]),
|
||||||
|
{"marked": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertFalse(Speaker.objects.get().marked)
|
||||||
|
|
||||||
|
def test_mark_speaker_self(self):
|
||||||
|
config["agenda_list_of_speakers_can_set_mark_self"] = True
|
||||||
|
self.make_admin_delegate()
|
||||||
|
speaker = Speaker.objects.add(self.admin, self.list_of_speakers)
|
||||||
|
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("speaker-detail", args=[speaker.pk]),
|
||||||
|
{"marked": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(Speaker.objects.get().marked)
|
||||||
|
|
||||||
|
def test_mark_speaker_self_not_allowed(self):
|
||||||
|
self.make_admin_delegate()
|
||||||
|
speaker = Speaker.objects.add(self.admin, self.list_of_speakers)
|
||||||
|
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("speaker-detail", args=[speaker.pk]),
|
||||||
|
{"marked": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertFalse(Speaker.objects.get().marked)
|
||||||
|
|
||||||
|
def test_pro_speech_admin(self):
|
||||||
|
config["agenda_list_of_speakers_enable_pro_contra_speech"] = True
|
||||||
|
speaker = Speaker.objects.add(self.user, self.list_of_speakers)
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("speaker-detail", args=[speaker.pk]),
|
||||||
|
{"pro_speech": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(Speaker.objects.get().pro_speech)
|
||||||
|
|
||||||
|
def test_pro_speech_disabled(self):
|
||||||
|
speaker = Speaker.objects.add(self.user, self.list_of_speakers)
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("speaker-detail", args=[speaker.pk]),
|
||||||
|
{"pro_speech": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertFalse(Speaker.objects.get().pro_speech)
|
||||||
|
|
||||||
|
def test_pro_speech_non_admin(self):
|
||||||
|
config["agenda_list_of_speakers_enable_pro_contra_speech"] = True
|
||||||
|
self.make_admin_delegate()
|
||||||
|
speaker = Speaker.objects.add(self.user, self.list_of_speakers)
|
||||||
|
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("speaker-detail", args=[speaker.pk]),
|
||||||
|
{"pro_speech": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertFalse(Speaker.objects.get().pro_speech)
|
||||||
|
|
||||||
|
def test_pro_speech_self(self):
|
||||||
|
config["agenda_list_of_speakers_enable_pro_contra_speech"] = True
|
||||||
|
self.make_admin_delegate()
|
||||||
|
speaker = Speaker.objects.add(self.admin, self.list_of_speakers)
|
||||||
|
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse("speaker-detail", args=[speaker.pk]),
|
||||||
|
{"pro_speech": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(Speaker.objects.get().pro_speech)
|
||||||
|
|
||||||
|
|
||||||
class Speak(TestCase):
|
class Speak(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests view to begin or end speech.
|
Tests view to begin or end speech.
|
||||||
|
@ -32,7 +32,10 @@ class ListOfSpeakersViewSetManageSpeaker(TestCase):
|
|||||||
self.view_instance.manage_speaker(self.request)
|
self.view_instance.manage_speaker(self.request)
|
||||||
|
|
||||||
mock_speaker.objects.add.assert_called_with(
|
mock_speaker.objects.add.assert_called_with(
|
||||||
self.request.user, self.mock_list_of_speakers, point_of_order=False
|
self.request.user,
|
||||||
|
self.mock_list_of_speakers,
|
||||||
|
point_of_order=False,
|
||||||
|
note=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("openslides.agenda.views.inform_changed_data")
|
@patch("openslides.agenda.views.inform_changed_data")
|
||||||
@ -56,7 +59,7 @@ class ListOfSpeakersViewSetManageSpeaker(TestCase):
|
|||||||
|
|
||||||
MockUser.objects.get.assert_called_with(pk=2)
|
MockUser.objects.get.assert_called_with(pk=2)
|
||||||
mock_speaker.objects.add.assert_called_with(
|
mock_speaker.objects.add.assert_called_with(
|
||||||
mock_user, self.mock_list_of_speakers, point_of_order=False
|
mock_user, self.mock_list_of_speakers, point_of_order=False, note=None
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("openslides.agenda.views.Speaker")
|
@patch("openslides.agenda.views.Speaker")
|
||||||
|
Loading…
Reference in New Issue
Block a user