Merge pull request #4143 from MaximilianKrambach/callList

Speaker list opening/closing/clearing
This commit is contained in:
Emanuel Schütze 2019-01-19 22:05:45 +01:00 committed by GitHub
commit a7eaecfa14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 128 additions and 21 deletions

View File

@ -1,9 +1,12 @@
<os-head-bar [nav]="false" [goBack]="true"> <os-head-bar [nav]="false" [goBack]="true">
<!-- Title --> <!-- Title -->
<div class="title-slot"> <div class="title-slot">
<h2> <h2><span translate>List of speakers</span></h2>
<span translate>List of speakers</span> </div>
</h2> <div class="menu-slot" *osPerms="'agenda.can_manage_list_of_speakers'">
<button type="button" mat-icon-button [matMenuTriggerFor]="speakerMenu">
<mat-icon>more_vert</mat-icon>
</button>
</div> </div>
</os-head-bar> </os-head-bar>
@ -27,8 +30,12 @@
<!-- <span translate>minutes</span> --> <!-- <span translate>minutes</span> -->
<span>&nbsp;(</span> <span translate>Start time</span> <span>:&nbsp;{{ speaker.begin_time }})</span> <span>&nbsp;(</span> <span translate>Start time</span> <span>:&nbsp;{{ speaker.begin_time }})</span>
</div> </div>
<button mat-stroked-button matTooltip="{{ 'Remove' | translate }}" <button
*osPerms="'agenda.can_manage_list_of_speakers'" (click)="onDeleteButton(speaker)"> mat-stroked-button
matTooltip="{{ 'Remove' | translate }}"
*osPerms="'agenda.can_manage_list_of_speakers'"
(click)="onDeleteButton(speaker)"
>
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
</button> </button>
</mat-list-item> </mat-list-item>
@ -40,30 +47,34 @@
<mat-icon class="speaking-icon">play_arrow</mat-icon> <mat-icon class="speaking-icon">play_arrow</mat-icon>
<span class="speaking-name">{{ activeSpeaker }}</span> <span class="speaking-name">{{ activeSpeaker }}</span>
<button mat-stroked-button matTooltip="{{ 'End speech' | translate }}" <button
*osPerms="'agenda.can_manage_list_of_speakers'" (click)="onStopButton()"> mat-stroked-button
matTooltip="{{ 'End speech' | translate }}"
*osPerms="'agenda.can_manage_list_of_speakers'"
(click)="onStopButton()"
>
<mat-icon>mic_off</mat-icon> <mat-icon>mic_off</mat-icon>
<span translate>Stop</span> <span translate>Stop</span>
</button> </button>
</div> </div>
<!-- Waiting speakers --> <!-- Waiting speakers -->
<div *osPerms="'agenda.can_manage_list_of_speakers'"> <div>
<div class="waiting-list" *ngIf="speakers && speakers.length > 0"> <div class="waiting-list" *ngIf="speakers && speakers.length > 0">
<os-sorting-list [input]="speakers" [live]="true" [count]="true" (sortEvent)="onSortingChange($event)"> <os-sorting-list [input]="speakers" [live]="true" [count]="true" (sortEvent)="onSortingChange($event)">
<!-- implicit item references into the component using ng-template slot --> <!-- implicit item references into the component using ng-template slot -->
<ng-template let-item> <ng-template let-item>
<span *ngIf="hasSpokenCount(item)" class="red-warning-text speaker-warning"> <span *osPerms="'agenda.can_manage_list_of_speakers'">
<span translate>Call</span><span>&nbsp;{{ hasSpokenCount(item) + 1 }}</span> <span *ngIf="hasSpokenCount(item)" class="red-warning-text speaker-warning">
<span translate>Call</span><span>&nbsp;{{ hasSpokenCount(item) + 1 }}</span>
</span>
</span> </span>
<mat-button-toggle-group> <mat-button-toggle-group *osPerms="'agenda.can_manage_list_of_speakers'">
<mat-button-toggle matTooltip="{{ 'Begin speech' | translate }}" <mat-button-toggle matTooltip="{{ 'Begin speech' | translate }}" (click)="onStartButton(item)">
(click)="onStartButton(item)">
<mat-icon>mic</mat-icon> <mat-icon>mic</mat-icon>
<span translate>Start</span> <span translate>Start</span>
</mat-button-toggle> </mat-button-toggle>
<mat-button-toggle matTooltip="{{ 'Mark speaker' | translate }}" <mat-button-toggle matTooltip="{{ 'Mark speaker' | translate }}" (click)="onMarkButton(item)">
(click)="onMarkButton(item)">
<mat-icon>{{ item.marked ? 'star' : 'star_border' }}</mat-icon> <mat-icon>{{ item.marked ? 'star' : 'star_border' }}</mat-icon>
</mat-button-toggle> </mat-button-toggle>
<mat-button-toggle matTooltip="{{ 'Remove' | translate }}" (click)="onDeleteButton(item)"> <mat-button-toggle matTooltip="{{ 'Remove' | translate }}" (click)="onDeleteButton(item)">
@ -92,12 +103,11 @@
<!-- 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 *osPerms="'agenda.can_be_speaker'" class="add-self-buttons">
<div *ngIf="speakers"> <div *ngIf="speakers && !closedList">
<button mat-raised-button (click)="addNewSpeaker()" *ngIf="!isOpInList()"> <button mat-raised-button (click)="addNewSpeaker()" *ngIf="!isOpInList()">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
<span translate>Add me</span> <span translate>Add me</span>
</button> </button>
<button mat-raised-button (click)="onDeleteButton()" *ngIf="isOpInList()"> <button mat-raised-button (click)="onDeleteButton()" *ngIf="isOpInList()">
<mat-icon>remove</mat-icon> <mat-icon>remove</mat-icon>
<span translate>Remove me</span> <span translate>Remove me</span>
@ -105,3 +115,23 @@
</div> </div>
</div> </div>
</mat-card> </mat-card>
<mat-menu #speakerMenu="matMenu">
<button mat-menu-item *ngIf="closedList" (click)="openSpeakerList()">
<mat-icon>mic</mat-icon>
<span translate>Open list of speakers</span>
</button>
<button mat-menu-item *ngIf="!closedList" (click)="closeSpeakerList()">
<mat-icon>mic_off</mat-icon>
<span translate>Close list of speakers</span>
</button>
<mat-divider *ngIf="!emptyList"></mat-divider>
<button mat-menu-item (click)="clearSpeakerList()" *ngIf="!emptyList" class="red-warning-text">
<mat-icon>delete</mat-icon>
<span trabslate>Remove all speakers</span>
</button>
</mat-menu>

