Add "point of order" feature to ListOfSpeakers
Adds the option "point of order" to the list of speakers - You can make a point of order even though you normally have no permission to add yourself to the ListOfSpeakers - You can make a point of order even though you are already on theListOfSpeakers (but you may only be there once) - new points of order will be on top of the list of speakers - Point of orders will be highlighted by a red triangle This feature can be used to request to speak with a higher level of urgency
This commit is contained in:
parent
266f9b73e9
commit
ccc48e6b3f
@ -143,6 +143,14 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public async delete(viewModel: ViewListOfSpeakers): Promise<void> {
|
||||||
|
throw new Error('Not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async create(model: ListOfSpeakers): Promise<Identifiable> {
|
||||||
|
throw new Error('Not supported');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new speaker to a list of speakers.
|
* Add a new speaker to a list of speakers.
|
||||||
* Sends the users id to the server
|
* Sends the users id to the server
|
||||||
@ -150,9 +158,13 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit
|
|||||||
* @param userId {@link User} id of the new speaker
|
* @param userId {@link User} id of the new speaker
|
||||||
* @param listOfSpeakers the target agenda item
|
* @param listOfSpeakers the target agenda item
|
||||||
*/
|
*/
|
||||||
public async createSpeaker(listOfSpeakers: ViewListOfSpeakers, userId: number): Promise<Identifiable> {
|
public async createSpeaker(
|
||||||
|
listOfSpeakers: ViewListOfSpeakers,
|
||||||
|
userId: number,
|
||||||
|
pointOfOrder?: boolean
|
||||||
|
): 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 });
|
return await this.httpService.post<Identifiable>(restUrl, { user: userId, point_of_order: pointOfOrder });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -162,9 +174,20 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit
|
|||||||
* @param speakerId (otional) the speakers id. If no id is given, the speaker with the
|
* @param speakerId (otional) the speakers id. If no id is given, the speaker with the
|
||||||
* current operator is removed.
|
* current operator is removed.
|
||||||
*/
|
*/
|
||||||
public async delete(listOfSpeakers: ViewListOfSpeakers, speakerId?: number): Promise<void> {
|
public async deleteSpeaker(
|
||||||
|
listOfSpeakers: ViewListOfSpeakers,
|
||||||
|
speakerId?: number,
|
||||||
|
pointOfOrder?: boolean
|
||||||
|
): Promise<void> {
|
||||||
const restUrl = this.getRestUrl(listOfSpeakers.id, 'manage_speaker');
|
const restUrl = this.getRestUrl(listOfSpeakers.id, 'manage_speaker');
|
||||||
await this.httpService.delete(restUrl, speakerId ? { speaker: speakerId } : null);
|
const payload: { speaker?: number; point_of_order?: boolean } = {};
|
||||||
|
if (speakerId) {
|
||||||
|
payload.speaker = speakerId;
|
||||||
|
}
|
||||||
|
if (pointOfOrder) {
|
||||||
|
payload.point_of_order = pointOfOrder;
|
||||||
|
}
|
||||||
|
await this.httpService.delete(restUrl, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4,9 +4,7 @@
|
|||||||
<span>
|
<span>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</span>
|
</span>
|
||||||
<mat-icon *ngIf="closed" matTooltip="{{ 'The list of speakers is closed.' | translate }}">
|
<mat-icon *ngIf="closed" matTooltip="{{ 'The list of speakers is closed.' | translate }}"> lock </mat-icon>
|
||||||
lock
|
|
||||||
</mat-icon>
|
|
||||||
</h1>
|
</h1>
|
||||||
<span *ngIf="customTitle">
|
<span *ngIf="customTitle">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
@ -22,6 +20,7 @@
|
|||||||
<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="time">
|
<div class="time">
|
||||||
{{ durationString(speaker) }} ({{ 'Start time' | translate }}: {{ startTimeToString(speaker) }})
|
{{ durationString(speaker) }} ({{ 'Start time' | translate }}: {{ startTimeToString(speaker) }})
|
||||||
</div>
|
</div>
|
||||||
@ -30,7 +29,7 @@
|
|||||||
mat-icon-button
|
mat-icon-button
|
||||||
matTooltip="{{ 'Remove' | translate }}"
|
matTooltip="{{ 'Remove' | translate }}"
|
||||||
*osPerms="'agenda.can_manage_list_of_speakers'"
|
*osPerms="'agenda.can_manage_list_of_speakers'"
|
||||||
(click)="onDeleteButton(speaker)"
|
(click)="removeSpeaker(speaker)"
|
||||||
>
|
>
|
||||||
<mat-icon>close</mat-icon>
|
<mat-icon>close</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
@ -53,6 +52,15 @@
|
|||||||
<span class="name">{{ activeSpeaker.getListTitle() }}</span>
|
<span class="name">{{ activeSpeaker.getListTitle() }}</span>
|
||||||
|
|
||||||
<span class="suffix">
|
<span class="suffix">
|
||||||
|
|
||||||
|
<!-- point of order visible for everyone -->
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
matTooltip="{{ 'Point of order' | translate }}"
|
||||||
|
*ngIf="activeSpeaker.point_of_order"
|
||||||
|
>
|
||||||
|
<mat-icon color="warn">warning</mat-icon>
|
||||||
|
</button>
|
||||||
<!-- Stop speaker button -->
|
<!-- Stop speaker button -->
|
||||||
<button
|
<button
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
@ -76,7 +84,7 @@
|
|||||||
>
|
>
|
||||||
<!-- 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'">
|
<span *osPerms="'agenda.can_manage_list_of_speakers'; 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>
|
||||||
@ -95,12 +103,13 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Start, start and delete buttons -->
|
<!-- Start, start and delete buttons -->
|
||||||
<span *osPerms="'agenda.can_manage_list_of_speakers'">
|
<span>
|
||||||
<!-- 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>
|
||||||
@ -110,21 +119,34 @@
|
|||||||
mat-icon-button
|
mat-icon-button
|
||||||
matTooltip="{{ 'Mark speaker' | translate }}"
|
matTooltip="{{ 'Mark speaker' | translate }}"
|
||||||
(click)="onMarkButton(speaker)"
|
(click)="onMarkButton(speaker)"
|
||||||
|
*osPerms="'agenda.can_manage_list_of_speakers'; and: !speaker.point_of_order"
|
||||||
>
|
>
|
||||||
<mat-icon>{{ speaker.marked ? 'star' : 'star_border' }}</mat-icon>
|
<mat-icon>{{ speaker.marked ? 'star' : 'star_border' }}</mat-icon>
|
||||||
</button>
|
</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 -->
|
<!-- delete button -->
|
||||||
<button mat-icon-button matTooltip="{{ 'Remove' | translate }}" (click)="onDeleteButton(speaker)">
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
matTooltip="{{ 'Remove' | translate }}"
|
||||||
|
(click)="removeSpeaker(speaker)"
|
||||||
|
*osPerms="'agenda.can_manage_list_of_speakers'"
|
||||||
|
>
|
||||||
<mat-icon>close</mat-icon>
|
<mat-icon>close</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- For thouse without LOS -->
|
<!-- For thouse without LOS -->
|
||||||
<span *osPerms="'agenda.can_manage_list_of_speakers'; complement: true">
|
<span *osPerms="'agenda.can_manage_list_of_speakers'; complement: true">
|
||||||
<mat-icon *ngIf="speaker.marked">
|
<mat-icon *ngIf="speaker.marked"> star </mat-icon>
|
||||||
star
|
|
||||||
</mat-icon>
|
|
||||||
</span>
|
</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</os-sorting-list>
|
</os-sorting-list>
|
||||||
@ -155,17 +177,37 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="add-self-buttons">
|
||||||
<!-- Add me and remove me if OP has correct permission -->
|
<!-- Add me and remove me if OP has correct permission -->
|
||||||
<div *osPerms="'agenda.can_be_speaker'" class="add-self-buttons">
|
|
||||||
<div *ngIf="waitingSpeakers && !closed">
|
<button
|
||||||
<button mat-stroked-button [disabled]="closed" (click)="addNewSpeaker()" *ngIf="!isOpInList && canAddSelf">
|
*osPerms="'agenda.can_be_speaker'; and: !isOpInWaitlist()"
|
||||||
|
mat-stroked-button
|
||||||
|
[disabled]="closed || !canAddSelf"
|
||||||
|
(click)="addUserAsNewSpeaker()"
|
||||||
|
>
|
||||||
<mat-icon>add</mat-icon>
|
<mat-icon>add</mat-icon>
|
||||||
<span>{{ 'Add me' | translate }}</span>
|
<span>{{ 'Add me' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button mat-stroked-button (click)="onDeleteButton()" *ngIf="isOpInList">
|
|
||||||
|
<button
|
||||||
|
*osPerms="'agenda.can_be_speaker'; and: 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>
|
||||||
</div>
|
|
||||||
|
<!-- Point Of order -->
|
||||||
|
<button mat-raised-button color="warn" (click)="addPointOfOrder()" *ngIf="showPointOfOrders && !isOpInWaitlist(true)">
|
||||||
|
<mat-icon>warning</mat-icon>
|
||||||
|
<span>{{ 'Point of order' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
<button mat-raised-button color="warn" (click)="removePointOfOrder()" *ngIf="showPointOfOrders && isOpInWaitlist(true)">
|
||||||
|
<mat-icon>warning</mat-icon>
|
||||||
|
<span>{{ 'Remove point of order request' | translate }}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
@include desktop {
|
@include desktop {
|
||||||
.finished-speaker-grid {
|
.finished-speaker-grid {
|
||||||
grid-template-areas: 'number name time controls';
|
grid-template-areas: 'number name point-of-order time controls';
|
||||||
grid-template-columns: 30px 1fr min-content min-content;
|
grid-template-columns: 30px 1fr min-content min-content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -41,6 +41,18 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.point-of-order {
|
||||||
|
grid-area: point-of-order;
|
||||||
|
margin: 0;
|
||||||
|
//allows pushing this grid area as small as possible and aligns the end to the same level
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 80%;
|
||||||
|
|
||||||
|
.mat-icon {
|
||||||
|
line-height: 19px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.time {
|
.time {
|
||||||
grid-area: time;
|
grid-area: time;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -102,7 +114,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search-new-speaker-form {
|
.search-new-speaker-form {
|
||||||
padding: 15px 25px 10px 25px;
|
padding: 15px 25px 0 25px;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
||||||
.search-users-field {
|
.search-users-field {
|
||||||
@ -117,7 +129,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.add-self-buttons {
|
.add-self-buttons {
|
||||||
padding: 15px 0 20px 25px;
|
margin: 15px 25px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
button + button {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.speaker-warning {
|
.speaker-warning {
|
||||||
|
@ -58,6 +58,8 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
|||||||
|
|
||||||
public showFistContributionHint: boolean;
|
public showFistContributionHint: boolean;
|
||||||
|
|
||||||
|
public showPointOfOrders: boolean;
|
||||||
|
|
||||||
public get title(): string {
|
public get title(): string {
|
||||||
return this.viewListOfSpeakers?.getTitle();
|
return this.viewListOfSpeakers?.getTitle();
|
||||||
}
|
}
|
||||||
@ -70,10 +72,6 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
|||||||
return this.operator.hasPerms(this.permission.agendaCanManageListOfSpeakers);
|
return this.operator.hasPerms(this.permission.agendaCanManageListOfSpeakers);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isOpInList(): boolean {
|
|
||||||
return this.waitingSpeakers.some(speaker => speaker.user_id === this.operator.user.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get canAddSelf(): boolean {
|
public get canAddSelf(): boolean {
|
||||||
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;
|
||||||
}
|
}
|
||||||
@ -98,7 +96,7 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
|||||||
private isListOfSpeakersEmptyEvent = new EventEmitter<boolean>();
|
private isListOfSpeakersEmptyEvent = new EventEmitter<boolean>();
|
||||||
|
|
||||||
@Output()
|
@Output()
|
||||||
private hasFinishesSpeakersEvent = new EventEmitter<boolean>();
|
private canReaddLastSpeakerEvent = new EventEmitter<boolean>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
title: Title,
|
title: Title,
|
||||||
@ -129,7 +127,7 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
|||||||
this.addSpeakerForm.valueChanges.subscribe(formResult => {
|
this.addSpeakerForm.valueChanges.subscribe(formResult => {
|
||||||
// resetting a form triggers a form.next(null) - check if user_id
|
// resetting a form triggers a form.next(null) - check if user_id
|
||||||
if (formResult && formResult.user_id) {
|
if (formResult && formResult.user_id) {
|
||||||
this.addNewSpeaker(formResult.user_id);
|
this.addUserAsNewSpeaker(formResult.user_id);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
// observe changes to the viewport
|
// observe changes to the viewport
|
||||||
@ -144,6 +142,10 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
|||||||
// observe changes to the agenda_show_first_contribution config
|
// observe changes to the agenda_show_first_contribution config
|
||||||
this.config.get<boolean>('agenda_show_first_contribution').subscribe(show => {
|
this.config.get<boolean>('agenda_show_first_contribution').subscribe(show => {
|
||||||
this.showFistContributionHint = show;
|
this.showFistContributionHint = show;
|
||||||
|
}),
|
||||||
|
// observe point of order settings
|
||||||
|
this.config.get<boolean>('agenda_enable_point_of_order_speakers').subscribe(show => {
|
||||||
|
this.showPointOfOrders = show;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -157,8 +159,16 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
|||||||
return this.isListOfSpeakersEmptyEvent.emit(!this.activeSpeaker);
|
return this.isListOfSpeakersEmptyEvent.emit(!this.activeSpeaker);
|
||||||
}
|
}
|
||||||
|
|
||||||
private hasFinishesSpeakers(): void {
|
private updateCanReaddLastSpeaker(): void {
|
||||||
this.hasFinishesSpeakersEvent.emit(this.finishedSpeakers?.length > 0);
|
let canReaddLast;
|
||||||
|
if (this.finishedSpeakers?.length > 0) {
|
||||||
|
const lastSpeaker = this.finishedSpeakers[this.finishedSpeakers.length - 1];
|
||||||
|
const isLastSpeakerWaiting = this.waitingSpeakers.some(speaker => speaker.user_id === lastSpeaker.user_id);
|
||||||
|
canReaddLast = !lastSpeaker.point_of_order && !isLastSpeakerWaiting;
|
||||||
|
} else {
|
||||||
|
canReaddLast = false;
|
||||||
|
}
|
||||||
|
this.canReaddLastSpeakerEvent.emit(canReaddLast);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -166,10 +176,13 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
|||||||
*
|
*
|
||||||
* @param userId the user id to add to the list. No parameter adds the operators user as speaker.
|
* @param userId the user id to add to the list. No parameter adds the operators user as speaker.
|
||||||
*/
|
*/
|
||||||
public addNewSpeaker(userId?: number): void {
|
public async addUserAsNewSpeaker(userId?: number): Promise<void> {
|
||||||
this.listOfSpeakersRepo
|
try {
|
||||||
.createSpeaker(this.viewListOfSpeakers, userId)
|
await this.listOfSpeakersRepo.createSpeaker(this.viewListOfSpeakers, userId);
|
||||||
.then(() => this.addSpeakerForm.reset(), this.raiseError);
|
this.addSpeakerForm.reset();
|
||||||
|
} catch (e) {
|
||||||
|
this.raiseError(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -178,13 +191,13 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
|||||||
* @param speaker optional speaker to remove. If none is given,
|
* @param speaker optional speaker to remove. If none is given,
|
||||||
* the operator themself is removed
|
* the operator themself is removed
|
||||||
*/
|
*/
|
||||||
public async onDeleteButton(speaker?: ViewSpeaker): Promise<void> {
|
public async removeSpeaker(speaker?: ViewSpeaker): Promise<void> {
|
||||||
const title = this.translate.instant(
|
const title = this.translate.instant(
|
||||||
'Are you sure you want to delete this speaker from this list of speakers?'
|
'Are you sure you want to delete this speaker from this list of speakers?'
|
||||||
);
|
);
|
||||||
if (await this.promptService.open(title)) {
|
if (await this.promptService.open(title)) {
|
||||||
try {
|
try {
|
||||||
await this.listOfSpeakersRepo.delete(this.viewListOfSpeakers, speaker ? speaker.id : null);
|
await this.listOfSpeakersRepo.deleteSpeaker(this.viewListOfSpeakers, speaker ? speaker.id : null);
|
||||||
this.filterUsers();
|
this.filterUsers();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.raiseError(e);
|
this.raiseError(e);
|
||||||
@ -192,6 +205,30 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async addPointOfOrder(): Promise<void> {
|
||||||
|
const title = this.translate.instant('Are you sure you want to submit a new point of order?');
|
||||||
|
if (await this.promptService.open(title)) {
|
||||||
|
try {
|
||||||
|
await this.listOfSpeakersRepo.createSpeaker(this.viewListOfSpeakers, undefined, true);
|
||||||
|
} catch (e) {
|
||||||
|
this.raiseError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public removePointOfOrder(): void {
|
||||||
|
this.listOfSpeakersRepo.deleteSpeaker(this.viewListOfSpeakers, undefined, true).catch(this.raiseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isOpInWaitlist(pointOfOrder: boolean = false): boolean {
|
||||||
|
if (!this.waitingSpeakers) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.waitingSpeakers.some(
|
||||||
|
speaker => speaker.user_id === this.operator.user.id && speaker.point_of_order === pointOfOrder
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Click on the mic button to mark a speaker as speaking
|
* Click on the mic button to mark a speaker as speaking
|
||||||
*
|
*
|
||||||
@ -268,7 +305,7 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.activeSpeaker = allSpeakers?.find(speaker => speaker.state === SpeakerState.CURRENT);
|
this.activeSpeaker = allSpeakers?.find(speaker => speaker.state === SpeakerState.CURRENT);
|
||||||
this.hasFinishesSpeakers();
|
this.updateCanReaddLastSpeaker();
|
||||||
this.isListOfSpeakersEmpty();
|
this.isListOfSpeakersEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,7 +330,7 @@ export class ListOfSpeakersContentComponent extends BaseViewComponentDirective i
|
|||||||
*/
|
*/
|
||||||
public async onCreateUser(username: string): Promise<void> {
|
public async onCreateUser(username: string): Promise<void> {
|
||||||
const newUser = await this.userRepository.createFromString(username);
|
const newUser = await this.userRepository.createFromString(username);
|
||||||
this.addNewSpeaker(newUser.id);
|
this.addUserAsNewSpeaker(newUser.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,7 +33,6 @@
|
|||||||
.section-three {
|
.section-three {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
vertical-align: middle;
|
|
||||||
width: auto;
|
width: auto;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ export class Speaker extends BaseModel<Speaker> {
|
|||||||
public weight: number;
|
public weight: number;
|
||||||
public marked: boolean;
|
public marked: boolean;
|
||||||
public item_id: number;
|
public item_id: number;
|
||||||
|
public point_of_order: 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
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
mat-icon-button
|
mat-icon-button
|
||||||
matTooltip="{{ 'Re-add last speaker' | translate }}"
|
matTooltip="{{ 'Re-add last speaker' | translate }}"
|
||||||
(click)="readdLastSpeaker()"
|
(click)="readdLastSpeaker()"
|
||||||
[disabled]="!hasFinishedSpeakers"
|
[disabled]="!canReaddLastSpeaker"
|
||||||
>
|
>
|
||||||
<mat-icon>undo</mat-icon>
|
<mat-icon>undo</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
@ -31,7 +31,7 @@
|
|||||||
[speakers]="viewListOfSpeakers"
|
[speakers]="viewListOfSpeakers"
|
||||||
[sortMode]="manualSortMode"
|
[sortMode]="manualSortMode"
|
||||||
(isListOfSpeakersEmptyEvent)="isListOfSpeakersEmpty = $event"
|
(isListOfSpeakersEmptyEvent)="isListOfSpeakersEmpty = $event"
|
||||||
(hasFinishesSpeakersEvent)="hasFinishedSpeakers = $event"
|
(canReaddLastSpeakerEvent)="canReaddLastSpeaker = $event"
|
||||||
></os-list-of-speakers-content>
|
></os-list-of-speakers-content>
|
||||||
|
|
||||||
<mat-menu #speakerMenu="matMenu">
|
<mat-menu #speakerMenu="matMenu">
|
||||||
|
@ -63,7 +63,7 @@ export class ListOfSpeakersComponent extends BaseViewComponentDirective implemen
|
|||||||
/**
|
/**
|
||||||
* filled by child component
|
* filled by child component
|
||||||
*/
|
*/
|
||||||
public hasFinishedSpeakers: boolean;
|
public canReaddLastSpeaker: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for speaker list component. Generates the forms.
|
* Constructor for speaker list component. Generates the forms.
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
"order": ["static-field", "static-method", "instance-field", "constructor", "instance-method"]
|
"order": ["static-field", "static-method", "instance-field", "constructor", "instance-method"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"no-unused-variable": true,
|
|
||||||
"typedef": [
|
"typedef": [
|
||||||
true,
|
true,
|
||||||
"call-signature",
|
"call-signature",
|
||||||
|
@ -136,6 +136,16 @@ def get_config_variables():
|
|||||||
validators=(MinValueValidator(-1),),
|
validators=(MinValueValidator(-1),),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
yield ConfigVariable(
|
||||||
|
name="agenda_enable_point_of_order_speakers",
|
||||||
|
default_value=False,
|
||||||
|
input_type="boolean",
|
||||||
|
label="Enables point of order speakers",
|
||||||
|
weight=223,
|
||||||
|
group="Agenda",
|
||||||
|
subgroup="List of speakers",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name="agenda_countdown_warning_time",
|
name="agenda_countdown_warning_time",
|
||||||
default_value=0,
|
default_value=0,
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.16 on 2020-10-13 11:53
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("agenda", "0009_item_tags"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="speaker",
|
||||||
|
name="point_of_order",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
@ -424,7 +424,7 @@ 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):
|
def add(self, user, list_of_speakers, skip_autoupdate=False, point_of_order=False):
|
||||||
"""
|
"""
|
||||||
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
|
||||||
@ -433,20 +433,39 @@ class SpeakerManager(models.Manager):
|
|||||||
if isinstance(user, AnonymousUser):
|
if isinstance(user, AnonymousUser):
|
||||||
raise OpenSlidesError("An anonymous user can not be on lists of speakers.")
|
raise OpenSlidesError("An anonymous user can not be on lists of speakers.")
|
||||||
|
|
||||||
|
if point_of_order and not config["agenda_enable_point_of_order_speakers"]:
|
||||||
|
raise OpenSlidesError("Point of order speakers are not enabled.")
|
||||||
|
|
||||||
if self.filter(
|
if self.filter(
|
||||||
user=user, list_of_speakers=list_of_speakers, begin_time=None
|
user=user,
|
||||||
|
list_of_speakers=list_of_speakers,
|
||||||
|
begin_time=None,
|
||||||
|
point_of_order=point_of_order,
|
||||||
).exists():
|
).exists():
|
||||||
raise OpenSlidesError(f"{user} is already on the list of speakers.")
|
raise OpenSlidesError(f"{user} is already on the list of speakers.")
|
||||||
if config["agenda_present_speakers_only"] and not user.is_present:
|
if config["agenda_present_speakers_only"] and not user.is_present:
|
||||||
raise OpenSlidesError("Only present users can be on the lists of speakers.")
|
raise OpenSlidesError("Only present users can be on the lists of speakers.")
|
||||||
|
|
||||||
|
if point_of_order:
|
||||||
|
weight = (
|
||||||
|
self.filter(list_of_speakers=list_of_speakers).aggregate(
|
||||||
|
models.Min("weight")
|
||||||
|
)["weight__min"]
|
||||||
|
or 0
|
||||||
|
) - 1
|
||||||
|
else:
|
||||||
weight = (
|
weight = (
|
||||||
self.filter(list_of_speakers=list_of_speakers).aggregate(
|
self.filter(list_of_speakers=list_of_speakers).aggregate(
|
||||||
models.Max("weight")
|
models.Max("weight")
|
||||||
)["weight__max"]
|
)["weight__max"]
|
||||||
or 0
|
or 0
|
||||||
)
|
) + 1
|
||||||
|
|
||||||
speaker = self.model(
|
speaker = self.model(
|
||||||
list_of_speakers=list_of_speakers, user=user, weight=weight + 1
|
list_of_speakers=list_of_speakers,
|
||||||
|
user=user,
|
||||||
|
weight=weight,
|
||||||
|
point_of_order=point_of_order,
|
||||||
)
|
)
|
||||||
speaker.save(
|
speaker.save(
|
||||||
force_insert=True,
|
force_insert=True,
|
||||||
@ -495,6 +514,11 @@ class Speaker(RESTModelMixin, models.Model):
|
|||||||
Marks a speaker.
|
Marks a speaker.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
point_of_order = models.BooleanField(default=False)
|
||||||
|
"""
|
||||||
|
Identifies the speaker as someone with a point of order
|
||||||
|
"""
|
||||||
|
|
||||||
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"),)
|
||||||
|
@ -10,7 +10,15 @@ class SpeakerSerializer(ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Speaker
|
model = Speaker
|
||||||
fields = ("id", "user", "begin_time", "end_time", "weight", "marked")
|
fields = (
|
||||||
|
"id",
|
||||||
|
"user",
|
||||||
|
"begin_time",
|
||||||
|
"end_time",
|
||||||
|
"weight",
|
||||||
|
"marked",
|
||||||
|
"point_of_order",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RelatedItemRelatedField(RelatedField):
|
class RelatedItemRelatedField(RelatedField):
|
||||||
|
@ -310,8 +310,10 @@ class ListOfSpeakersViewSet(
|
|||||||
def manage_speaker(self, request, pk=None):
|
def manage_speaker(self, request, pk=None):
|
||||||
"""
|
"""
|
||||||
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. Omit
|
them. Send POST {'user': <user_id>} to add a new speaker.
|
||||||
data to add yourself. Send DELETE {'speaker': <speaker_id>} or
|
Send POST {'user': <user_id>, 'point_of_order': True } to add a point
|
||||||
|
of 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
|
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.
|
Send PATCH {'user': <user_id>, 'marked': <bool>} to mark the speaker.
|
||||||
@ -329,16 +331,22 @@ class ListOfSpeakersViewSet(
|
|||||||
if request.method == "POST": # Add new speaker
|
if request.method == "POST": # Add new speaker
|
||||||
# Retrieve user_id
|
# Retrieve user_id
|
||||||
user_id = request.data.get("user")
|
user_id = request.data.get("user")
|
||||||
|
point_of_order = request.data.get("point_of_order") or False
|
||||||
|
if not isinstance(point_of_order, bool):
|
||||||
|
raise ValidationError({"detail": "point_of_order has to be a bool."})
|
||||||
|
|
||||||
# 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
|
||||||
if not has_perm(self.request.user, "agenda.can_be_speaker"):
|
if not point_of_order and not has_perm(
|
||||||
|
self.request.user, "agenda.can_be_speaker"
|
||||||
|
):
|
||||||
self.permission_denied(request)
|
self.permission_denied(request)
|
||||||
if list_of_speakers.closed:
|
if list_of_speakers.closed:
|
||||||
raise ValidationError({"detail": "The list of speakers is closed."})
|
raise ValidationError({"detail": "The list of speakers is closed."})
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
else:
|
else:
|
||||||
|
point_of_order = False # not for someone else
|
||||||
# Add someone else.
|
# Add someone else.
|
||||||
if not has_perm(
|
if not has_perm(
|
||||||
self.request.user, "agenda.can_manage_list_of_speakers"
|
self.request.user, "agenda.can_manage_list_of_speakers"
|
||||||
@ -352,7 +360,9 @@ class ListOfSpeakersViewSet(
|
|||||||
# Try to add the user. This ensurse that a user is not twice in the
|
# Try to add the user. This ensurse that a user is not twice in the
|
||||||
# list of coming speakers.
|
# list of coming speakers.
|
||||||
try:
|
try:
|
||||||
speaker = Speaker.objects.add(user, list_of_speakers)
|
speaker = Speaker.objects.add(
|
||||||
|
user, list_of_speakers, point_of_order=point_of_order
|
||||||
|
)
|
||||||
except OpenSlidesError as e:
|
except OpenSlidesError as e:
|
||||||
raise ValidationError({"detail": str(e)})
|
raise ValidationError({"detail": str(e)})
|
||||||
|
|
||||||
@ -360,7 +370,7 @@ class ListOfSpeakersViewSet(
|
|||||||
# 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)
|
||||||
|
|
||||||
# Toggle 'marked' for the speaker
|
# Set 'marked' for the speaker
|
||||||
elif request.method == "PATCH":
|
elif request.method == "PATCH":
|
||||||
# Check permissions
|
# Check permissions
|
||||||
if not has_perm(self.request.user, "agenda.can_manage_list_of_speakers"):
|
if not has_perm(self.request.user, "agenda.can_manage_list_of_speakers"):
|
||||||
@ -380,17 +390,12 @@ class ListOfSpeakersViewSet(
|
|||||||
queryset = Speaker.objects.filter(
|
queryset = Speaker.objects.filter(
|
||||||
list_of_speakers=list_of_speakers, user=user, begin_time=None
|
list_of_speakers=list_of_speakers, user=user, begin_time=None
|
||||||
)
|
)
|
||||||
try:
|
|
||||||
# We assume that there aren't multiple entries for speakers that
|
if not queryset.exists():
|
||||||
# did not yet begin to speak, because this
|
|
||||||
# is forbidden by the Manager's add method. We assume that
|
|
||||||
# there is only one speaker instance or none.
|
|
||||||
speaker = queryset.get()
|
|
||||||
except Speaker.DoesNotExist:
|
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{"detail": "The user is not in the list of speakers."}
|
{"detail": "The user is not in the list of speakers."}
|
||||||
)
|
)
|
||||||
else:
|
for speaker in queryset.all():
|
||||||
speaker.marked = marked
|
speaker.marked = marked
|
||||||
speaker.save()
|
speaker.save()
|
||||||
|
|
||||||
@ -400,21 +405,29 @@ class ListOfSpeakersViewSet(
|
|||||||
|
|
||||||
# Check permissions and other conditions. Get speaker instance.
|
# Check permissions and other conditions. Get speaker instance.
|
||||||
if speaker_ids is None:
|
if speaker_ids is None:
|
||||||
|
point_of_order = request.data.get("point_of_order") or False
|
||||||
|
if not isinstance(point_of_order, bool):
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "point_of_order has to be a bool."}
|
||||||
|
)
|
||||||
# Remove oneself
|
# Remove oneself
|
||||||
queryset = Speaker.objects.filter(
|
queryset = Speaker.objects.filter(
|
||||||
list_of_speakers=list_of_speakers, user=self.request.user
|
list_of_speakers=list_of_speakers,
|
||||||
|
user=self.request.user,
|
||||||
|
point_of_order=point_of_order,
|
||||||
).exclude(weight=None)
|
).exclude(weight=None)
|
||||||
try:
|
|
||||||
# We assume that there aren't multiple entries because this
|
if not queryset.exists():
|
||||||
# is forbidden by the Manager's add method. We assume that
|
|
||||||
# there is only one speaker instance or none.
|
|
||||||
speaker = queryset.get()
|
|
||||||
except Speaker.DoesNotExist:
|
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
{"detail": "You are not on the list of speakers."}
|
{"detail": "The user is not in the list of speakers."}
|
||||||
)
|
)
|
||||||
else:
|
# We delete all() from the queryset and do not use get():
|
||||||
speaker.delete()
|
# The Speaker.objects.add method should assert, that there
|
||||||
|
# is only one speaker. But due to race conditions, sometimes
|
||||||
|
# there are multiple ones. Using all() ensures, that there is
|
||||||
|
# no server crash, if this happens.
|
||||||
|
queryset.all().delete()
|
||||||
|
inform_changed_data(list_of_speakers)
|
||||||
else:
|
else:
|
||||||
# Remove someone else.
|
# Remove someone else.
|
||||||
if not has_perm(
|
if not has_perm(
|
||||||
@ -423,7 +436,7 @@ class ListOfSpeakersViewSet(
|
|||||||
self.permission_denied(request)
|
self.permission_denied(request)
|
||||||
if isinstance(speaker_ids, int):
|
if isinstance(speaker_ids, int):
|
||||||
speaker_ids = [speaker_ids]
|
speaker_ids = [speaker_ids]
|
||||||
deleted_speaker_count = 0
|
deleted_some_speakers = False
|
||||||
for speaker_id in speaker_ids:
|
for speaker_id in speaker_ids:
|
||||||
try:
|
try:
|
||||||
speaker = Speaker.objects.get(pk=int(speaker_id))
|
speaker = Speaker.objects.get(pk=int(speaker_id))
|
||||||
@ -431,9 +444,9 @@ class ListOfSpeakersViewSet(
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
speaker.delete(skip_autoupdate=True)
|
speaker.delete(skip_autoupdate=True)
|
||||||
deleted_speaker_count += 1
|
deleted_some_speakers = True
|
||||||
# send autoupdate if speakers are deleted
|
# send autoupdate if speakers are deleted
|
||||||
if deleted_speaker_count > 0:
|
if deleted_some_speakers:
|
||||||
inform_changed_data(list_of_speakers)
|
inform_changed_data(list_of_speakers)
|
||||||
|
|
||||||
return Response()
|
return Response()
|
||||||
@ -544,6 +557,16 @@ class ListOfSpeakersViewSet(
|
|||||||
if not last_speaker:
|
if not last_speaker:
|
||||||
raise ValidationError({"detail": "There is no last speaker at the moment."})
|
raise ValidationError({"detail": "There is no last speaker at the moment."})
|
||||||
|
|
||||||
|
if last_speaker.point_of_order:
|
||||||
|
raise ValidationError(
|
||||||
|
{"detail": "You cannot readd a point of order speaker."}
|
||||||
|
)
|
||||||
|
|
||||||
|
if list_of_speakers.speakers.filter(
|
||||||
|
user=last_speaker.user, begin_time=None
|
||||||
|
).exists():
|
||||||
|
raise ValidationError({"detail": "The last speaker is already waiting."})
|
||||||
|
|
||||||
next_speaker = list_of_speakers.get_next_speaker()
|
next_speaker = list_of_speakers.get_next_speaker()
|
||||||
new_weight = 1
|
new_weight = 1
|
||||||
# if there is a next speaker, insert last speaker before it
|
# if there is a next speaker, insert last speaker before it
|
||||||
|
@ -294,6 +294,7 @@ class ManageSpeaker(TestCase):
|
|||||||
title="test_title_aZaedij4gohn5eeQu8fe"
|
title="test_title_aZaedij4gohn5eeQu8fe"
|
||||||
).list_of_speakers
|
).list_of_speakers
|
||||||
self.user, _ = self.create_user()
|
self.user, _ = self.create_user()
|
||||||
|
self.admin = get_user_model().objects.get(username="admin")
|
||||||
|
|
||||||
def test_add_oneself_once(self):
|
def test_add_oneself_once(self):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@ -303,9 +304,7 @@ class ManageSpeaker(TestCase):
|
|||||||
self.assertTrue(Speaker.objects.all().exists())
|
self.assertTrue(Speaker.objects.all().exists())
|
||||||
|
|
||||||
def test_add_oneself_twice(self):
|
def test_add_oneself_twice(self):
|
||||||
Speaker.objects.add(
|
Speaker.objects.add(self.admin, self.list_of_speakers)
|
||||||
get_user_model().objects.get(username="admin"), self.list_of_speakers
|
|
||||||
)
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk])
|
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk])
|
||||||
)
|
)
|
||||||
@ -320,9 +319,19 @@ class ManageSpeaker(TestCase):
|
|||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
def test_remove_oneself(self):
|
def test_remove_oneself(self):
|
||||||
Speaker.objects.add(
|
Speaker.objects.add(self.admin, self.list_of_speakers)
|
||||||
get_user_model().objects.get(username="admin"), self.list_of_speakers
|
response = self.client.delete(
|
||||||
|
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk])
|
||||||
)
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertFalse(Speaker.objects.all().exists())
|
||||||
|
|
||||||
|
def test_remove_oneself_if_twice_on_los(self):
|
||||||
|
# This one is a test, if there is malformed speaker data, that
|
||||||
|
# this method still works.
|
||||||
|
Speaker.objects.add(self.admin, self.list_of_speakers)
|
||||||
|
s2 = Speaker(user=self.admin, list_of_speakers=self.list_of_speakers, weight=2)
|
||||||
|
s2.save()
|
||||||
response = self.client.delete(
|
response = self.client.delete(
|
||||||
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk])
|
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk])
|
||||||
)
|
)
|
||||||
@ -347,6 +356,94 @@ class ManageSpeaker(TestCase):
|
|||||||
).exists()
|
).exists()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_point_of_order_single(self):
|
||||||
|
config["agenda_enable_point_of_order_speakers"] = True
|
||||||
|
self.assertEqual(Speaker.objects.all().count(), 0)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk]),
|
||||||
|
{"point_of_order": True},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(Speaker.objects.all().count(), 1)
|
||||||
|
self.assertTrue(Speaker.objects.get().point_of_order)
|
||||||
|
self.assertEqual(Speaker.objects.get().weight, -1)
|
||||||
|
|
||||||
|
def test_point_of_order_not_enabled(self):
|
||||||
|
self.assertEqual(Speaker.objects.all().count(), 0)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk]),
|
||||||
|
{"point_of_order": True},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(Speaker.objects.all().count(), 0)
|
||||||
|
|
||||||
|
def test_point_of_order_before_other(self):
|
||||||
|
config["agenda_enable_point_of_order_speakers"] = True
|
||||||
|
normal_speaker = Speaker.objects.add(self.user, self.list_of_speakers)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk]),
|
||||||
|
{"point_of_order": True},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(Speaker.objects.all().count(), 2)
|
||||||
|
poo_speaker = Speaker.objects.get(user=self.admin)
|
||||||
|
self.assertTrue(poo_speaker.point_of_order)
|
||||||
|
self.assertEqual(poo_speaker.weight, normal_speaker.weight - 1)
|
||||||
|
|
||||||
|
def test_point_of_order_with_normal_speaker(self):
|
||||||
|
config["agenda_enable_point_of_order_speakers"] = True
|
||||||
|
Speaker.objects.add(self.admin, self.list_of_speakers)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk]),
|
||||||
|
{"point_of_order": True},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(Speaker.objects.all().count(), 2)
|
||||||
|
self.assertTrue(Speaker.objects.filter(user=self.admin).count(), 2)
|
||||||
|
|
||||||
|
def test_point_of_order_twice(self):
|
||||||
|
config["agenda_enable_point_of_order_speakers"] = True
|
||||||
|
Speaker.objects.add(self.admin, self.list_of_speakers, point_of_order=True)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk]),
|
||||||
|
{"point_of_order": True},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(Speaker.objects.all().count(), 1)
|
||||||
|
|
||||||
|
def test_remove_point_of_order(self):
|
||||||
|
config["agenda_enable_point_of_order_speakers"] = True
|
||||||
|
Speaker.objects.add(self.admin, self.list_of_speakers, point_of_order=True)
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk]),
|
||||||
|
{"point_of_order": True},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertFalse(Speaker.objects.all().exists())
|
||||||
|
|
||||||
|
def test_remove_point_of_order_with_normal_speaker(self):
|
||||||
|
config["agenda_enable_point_of_order_speakers"] = True
|
||||||
|
Speaker.objects.add(self.admin, self.list_of_speakers)
|
||||||
|
Speaker.objects.add(self.admin, self.list_of_speakers, point_of_order=True)
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk]),
|
||||||
|
{"point_of_order": True},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(Speaker.objects.all().count(), 1)
|
||||||
|
self.assertFalse(Speaker.objects.get().point_of_order)
|
||||||
|
|
||||||
|
def test_remove_self_with_point_of_order(self):
|
||||||
|
config["agenda_enable_point_of_order_speakers"] = True
|
||||||
|
Speaker.objects.add(self.admin, self.list_of_speakers)
|
||||||
|
Speaker.objects.add(self.admin, self.list_of_speakers, point_of_order=True)
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk])
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(Speaker.objects.all().count(), 1)
|
||||||
|
self.assertTrue(Speaker.objects.get().point_of_order)
|
||||||
|
|
||||||
def test_invalid_data_string_instead_of_integer(self):
|
def test_invalid_data_string_instead_of_integer(self):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk]),
|
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk]),
|
||||||
@ -447,6 +544,7 @@ class ManageSpeaker(TestCase):
|
|||||||
speaker.end_time = timezone.now()
|
speaker.end_time = timezone.now()
|
||||||
speaker.weight = None
|
speaker.weight = None
|
||||||
speaker.save()
|
speaker.save()
|
||||||
|
return speaker
|
||||||
|
|
||||||
def test_readd_last_speaker_no_speaker(self):
|
def test_readd_last_speaker_no_speaker(self):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@ -514,6 +612,29 @@ class ManageSpeaker(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_readd_last_speaker_already_waiting(self):
|
||||||
|
self.util_add_user_as_last_speaker()
|
||||||
|
Speaker.objects.add(self.user, self.list_of_speakers)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"listofspeakers-readd-last-speaker", args=[self.list_of_speakers.pk]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
def test_readd_last_speaker_point_of_order(self):
|
||||||
|
speaker = self.util_add_user_as_last_speaker()
|
||||||
|
speaker.point_of_order = True
|
||||||
|
speaker.save()
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
"listofspeakers-readd-last-speaker", args=[self.list_of_speakers.pk]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
|
||||||
class Speak(TestCase):
|
class Speak(TestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -32,7 +32,7 @@ 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
|
self.request.user, self.mock_list_of_speakers, point_of_order=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("openslides.agenda.views.inform_changed_data")
|
@patch("openslides.agenda.views.inform_changed_data")
|
||||||
@ -56,7 +56,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
|
mock_user, self.mock_list_of_speakers, point_of_order=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch("openslides.agenda.views.Speaker")
|
@patch("openslides.agenda.views.Speaker")
|
||||||
@ -66,7 +66,7 @@ class ListOfSpeakersViewSetManageSpeaker(TestCase):
|
|||||||
self.request.data = {}
|
self.request.data = {}
|
||||||
self.view_instance.manage_speaker(self.request)
|
self.view_instance.manage_speaker(self.request)
|
||||||
mock_queryset = mock_speaker.objects.filter.return_value.exclude.return_value
|
mock_queryset = mock_speaker.objects.filter.return_value.exclude.return_value
|
||||||
mock_queryset.get.return_value.delete.assert_called_with()
|
mock_queryset.all.return_value.delete.assert_called_with()
|
||||||
|
|
||||||
@patch("openslides.agenda.views.inform_changed_data")
|
@patch("openslides.agenda.views.inform_changed_data")
|
||||||
@patch("openslides.agenda.views.has_perm")
|
@patch("openslides.agenda.views.has_perm")
|
||||||
|
Loading…
Reference in New Issue
Block a user