Merge pull request #4659 from MaximilianKrambach/presentSpeakers
Filter present speaker search-value selector
This commit is contained in:
commit
318533cf68
@ -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>
|
||||||
|
@ -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 */
|
||||||
|
this.userRepository.getViewModelListObservable().subscribe(users => {
|
||||||
|
this.users.next(users);
|
||||||
|
this.filterUsers();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.subscriptions.push(
|
||||||
// detect changes in the form
|
// detect changes in the form
|
||||||
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.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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
)
|
||||||
|
@ -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")
|
||||||
|
Loading…
Reference in New Issue
Block a user