filter potential speakers

- filter by those already on list
- filter by present, if configured
This commit is contained in:
Maximilian Krambach 2019-05-06 12:28:28 +02:00
parent 1599e91fa5
commit 16477a4e92
5 changed files with 103 additions and 32 deletions

View File

@ -109,7 +109,7 @@
<!-- Search for speakers --> <!-- Search for speakers -->
<div *osPerms="'agenda.can_manage_list_of_speakers'"> <div *osPerms="'agenda.can_manage_list_of_speakers'">
<form *ngIf="users && users.value.length > 0" [formGroup]="addSpeakerForm"> <form *ngIf="filteredUsers && filteredUsers.value.length > 0" [formGroup]="addSpeakerForm">
<os-search-value-selector <os-search-value-selector
class="search-users" class="search-users"
ngDefaultControl ngDefaultControl
@ -117,7 +117,7 @@
[formControl]="addSpeakerForm.get('user_id')" [formControl]="addSpeakerForm.get('user_id')"
[multiple]="false" [multiple]="false"
listname="{{ 'Select or search new speaker ...' | translate }}" listname="{{ 'Select or search new speaker ...' | translate }}"
[InputListValues]="users" [InputListValues]="filteredUsers"
></os-search-value-selector> ></os-search-value-selector>
</form> </form>
</div> </div>
@ -125,7 +125,7 @@
<!-- 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 && !closedList"> <div *ngIf="speakers && !closedList">
<button mat-stroked-button (click)="addNewSpeaker()" *ngIf="!isOpInList()"> <button mat-stroked-button (click)="addNewSpeaker()" *ngIf="!isOpInList() && canAddSelf">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
<span translate>Add me</span> <span translate>Add me</span>
</button> </button>

View File