View File

@ -14,6 +14,7 @@ import { BaseViewComponent } from 'app/site/base/base-view';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar } from '@angular/material';
import { PromptService } from 'app/core/services/prompt.service';
/** /**
* The list of speakers for agenda items. * The list of speakers for agenda items.
@ -54,6 +55,22 @@ export class SpeakerListComponent extends BaseViewComponent implements OnInit {
*/ */
public addSpeakerForm: FormGroup; public addSpeakerForm: FormGroup;
/**
* @returns true if the items' speaker list is currently not open
*/
public get closedList(): boolean {
return this.viewItem && this.viewItem.item.speaker_list_closed;
}
public get emptyList(): boolean {
if (this.speakers && this.speakers.length) {
return false;
} else if (this.finishedSpeakers && this.finishedSpeakers.length) {
return false;
}
return this.activeSpeaker ? false : true;
}
/** /**
* Constructor for speaker list component * Constructor for speaker list component
* @param title * @param title
@ -71,7 +88,8 @@ export class SpeakerListComponent extends BaseViewComponent implements OnInit {
private route: ActivatedRoute, private route: ActivatedRoute,
private DS: DataStoreService, private DS: DataStoreService,
private itemRepo: AgendaRepositoryService, private itemRepo: AgendaRepositoryService,
private op: OperatorService private op: OperatorService,
private promptService: PromptService
) { ) {
super(title, translate, snackBar); super(title, translate, snackBar);
this.addSpeakerForm = new FormGroup({ user_id: new FormControl([]) }); this.addSpeakerForm = new FormGroup({ user_id: new FormControl([]) });
@ -117,7 +135,8 @@ export class SpeakerListComponent extends BaseViewComponent implements OnInit {
this.speakers = allSpeakers.filter(speaker => speaker.state === SpeakerState.WAITING); this.speakers = allSpeakers.filter(speaker => speaker.state === SpeakerState.WAITING);
this.finishedSpeakers = allSpeakers.filter(speaker => speaker.state === SpeakerState.FINISHED); this.finishedSpeakers = allSpeakers.filter(speaker => speaker.state === SpeakerState.FINISHED);
this.activeSpeaker = allSpeakers.find(speaker => speaker.state === SpeakerState.CURRENT); const currentSpeaker = allSpeakers.find(speaker => speaker.state === SpeakerState.CURRENT);
this.activeSpeaker = currentSpeaker ? currentSpeaker : null;
} }
}); });
} }
@ -190,4 +209,41 @@ export class SpeakerListComponent extends BaseViewComponent implements OnInit {
public hasSpokenCount(speaker: ViewSpeaker): number { public hasSpokenCount(speaker: ViewSpeaker): number {
return this.finishedSpeakers.filter(finishedSpeaker => finishedSpeaker.user.id === speaker.user.id).length; return this.finishedSpeakers.filter(finishedSpeaker => finishedSpeaker.user.id === speaker.user.id).length;
} }
/**
* Closes the current speaker list
*/
public closeSpeakerList(): Promise<void> {
if (!this.viewItem.item.speaker_list_closed) {
return this.itemRepo.update({ speaker_list_closed: true }, this.viewItem);
}
}
/**
* Opens the speaker list for the current item
*/
public openSpeakerList(): Promise<void> {
if (this.viewItem.item.speaker_list_closed) {
return this.itemRepo.update({ speaker_list_closed: false }, this.viewItem);
}
}
/**
* Clears the speaker list by removing all current, past and future speakers
* after a confirmation dialog
*/
public async clearSpeakerList(): Promise<void> {
const content = this.translate.instant('This will clear all speakers from the list.');
if (await this.promptService.open('Are you sure?', content)) {
this.speakers.forEach(speaker => {
this.itemRepo.deleteSpeaker(this.viewItem.item, speaker.id);
});
this.finishedSpeakers.forEach(speaker => {
this.itemRepo.deleteSpeaker(this.viewItem.item, speaker.id);
});
if (this.activeSpeaker) {
this.itemRepo.deleteSpeaker(this.viewItem.item, this.activeSpeaker.id);
}
}
}
} }

