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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 0 for the anonymous
|
||||
*/
|
||||
public get userId(): number {
|
||||
return this.user?.id || 0;
|
||||
}
|
||||
|
||||
public get isAnonymous(): boolean {
|
||||
return !this.user || this.user.id === 0;
|
||||
}
|
||||
|
@ -161,10 +161,11 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit
|
||||
public async createSpeaker(
|
||||
listOfSpeakers: ViewListOfSpeakers,
|
||||
userId: number,
|
||||
pointOfOrder?: boolean
|
||||
pointOfOrder?: boolean,
|
||||
note?: string
|
||||
): Promise<Identifiable> {
|
||||
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
|
||||
*
|
||||
* @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
|
||||
* Toggles the mark for a given speaker.
|
||||
*/
|
||||
public async markSpeaker(listOfSpeakers: ViewListOfSpeakers, speaker: ViewSpeaker, marked: boolean): Promise<void> {
|
||||
const restUrl = this.getRestUrl(listOfSpeakers.id, 'manage_speaker');
|
||||
await this.httpService.patch(restUrl, { user: speaker.user.id, marked: marked });
|
||||
public async toggleMarked(speaker: ViewSpeaker): Promise<void> {
|
||||
await this.httpService.put(`/rest/agenda/speaker/${speaker.id}/`, { marked: !speaker.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="number">{{ number + 1 }}.</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">
|
||||
{{ durationString(speaker) }} ({{ 'Start time' | translate }}: {{ startTimeToString(speaker) }})
|
||||
</div>
|
||||
@ -49,10 +51,24 @@
|
||||
<mat-icon>mic</mat-icon>
|
||||
</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">
|
||||
|
||||
<!-- point of order visible for everyone -->
|
||||
<button
|
||||
mat-icon-button
|
||||
@ -84,70 +100,136 @@
|
||||
>
|
||||
<!-- implicit speaker references into the component using ng-template slot -->
|
||||
<ng-template let-speaker>
|
||||
<span *osPerms="'agenda.can_manage_list_of_speakers'; and: !speaker.point_of_order">
|
||||
<!-- Speaker count -->
|
||||
<span *ngIf="hasSpokenCount(speaker)" class="red-warning-text speaker-warning">
|
||||
{{ hasSpokenCount(speaker) + 1 }}. <span>{{ 'contribution' | translate }}</span>
|
||||
</span>
|
||||
<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>
|
||||
|
||||
<!-- First contribution -->
|
||||
<span
|
||||
*ngIf="showFistContributionHint && isFirstContribution(speaker)"
|
||||
class="red-warning-text speaker-warning"
|
||||
>
|
||||
{{ 'First speech' | translate }}
|
||||
</span>
|
||||
<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>
|
||||
|
||||
<!-- Speaker gender -->
|
||||
<span *ngIf="speaker.gender">({{ speaker.gender | translate }})</span>
|
||||
</span>
|
||||
<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>
|
||||
|
||||
<!-- Start, start and delete buttons -->
|
||||
<span>
|
||||
<!-- start button -->
|
||||
<button
|
||||
mat-icon-button
|
||||
matTooltip="{{ 'Begin speech' | translate }}"
|
||||
(click)="onStartButton(speaker)"
|
||||
*osPerms="'agenda.can_manage_list_of_speakers'"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
</button>
|
||||
<!-- 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 -->
|
||||
<span *ngIf="hasSpokenCount(speaker)" class="red-warning-text speaker-warning">
|
||||
{{ hasSpokenCount(speaker) + 1 }}. <span>{{ 'contribution' | translate }}</span>
|
||||
</span>
|
||||
|
||||
<!-- star button -->
|
||||
<button
|
||||
mat-icon-button
|
||||
matTooltip="{{ 'Mark speaker' | translate }}"
|
||||
(click)="onMarkButton(speaker)"
|
||||
*osPerms="'agenda.can_manage_list_of_speakers'; and: !speaker.point_of_order"
|
||||
>
|
||||
<mat-icon>{{ speaker.marked ? 'star' : 'star_border' }}</mat-icon>
|
||||
</button>
|
||||
<!-- First contribution -->
|
||||
<span
|
||||
*ngIf="showFistContributionHint && isFirstContribution(speaker)"
|
||||
class="red-warning-text speaker-warning"
|
||||
>
|
||||
{{ 'First speech' | translate }}
|
||||
</span>
|
||||
|
||||
<!-- 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>
|
||||
<!-- Speaker gender -->
|
||||
<span *ngIf="speaker.gender">({{ speaker.gender | translate }})</span>
|
||||
</span>
|
||||
|
||||
<!-- 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>
|
||||
</span>
|
||||
<!-- For normal users -->
|
||||
<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>
|
||||
|
||||
<!-- For thouse without LOS -->
|
||||
<span *osPerms="'agenda.can_manage_list_of_speakers'; complement: true">
|
||||
<mat-icon *ngIf="speaker.marked"> star </mat-icon>
|
||||
</span>
|
||||
<!-- 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 -->
|
||||
<button
|
||||
mat-icon-button
|
||||
matTooltip="{{ 'Begin speech' | translate }}"
|
||||
(click)="onStartButton(speaker)"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- more menu button -->
|
||||
<button
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="manageSpeakerMenu"
|
||||
[matMenuTriggerData]="{ speaker: speaker }"
|
||||
>
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</os-sorting-list>
|
||||
</div>
|
||||
@ -188,24 +270,63 @@
|
||||
<mat-icon>add</mat-icon>
|
||||
<span>{{ 'Add me' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="isOpInWaitlist()"
|
||||
mat-stroked-button
|
||||
[disabled]="closed"
|
||||
(click)="removeSpeaker()"
|
||||
>
|
||||
<button *ngIf="isOpInWaitlist()" mat-stroked-button [disabled]="closed" (click)="removeSpeaker()">
|
||||
<mat-icon>remove</mat-icon>
|
||||
<span>{{ 'Remove me' | translate }}</span>
|
||||
</button>
|
||||
|
||||
<!-- 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>
|
||||
<span>{{ 'Point of order' | translate }}</span>
|
||||
</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>
|
||||
<span>{{ 'Withdraw point of order' | translate }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</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;
|
||||
margin: 50px 25px 20px 25px;
|
||||
box-shadow: 0px 3px 10px 0px rgba(0, 0, 0, 0.25);
|
||||
line-height: 1;
|
||||
|
||||
.prefix {
|
||||
display: table-cell;
|
||||
@ -99,6 +100,13 @@
|
||||
font-weight: bold;
|
||||
padding-left: 10px;
|
||||
width: 100%;
|
||||
|
||||
.active-speaker-subtitle {
|
||||
* {
|
||||
font-weight: initial;
|
||||
font-size: 95%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.suffix {
|
||||
@ -111,6 +119,34 @@
|
||||
|
||||
.waiting-list {
|
||||
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 {
|
||||
@ -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
|
||||
} from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
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 { PromptService } from 'app/core/ui-services/prompt.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 { SpeakerState, ViewSpeaker } from 'app/site/agenda/models/view-speaker';
|
||||
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
||||
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 { SortingListComponent } from '../sorting-list/sorting-list.component';
|
||||
|
||||
@ -63,6 +67,9 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
||||
}
|
||||
|
||||
private pointOfOrderEnabled: boolean;
|
||||
public enableProContraSpeech: boolean;
|
||||
private canSetMarkSelf: boolean;
|
||||
private noteForAll: boolean;
|
||||
|
||||
public get title(): string {
|
||||
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;
|
||||
}
|
||||
|
||||
public get showSpeakersOrderNote(): boolean {
|
||||
return this.noteForAll || this.opCanManage;
|
||||
}
|
||||
|
||||
@Input()
|
||||
public set speakers(los: ViewListOfSpeakers) {
|
||||
this.setListOfSpeakers(los);
|
||||
@ -113,7 +124,8 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
||||
private userRepository: UserRepositoryService,
|
||||
private config: ConfigService,
|
||||
private viewport: ViewportService,
|
||||
private cd: ChangeDetectorRef
|
||||
private cd: ChangeDetectorRef,
|
||||
private dialog: MatDialog
|
||||
) {
|
||||
super(title, translate, snackBar);
|
||||
this.addSpeakerForm = new FormGroup({ user_id: new FormControl() });
|
||||
@ -150,6 +162,15 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
||||
// observe point of order settings
|
||||
this.config.get<boolean>('agenda_enable_point_of_order_speakers').subscribe(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,13 +230,18 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
||||
}
|
||||
|
||||
public async addPointOfOrder(): Promise<void> {
|
||||
const title = this.translate.instant('Are you sure you want to submit a point of order?');
|
||||
if (await this.promptService.open(title)) {
|
||||
try {
|
||||
await this.listOfSpeakersRepo.createSpeaker(this.viewListOfSpeakers, undefined, true);
|
||||
} catch (e) {
|
||||
this.raiseError(e);
|
||||
const dialogRef = this.dialog.open<PointOfOrderDialogComponent, ViewListOfSpeakers, Promise<Identifiable>>(
|
||||
PointOfOrderDialogComponent,
|
||||
{
|
||||
data: this.viewListOfSpeakers,
|
||||
...infoDialogSettings
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
await dialogRef.afterClosed().toPromise();
|
||||
} catch (e) {
|
||||
this.raiseError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -235,7 +261,7 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
||||
/**
|
||||
* 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> {
|
||||
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.
|
||||
*
|
||||
* @param speaker The speaker clicked on.
|
||||
*/
|
||||
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)"
|
||||
(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>
|
||||
</div>
|
||||
<div class="section-two">
|
||||
<!-- {number}. {item.getTitle()} -->
|
||||
<span *ngIf="count">{{ i + 1 }}. </span>
|
||||
<span>{{ item?.getTitle() }}</span>
|
||||
|
||||
<!-- Count number -->
|
||||
<div class="count" *ngIf="count">
|
||||
<span>{{ i + 1 }}.</span>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<!-- Drag prev -->
|
||||
<div class="line" *cdkDragPreview>
|
||||
<div class="spacer.left-10" *ngIf="multiSelectedIndex.length > 0">
|
||||
{{ multiSelectedIndex.length }} <span>{{ 'items selected' | translate }}</span>
|
||||
|
@ -9,31 +9,26 @@
|
||||
min-height: 50px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.section-one {
|
||||
.drag-indicator {
|
||||
display: table-cell;
|
||||
padding: 0 10px;
|
||||
line-height: 0px;
|
||||
vertical-align: middle;
|
||||
padding: 0 0.5em;
|
||||
line-height: 0px;
|
||||
width: 50px;
|
||||
color: slategrey;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.section-two {
|
||||
.count {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
min-width: 2em;
|
||||
padding-left: 1.25em;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
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 user_id: number;
|
||||
public weight: number;
|
||||
public note?: string;
|
||||
public marked: boolean;
|
||||
public item_id: number;
|
||||
public point_of_order: boolean;
|
||||
public pro_speech?: boolean;
|
||||
|
||||
/**
|
||||
* 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 { NgParticlesModule } from 'ng-particles';
|
||||
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.
|
||||
@ -305,7 +306,8 @@ import { ApplauseParticleDisplayComponent } from './components/applause-particle
|
||||
AssignmentPollDetailContentComponent,
|
||||
JitsiComponent,
|
||||
VideoPlayerComponent,
|
||||
ListOfSpeakersContentComponent
|
||||
ListOfSpeakersContentComponent,
|
||||
PointOfOrderDialogComponent
|
||||
],
|
||||
declarations: [
|
||||
PermsDirective,
|
||||
@ -372,7 +374,8 @@ import { ApplauseParticleDisplayComponent } from './components/applause-particle
|
||||
ListOfSpeakersContentComponent,
|
||||
ApplauseBarDisplayComponent,
|
||||
ProgressComponent,
|
||||
ApplauseParticleDisplayComponent
|
||||
ApplauseParticleDisplayComponent,
|
||||
PointOfOrderDialogComponent
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
@ -155,15 +155,19 @@
|
||||
>
|
||||
<!-- implicit item references into the component using ng-template slot -->
|
||||
<ng-template let-item>
|
||||
<span *ngIf="hasPerms('manage')">
|
||||
<button
|
||||
mat-icon-button
|
||||
matTooltip="{{ 'Remove candidate' | translate }}"
|
||||
(click)="removeUser(item)"
|
||||
>
|
||||
<mat-icon>clear</mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
<div class="single-candidate-line">
|
||||
<span *ngIf="hasPerms('manage')"> {{ item.getTitle() }} </span>
|
||||
|
||||
<span *ngIf="hasPerms('manage')">
|
||||
<button
|
||||
mat-icon-button
|
||||
matTooltip="{{ 'Remove candidate' | translate }}"
|
||||
(click)="removeUser(item)"
|
||||
>
|
||||
<mat-icon>clear</mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
</os-sorting-list>
|
||||
</div>
|
||||
|
@ -40,6 +40,16 @@
|
||||
|
||||
.candidates-list {
|
||||
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 {
|
||||
|
@ -15,9 +15,16 @@
|
||||
</div>
|
||||
<os-sorting-list (sortEvent)="onListUpdate($event)" [input]="motionObservable" #sorter>
|
||||
<ng-template let-motion>
|
||||
<mat-basic-chip class="bluegrey" disableRipple matTooltip="{{ 'Sequential number' | translate }}">
|
||||
{{ motion.id }}
|
||||
</mat-basic-chip>
|
||||
<div class="single-motion-line">
|
||||
<span>
|
||||
{{ motion.getTitle() }}
|
||||
</span>
|
||||
<span>
|
||||
<mat-basic-chip class="bluegrey" disableRipple matTooltip="{{ 'Sequential number' | translate }}">
|
||||
{{ motion.id }}
|
||||
</mat-basic-chip>
|
||||
</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
</os-sorting-list>
|
||||
</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 -->
|
||||
<mat-card class="os-form-card">
|
||||
<!-- 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>
|
||||
|
@ -27,9 +27,22 @@
|
||||
>
|
||||
<!-- implicit user references into the component using ng-template slot -->
|
||||
<ng-template let-user>
|
||||
<button type="button" mat-icon-button matTooltip="{{ 'Remove' | translate }}" (click)="onRemove(user)">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<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>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
</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 {
|
||||
border-radius: 16px !important;
|
||||
padding: 5px 15px !important;
|
||||
|
@ -2,6 +2,7 @@ export interface SlideSpeaker {
|
||||
user: string;
|
||||
marked: boolean;
|
||||
point_of_order: boolean;
|
||||
pro_speech?: boolean;
|
||||
}
|
||||
|
||||
export interface CommonListOfSpeakersSlideData {
|
||||
|
@ -17,7 +17,9 @@
|
||||
<div *ngFor="let speaker of data.data.finished">
|
||||
{{ speaker.user }}
|
||||
<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>
|
||||
|
||||
@ -26,7 +28,9 @@
|
||||
<mat-icon class="micicon">mic</mat-icon>
|
||||
{{ data.data.current.user }}
|
||||
<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>
|
||||
|
||||
<!-- Next speakers -->
|
||||
@ -35,7 +39,9 @@
|
||||
<li *ngFor="let speaker of data.data.waiting">
|
||||
{{ speaker.user }}
|
||||
<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>
|
||||
</ol>
|
||||
</div>
|
||||
|
@ -10,6 +10,8 @@
|
||||
<li *ngFor="let speaker of nextSpeakers" class="one-line">
|
||||
{{ speaker.user }}
|
||||
<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>
|
||||
</ol>
|
||||
</div>
|
||||
|
@ -173,15 +173,27 @@ b,
|
||||
}
|
||||
|
||||
.red-warning-text {
|
||||
color: red;
|
||||
color: red !important;
|
||||
.mat-icon {
|
||||
color: red !important;
|
||||
}
|
||||
}
|
||||
|
||||
.green-text {
|
||||
// TODO better name/theming
|
||||
color: #5a5;
|
||||
color: #5a5 !important;
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
@ -17,7 +17,7 @@ class AgendaAppConfig(AppConfig):
|
||||
listen_to_related_object_post_delete,
|
||||
listen_to_related_object_post_save,
|
||||
)
|
||||
from .views import ItemViewSet, ListOfSpeakersViewSet
|
||||
from .views import ItemViewSet, ListOfSpeakersViewSet, SpeakerViewSet
|
||||
|
||||
# Connect signals.
|
||||
post_save.connect(
|
||||
@ -38,6 +38,10 @@ class AgendaAppConfig(AppConfig):
|
||||
self.get_model("ListOfSpeakers").get_collection_string(),
|
||||
ListOfSpeakersViewSet,
|
||||
)
|
||||
router.register(
|
||||
self.get_model("Speaker").get_collection_string(),
|
||||
SpeakerViewSet,
|
||||
)
|
||||
|
||||
def get_config_variables(self):
|
||||
from .config_variables import get_config_variables
|
||||
|
@ -220,3 +220,33 @@ def get_config_variables():
|
||||
group="Agenda",
|
||||
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.
|
||||
"""
|
||||
|
||||
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
|
||||
list of speakers and that someone is twice on one list (off coming
|
||||
@ -450,6 +457,7 @@ class SpeakerManager(models.Manager):
|
||||
user=user,
|
||||
weight=weight,
|
||||
point_of_order=point_of_order,
|
||||
note=note,
|
||||
)
|
||||
speaker.save(
|
||||
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.
|
||||
"""
|
||||
|
||||
note = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
marked = models.BooleanField(default=False)
|
||||
"""
|
||||
Marks a speaker.
|
||||
@ -544,6 +554,8 @@ class Speaker(RESTModelMixin, models.Model):
|
||||
Identifies the speaker as someone with a point of order
|
||||
"""
|
||||
|
||||
pro_speech = models.BooleanField(null=True, blank=True, default=None)
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
permissions = (("can_be_speaker", "Can put oneself on the list of speakers"),)
|
||||
|
@ -16,8 +16,19 @@ class SpeakerSerializer(ModelSerializer):
|
||||
"begin_time",
|
||||
"end_time",
|
||||
"weight",
|
||||
"note",
|
||||
"marked",
|
||||
"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.db import transaction
|
||||
from django.db.utils import IntegrityError
|
||||
from django.http.request import QueryDict
|
||||
|
||||
from openslides.core.config import config
|
||||
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
|
||||
them. Send POST {'user': <user_id>} to add a new speaker.
|
||||
Send POST {'user': <user_id>, 'point_of_order': True } to add a point
|
||||
of order to the list of speakers.
|
||||
Send POST {'user': <user_id>, 'point_of_order': True, 'note': <optional string> }
|
||||
to add a pointof order to the list of speakers.
|
||||
Omit data to add yourself. Send DELETE {'speaker': <speaker_id>} or
|
||||
DELETE {'speaker': [<speaker_id>, <speaker_id>, ...]} to remove one or
|
||||
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
|
||||
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):
|
||||
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.
|
||||
if user_id is None:
|
||||
# Add oneself
|
||||
@ -352,7 +356,7 @@ class ListOfSpeakersViewSet(UpdateModelMixin, TreeSortMixin, GenericViewSet):
|
||||
# list of coming speakers.
|
||||
try:
|
||||
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:
|
||||
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.
|
||||
inform_changed_data(user, disable_history=True)
|
||||
|
||||
# Set 'marked' for the speaker
|
||||
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'
|
||||
elif request.method == "DELETE":
|
||||
speaker_ids = request.data.get("speaker")
|
||||
|
||||
# Check permissions and other conditions. Get speaker instance.
|
||||
@ -439,6 +413,8 @@ class ListOfSpeakersViewSet(UpdateModelMixin, TreeSortMixin, GenericViewSet):
|
||||
# send autoupdate if speakers are deleted
|
||||
if deleted_some_speakers:
|
||||
inform_changed_data(list_of_speakers)
|
||||
else:
|
||||
raise ValidationError({"detail": "Invalid method"})
|
||||
|
||||
return Response()
|
||||
|
||||
@ -576,3 +552,42 @@ class ListOfSpeakersViewSet(UpdateModelMixin, TreeSortMixin, GenericViewSet):
|
||||
Speaker.objects.all().delete()
|
||||
inform_changed_data(ListOfSpeakers.objects.all())
|
||||
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.generic.base import View
|
||||
|
||||
from openslides.agenda.models import ListOfSpeakers
|
||||
from openslides.utils.utils import split_element_id
|
||||
|
||||
from .. import __license__ as license, __url__ as url, __version__ as version
|
||||
@ -425,6 +426,7 @@ class ConfigViewSet(ModelViewSet):
|
||||
# Validate and change value.
|
||||
try:
|
||||
config[key] = value
|
||||
self.autoupdate_for_key(key)
|
||||
except ConfigNotFound:
|
||||
raise Http404
|
||||
except ConfigError as err:
|
||||
@ -461,6 +463,7 @@ class ConfigViewSet(ModelViewSet):
|
||||
for entry in request.data:
|
||||
try:
|
||||
config[entry["key"]] = entry["value"]
|
||||
self.autoupdate_for_key(entry["key"])
|
||||
except ConfigError as err:
|
||||
errors[entry["key"]] = str(err)
|
||||
|
||||
@ -484,9 +487,14 @@ class ConfigViewSet(ModelViewSet):
|
||||
and config[key] != config_variable.default_value
|
||||
):
|
||||
config[key] = config_variable.default_value
|
||||
self.autoupdate_for_key(key)
|
||||
|
||||
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):
|
||||
"""
|
||||
|
@ -529,27 +529,6 @@ class ManageSpeaker(TestCase):
|
||||
)
|
||||
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
|
||||
def util_add_user_as_last_speaker(self):
|
||||
speaker = Speaker.objects.add(self.user, self.list_of_speakers)
|
||||
@ -649,6 +628,109 @@ class ManageSpeaker(TestCase):
|
||||
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):
|
||||
"""
|
||||
Tests view to begin or end speech.
|
||||
|
@ -32,7 +32,10 @@ class ListOfSpeakersViewSetManageSpeaker(TestCase):
|
||||
self.view_instance.manage_speaker(self.request)
|
||||
|
||||
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")
|
||||
@ -56,7 +59,7 @@ class ListOfSpeakersViewSetManageSpeaker(TestCase):
|
||||
|
||||
MockUser.objects.get.assert_called_with(pk=2)
|
||||
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")
|
||||
|
Loading…
Reference in New Issue
Block a user