@ -8,18 +8,20 @@ import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Subscription } from 'rxjs'; import { BehaviorSubject, Subscription } from 'rxjs';
import { BaseViewComponent } from 'app/site/base/base-view'; import { BaseViewComponent } from 'app/site/base/base-view';
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { CurrentListOfSpeakersService } from 'app/site/projector/services/current-agenda-item.service';
import { CurrentListOfSpeakersSlideService } from 'app/site/projector/services/current-list-of-of-speakers-slide.service';
import { DurationService } from 'app/core/ui-services/duration.service';
import { OperatorService } from 'app/core/core-services/operator.service'; import { OperatorService } from 'app/core/core-services/operator.service';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { ProjectorRepositoryService } from 'app/core/repositories/projector/projector-repository.service'; import { ProjectorRepositoryService } from 'app/core/repositories/projector/projector-repository.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
import { ViewSpeaker, SpeakerState } from '../../models/view-speaker'; import { ViewSpeaker, SpeakerState } from '../../models/view-speaker';
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
import { ViewProjector } from 'app/site/projector/models/view-projector'; import { ViewProjector } from 'app/site/projector/models/view-projector';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
import { DurationService } from 'app/core/ui-services/duration.service';
import { CollectionStringMapperService } from 'app/core/core-services/collection-string-mapper.service';
import { CurrentListOfSpeakersService } from 'app/site/projector/services/current-agenda-item.service';
import { CurrentListOfSpeakersSlideService } from 'app/site/projector/services/current-list-of-of-speakers-slide.service';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { ListOfSpeakersRepositoryService } from 'app/core/repositories/agenda/list-of-speakers-repository.service'; import { ListOfSpeakersRepositoryService } from 'app/core/repositories/agenda/list-of-speakers-repository.service';
import { ViewListOfSpeakers } from '../../models/view-list-of-speakers'; import { ViewListOfSpeakers } from '../../models/view-list-of-speakers';
@ -70,7 +72,12 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
/** /**
* Hold the users * Hold the users
*/ */
public users: BehaviorSubject<ViewUser[]>; public users = new BehaviorSubject<ViewUser[]>([]);
/**
* A filtered list of users, excluding those not available to be added to the list
*/
public filteredUsers = new BehaviorSubject<ViewUser[]>([]);
/** /**
* Required for the user search selector * Required for the user search selector
@ -93,6 +100,13 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
return !this.activeSpeaker; return !this.activeSpeaker;
} }
/**
* @returns true if the current user can be added to the list of speakers
*/
public get canAddSelf(): boolean {
return !this.config.instant('agenda_present_speakers_only') || this.operator.user.is_present;
}
/** /**
* Used to detect changes in the projector reference. * Used to detect changes in the projector reference.
*/ */
@ -128,7 +142,8 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
private durationService: DurationService, private durationService: DurationService,
private userRepository: UserRepositoryService, private userRepository: UserRepositoryService,
private collectionStringMapper: CollectionStringMapperService, private collectionStringMapper: CollectionStringMapperService,
private currentListOfSpeakersSlideService: CurrentListOfSpeakersSlideService private currentListOfSpeakersSlideService: CurrentListOfSpeakersSlideService,
private config: ConfigService
) { ) {
super(title, translate, snackBar); super(title, translate, snackBar);
this.addSpeakerForm = new FormGroup({ user_id: new FormControl([]) }); this.addSpeakerForm = new FormGroup({ user_id: new FormControl([]) });
@ -160,15 +175,27 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
} }
// load and observe users // load and observe users
this.users = this.userRepository.getViewModelListBehaviorSubject(); this.subscriptions.push(
/* List of eligible users */
// detect changes in the form this.userRepository.getViewModelListObservable().subscribe(users => {
this.addSpeakerForm.valueChanges.subscribe(formResult => { this.users.next(users);
// resetting a form triggers a form.next(null) - check if user_id this.filterUsers();
if (formResult && formResult.user_id) { })
this.addNewSpeaker(formResult.user_id); );
} this.subscriptions.push(
}); // detect changes in the form
this.addSpeakerForm.valueChanges.subscribe(formResult => {
// resetting a form triggers a form.next(null) - check if user_id
if (formResult && formResult.user_id) {
this.addNewSpeaker(formResult.user_id);
}
})
);
this.subscriptions.push(
this.config.get('agenda_present_speakers_only').subscribe(() => {
this.filterUsers();
})
);
} }
public opCanManage(): boolean { public opCanManage(): boolean {
@ -224,6 +251,9 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
this.viewListOfSpeakers = listOfSpeakers; this.viewListOfSpeakers = listOfSpeakers;
const allSpeakers = this.viewListOfSpeakers.speakers.sort((a, b) => a.weight - b.weight); const allSpeakers = this.viewListOfSpeakers.speakers.sort((a, b) => a.weight - b.weight);
this.speakers = allSpeakers.filter(speaker => speaker.state === SpeakerState.WAITING); this.speakers = allSpeakers.filter(speaker => speaker.state === SpeakerState.WAITING);
// Since the speaker repository is not a normal repository, sorting cannot be handled there
this.speakers.sort((a: ViewSpeaker, b: ViewSpeaker) => a.weight - b.weight);
this.filterUsers();
this.finishedSpeakers = allSpeakers.filter(speaker => speaker.state === SpeakerState.FINISHED); this.finishedSpeakers = allSpeakers.filter(speaker => speaker.state === SpeakerState.FINISHED);
// convert begin time to date and sort // convert begin time to date and sort
@ -276,15 +306,25 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
* *
* @param speaker the speaker marked in the list * @param speaker the speaker marked in the list
*/ */
public onStartButton(speaker: ViewSpeaker): void { public async onStartButton(speaker: ViewSpeaker): Promise<void> {
this.listOfSpeakersRepo.startSpeaker(this.viewListOfSpeakers, speaker).then(null, this.raiseError); try {
await this.listOfSpeakersRepo.startSpeaker(this.viewListOfSpeakers, speaker);
this.filterUsers();
} catch (e) {
this.raiseError(e);
}
} }
/** /**
* Click on the mic-cross button * Click on the mic-cross button
*/ */
public onStopButton(): void { public async onStopButton(): Promise<void> {
this.listOfSpeakersRepo.stopCurrentSpeaker(this.viewListOfSpeakers).then(null, this.raiseError); try {
await this.listOfSpeakersRepo.stopCurrentSpeaker(this.viewListOfSpeakers);
this.filterUsers();
} catch (e) {
this.raiseError(e);
}
} }
/** /**
@ -299,14 +339,18 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
} }
/** /**
* Click on the X button * Click on the X button - removes the speaker from the list of speakers
* *
* @param speaker * @param speaker optional speaker to remove. If none is given,
* the operator themself is removed
*/ */
public onDeleteButton(speaker?: ViewSpeaker): void { public async onDeleteButton(speaker?: ViewSpeaker): Promise<void> {
this.listOfSpeakersRepo try {
.delete(this.viewListOfSpeakers, speaker ? speaker.id : null) await this.listOfSpeakersRepo.delete(this.viewListOfSpeakers, speaker ? speaker.id : null);
.then(null, this.raiseError); this.filterUsers();
} catch (e) {
this.raiseError(e);
}
} }
/** /**
@ -385,4 +429,18 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
); );
return `${this.durationService.durationToString(duration, 'm')}`; return `${this.durationService.durationToString(duration, 'm')}`;
} }
/**
* Triggers an update of the filter for the list of available potential speakers
* (triggered on an update of users or config)
*/
private filterUsers(): void {
const presentUsersOnly = this.config.instant('agenda_present_speakers_only');
const users = presentUsersOnly ? this.users.getValue().filter(u => u.is_present) : this.users.getValue();
if (!this.speakers || !this.speakers.length) {
this.filteredUsers.next(users);
} else {
this.filteredUsers.next(users.filter(u => !this.speakers.some(speaker => speaker.user.id === u.id)));
}
}
} }

View File

@ -47,14 +47,14 @@ export class ViewSpeaker implements Updateable, Identifiable {
/** /**
* @returns an ISO datetime string or null * @returns an ISO datetime string or null
*/ */
public get begin_time(): string { public get begin_time(): string | null {
return this.speaker.begin_time; return this.speaker.begin_time;
} }
/** /**
* @returns an ISO datetime string or null * @returns an ISO datetime string or null
*/ */
public get end_time(): string { public get end_time(): string | null {
return this.speaker.end_time; return this.speaker.end_time;
} }

View File

@ -115,3 +115,14 @@ def get_config_variables():
group="Agenda", group="Agenda",
subgroup="List of speakers", subgroup="List of speakers",
) )
yield ConfigVariable(
name="agenda_present_speakers_only",
default_value=False,
input_type="boolean",
label="Only present users can be on the list of speakers",
help_text="Users without the status 'present' will not be available for any list of speakers.",
weight=250,
group="Agenda",
subgroup="List of speakers",
)

View File

@ -422,6 +422,8 @@ class SpeakerManager(models.Manager):
raise OpenSlidesError(f"{user} is already on the list of speakers.") raise OpenSlidesError(f"{user} is already on the list of speakers.")
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 config["agenda_present_speakers_only"] and not user.is_present:
raise OpenSlidesError("Only present users can be on the lists of speakers.")
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")