View File

@ -125,6 +125,26 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
await this.httpService.delete(restUrl); await this.httpService.delete(restUrl);
} }
/**
* Stops the current speaker
*
* @param agenda the target agenda item
*/
public async closeSpeakerList(agenda: Item): Promise<void> {
const restUrl = `rest/agenda/item/${agenda.id}/speak/`;
await this.httpService.delete(restUrl);
}
/**
* Stops the current speaker
*
* @param agenda the target agenda item
*/
public async openSpeakerList(agenda: Item): Promise<void> {
const restUrl = `rest/agenda/item/${agenda.id}/speak/`;
await this.httpService.delete(restUrl);
}
/** /**
* Marks the current speaker * Marks the current speaker
* *

View File

@ -159,9 +159,10 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
if not isinstance(marked, bool): if not isinstance(marked, bool):
raise ValidationError({"detail": "Marked has to be a bool."}) raise ValidationError({"detail": "Marked has to be a bool."})
queryset = Speaker.objects.filter(item=item, user=user) queryset = Speaker.objects.filter(item=item, user=user, begin_time=None)
try: try:
# We assume that there aren't multiple entries because this # We assume that there aren't multiple entries for speakers that
# did not yet begin to speak, because this
# is forbidden by the Manager's add method. We assume that # is forbidden by the Manager's add method. We assume that
# there is only one speaker instance or none. # there is only one speaker instance or none.
speaker = queryset.get() speaker = queryset.get()