Adds sort mode, delete confirmation and re-add speaker button to list of
speakers
This commit is contained in:
parent
f9cea53659
commit
943e8f22d3
@ -168,6 +168,16 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit
|
||||
await this.httpService.post(restUrl, { speakers: speakerIds });
|
||||
}
|
||||
|
||||
/**
|
||||
* Readds the last speaker to the list of speakers
|
||||
*
|
||||
* @param listOfSpeakers the list of speakers to modify
|
||||
*/
|
||||
public async readdLastSpeaker(listOfSpeakers: ViewListOfSpeakers): Promise<void> {
|
||||
const restUrl = this.getRestUrl(listOfSpeakers.id, 'readd_last_speaker');
|
||||
await this.httpService.post(restUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks all speakers for a given user
|
||||
*
|
||||
@ -207,7 +217,10 @@ export class ListOfSpeakersRepositoryService extends BaseHasContentObjectReposit
|
||||
* @param listOfSpeakersId id of the list of speakers
|
||||
* @param method the desired speaker action
|
||||
*/
|
||||
private getRestUrl(listOfSpeakersId: number, method: 'manage_speaker' | 'sort_speakers' | 'speak'): string {
|
||||
private getRestUrl(
|
||||
listOfSpeakersId: number,
|
||||
method: 'manage_speaker' | 'sort_speakers' | 'speak' | 'readd_last_speaker'
|
||||
): string {
|
||||
return `/rest/agenda/list-of-speakers/${listOfSpeakersId}/${method}/`;
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
<div cdkDropList [cdkDropListDisabled]="!enable" (cdkDropListDropped)="drop($event)">
|
||||
<div class="line" *ngIf="!array.length">
|
||||
<div class="line" *ngIf="!sortedItems.length">
|
||||
<span translate>No data</span>
|
||||
</div>
|
||||
<div
|
||||
[ngClass]="isSelectedRow(i) ? 'backgroundColorSelected line' : 'backgroundColorLight line'"
|
||||
*ngFor="let item of array; let i = index"
|
||||
*ngFor="let item of sortedItems; let i = index"
|
||||
cdkDrag
|
||||
(click)="onItemClick($event, i)"
|
||||
(cdkDragStarted)="dragStarted(i)"
|
||||
|
@ -34,7 +34,7 @@ export class SortingListComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Sorted and returned
|
||||
*/
|
||||
public array: Selectable[];
|
||||
public sortedItems: Selectable[];
|
||||
|
||||
/**
|
||||
* The index of multiple selected elements. Allows for multiple items to be
|
||||
@ -104,6 +104,11 @@ export class SortingListComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
private inputSubscription: Subscription | null;
|
||||
|
||||
/**
|
||||
* Always stores the current items from the last update. Needed for restore and changing between live=true/false
|
||||
*/
|
||||
private currentItems: Selectable[];
|
||||
|
||||
/**
|
||||
* Inform the parent view about sorting.
|
||||
* Alternative approach to submit a new order of elements
|
||||
@ -118,7 +123,7 @@ export class SortingListComponent implements OnInit, OnDestroy {
|
||||
* @param translate the translation service
|
||||
*/
|
||||
public constructor(protected translate: TranslateService) {
|
||||
this.array = [];
|
||||
this.sortedItems = [];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -141,14 +146,21 @@ export class SortingListComponent implements OnInit, OnDestroy {
|
||||
* @param newValues The new values to set.
|
||||
*/
|
||||
private updateArray(newValues: Selectable[]): void {
|
||||
if (this.array.length !== newValues.length || this.live) {
|
||||
this.array = [];
|
||||
this.array = newValues.map(val => val);
|
||||
this.currentItems = newValues.map(val => val);
|
||||
if (this.sortedItems.length !== newValues.length || this.live) {
|
||||
this.sortedItems = newValues.map(val => val);
|
||||
} else {
|
||||
this.array = this.array.map(arrayValue => newValues.find(val => val.id === arrayValue.id));
|
||||
this.sortedItems = this.sortedItems.map(arrayValue => newValues.find(val => val.id === arrayValue.id));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the old order from the last update
|
||||
*/
|
||||
public restore(): void {
|
||||
this.sortedItems = this.currentItems.map(val => val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the start of a dragDrop event and clears multiSelect if the ittem dragged
|
||||
* is not part of the selected items
|
||||
@ -171,35 +183,35 @@ export class SortingListComponent implements OnInit, OnDestroy {
|
||||
dropBehind?: boolean
|
||||
): void {
|
||||
if (!this.multiSelectedIndex.length) {
|
||||
moveItemInArray(this.array, event.previousIndex, event.currentIndex);
|
||||
moveItemInArray(this.sortedItems, event.previousIndex, event.currentIndex);
|
||||
} else {
|
||||
const before: Selectable[] = [];
|
||||
const insertions: Selectable[] = [];
|
||||
const behind: Selectable[] = [];
|
||||
for (let i = 0; i < this.array.length; i++) {
|
||||
for (let i = 0; i < this.sortedItems.length; i++) {
|
||||
if (!this.multiSelectedIndex.includes(i)) {
|
||||
if (i < event.currentIndex) {
|
||||
before.push(this.array[i]);
|
||||
before.push(this.sortedItems[i]);
|
||||
} else if (i > event.currentIndex) {
|
||||
behind.push(this.array[i]);
|
||||
behind.push(this.sortedItems[i]);
|
||||
} else {
|
||||
if (dropBehind === false) {
|
||||
behind.push(this.array[i]);
|
||||
behind.push(this.sortedItems[i]);
|
||||
} else if (dropBehind === true) {
|
||||
before.push(this.array[i]);
|
||||
before.push(this.sortedItems[i]);
|
||||
} else {
|
||||
Math.min(...this.multiSelectedIndex) < i
|
||||
? before.push(this.array[i])
|
||||
: behind.push(this.array[i]);
|
||||
? before.push(this.sortedItems[i])
|
||||
: behind.push(this.sortedItems[i]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
insertions.push(this.array[i]);
|
||||
insertions.push(this.sortedItems[i]);
|
||||
}
|
||||
}
|
||||
this.array = [...before, ...insertions, ...behind];
|
||||
this.sortedItems = [...before, ...insertions, ...behind];
|
||||
}
|
||||
this.sortEvent.emit(this.array);
|
||||
this.sortEvent.emit(this.sortedItems);
|
||||
this.multiSelectedIndex = [];
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,14 @@
|
||||
<os-head-bar [nav]="false" [goBack]="true">
|
||||
<os-head-bar [nav]="false" [goBack]="true" [editMode]="isSortMode" (cancelEditEvent)="onCancelSorting()" (saveEvent)="onSaveSorting()">
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2>
|
||||
<span *ngIf="!isCurrentListOfSpeakers" translate>List of speakers</span>
|
||||
<span *ngIf="isCurrentListOfSpeakers" translate>Current list of speakers</span>
|
||||
<span *ngIf="!isCurrentListOfSpeakers && !isSortMode" translate>List of speakers</span>
|
||||
<span *ngIf="isCurrentListOfSpeakers && !isSortMode" translate>Current list of speakers</span>
|
||||
<span *ngIf="isSortMode" translate>Sort mode</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="menu-slot" *osPerms="['agenda.can_manage_list_of_speakers', 'core.can_manage_projector']">
|
||||
<button type="button" mat-icon-button matTooltip="{{ 'Re-add last speaker' | translate }}" (click)="readdLastSpeaker()" [disabled]="!finishedSpeakers || !finishedSpeakers.length"><mat-icon>undo</mat-icon></button>
|
||||
<button type="button" mat-icon-button [matMenuTriggerFor]="speakerMenu"><mat-icon>more_vert</mat-icon></button>
|
||||
</div>
|
||||
</os-head-bar>
|
||||
@ -77,10 +79,9 @@
|
||||
<div class="waiting-list" *ngIf="speakers && speakers.length > 0">
|
||||
<os-sorting-list
|
||||
[input]="speakers"
|
||||
[live]="true"
|
||||
[live]="!isSortMode"
|
||||
[count]="true"
|
||||
[enable]="opCanManage()"
|
||||
(sortEvent)="onSortingChange($event)"
|
||||
[enable]="opCanManage() && isSortMode"
|
||||
>
|
||||
<!-- implicit speaker references into the component using ng-template slot -->
|
||||
<ng-template let-speaker>
|
||||
@ -154,6 +155,11 @@
|
||||
</mat-card>
|
||||
|
||||
<mat-menu #speakerMenu="matMenu">
|
||||
<button mat-menu-item (click)="isSortMode = true">
|
||||
<mat-icon>sort</mat-icon>
|
||||
<span translate>Enable sorting</span>
|
||||
</button>
|
||||
|
||||
<os-projector-button
|
||||
*ngIf="viewListOfSpeakers && projectors && projectors.length > 1"
|
||||
[object]="getClosSlide()"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
@ -15,6 +15,7 @@ import { UserRepositoryService } from 'app/core/repositories/users/user-reposito
|
||||
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 { SortingListComponent } from 'app/shared/components/sorting-list/sorting-list.component';
|
||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
|
||||
import { ViewProjector } from 'app/site/projector/models/view-projector';
|
||||
@ -33,11 +34,19 @@ import { SpeakerState, ViewSpeaker } from '../../models/view-speaker';
|
||||
styleUrls: ['./list-of-speakers.component.scss']
|
||||
})
|
||||
export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit {
|
||||
@ViewChild(SortingListComponent, { static: false })
|
||||
public listElement: SortingListComponent;
|
||||
|
||||
/**
|
||||
* Determine if the user is viewing the current list if speakers
|
||||
*/
|
||||
public isCurrentListOfSpeakers = false;
|
||||
|
||||
/**
|
||||
* Holds whether the list is in sort mode or not
|
||||
*/
|
||||
public isSortMode = false;
|
||||
|
||||
/**
|
||||
* Holds the view item to the given topic
|
||||
*/
|
||||
@ -297,14 +306,25 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
||||
}
|
||||
|
||||
/**
|
||||
* React to manual in the sorting order.
|
||||
* Informs the repo about changes in the order
|
||||
* @param listInNewOrder Contains the newly ordered list of ViewSpeakers
|
||||
* send the current order of the sorting list to the server
|
||||
*/
|
||||
public onSortingChange(listInNewOrder: ViewSpeaker[]): void {
|
||||
// extract the ids from the ViewSpeaker array
|
||||
const userIds = listInNewOrder.map(speaker => speaker.id);
|
||||
this.listOfSpeakersRepo.sortSpeakers(this.viewListOfSpeakers, userIds).then(null, this.raiseError);
|
||||
public onSaveSorting(): void {
|
||||
if (this.isSortMode) {
|
||||
this.isSortMode = false;
|
||||
this.listOfSpeakersRepo
|
||||
.sortSpeakers(this.viewListOfSpeakers, this.listElement.sortedItems.map(el => el.id))
|
||||
.catch(this.raiseError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore old order on cancel
|
||||
*/
|
||||
public onCancelSorting(): void {
|
||||
if (this.isSortMode) {
|
||||
this.isSortMode = false;
|
||||
this.listElement.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -339,9 +359,14 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
||||
* @param speaker The speaker clicked on.
|
||||
*/
|
||||
public onMarkButton(speaker: ViewSpeaker): void {
|
||||
this.listOfSpeakersRepo
|
||||
.markSpeaker(this.viewListOfSpeakers, speaker, !speaker.marked)
|
||||
.then(null, this.raiseError);
|
||||
this.listOfSpeakersRepo.markSpeaker(this.viewListOfSpeakers, speaker, !speaker.marked).catch(this.raiseError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the last finished speaker from the list an re-adds him on pole position
|
||||
*/
|
||||
public readdLastSpeaker(): void {
|
||||
this.listOfSpeakersRepo.readdLastSpeaker(this.viewListOfSpeakers).catch(this.raiseError);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -351,6 +376,10 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
||||
* the operator themself is removed
|
||||
*/
|
||||
public async onDeleteButton(speaker?: ViewSpeaker): Promise<void> {
|
||||
const title = this.translate.instant(
|
||||
'Are you sure you want to delete this speaker from this list of speakers?'
|
||||
);
|
||||
if (await this.promptService.open(title)) {
|
||||
try {
|
||||
await this.listOfSpeakersRepo.delete(this.viewListOfSpeakers, speaker ? speaker.id : null);
|
||||
this.filterUsers();
|
||||
@ -358,6 +387,7 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
|
||||
this.raiseError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the operator is in the list of (waiting) speakers
|
||||
|
@ -216,7 +216,7 @@ export class CategoryMotionsSortComponent extends BaseViewComponent implements O
|
||||
if (this.sortSelector.multiSelectedIndex.length) {
|
||||
}
|
||||
const content = this.translate.instant('Move selected items ...');
|
||||
const choices = this.sortSelector.array
|
||||
const choices = this.sortSelector.sortedItems
|
||||
.map((item, index) => {
|
||||
return { id: index, label: item.getTitle() };
|
||||
})
|
||||
|
@ -290,7 +290,13 @@ class ListOfSpeakersViewSet(
|
||||
result = has_perm(self.request.user, "agenda.can_see_list_of_speakers")
|
||||
# For manage_speaker requests the rest of the check is
|
||||
# done in the specific method. See below.
|
||||
elif self.action in ("update", "partial_update", "speak", "sort_speakers"):
|
||||
elif self.action in (
|
||||
"update",
|
||||
"partial_update",
|
||||
"speak",
|
||||
"sort_speakers",
|
||||
"readd_last_speaker",
|
||||
):
|
||||
result = has_perm(
|
||||
self.request.user, "agenda.can_see_list_of_speakers"
|
||||
) and has_perm(self.request.user, "agenda.can_manage_list_of_speakers")
|
||||
@ -344,7 +350,7 @@ class ListOfSpeakersViewSet(
|
||||
# Try to add the user. This ensurse that a user is not twice in the
|
||||
# list of coming speakers.
|
||||
try:
|
||||
Speaker.objects.add(user, list_of_speakers)
|
||||
speaker = Speaker.objects.add(user, list_of_speakers)
|
||||
except OpenSlidesError as e:
|
||||
raise ValidationError({"detail": str(e)})
|
||||
|
||||
@ -520,3 +526,32 @@ class ListOfSpeakersViewSet(
|
||||
|
||||
# Initiate response.
|
||||
return Response({"detail": "List of speakers successfully sorted."})
|
||||
|
||||
@detail_route(methods=["POST"])
|
||||
def readd_last_speaker(self, request, pk=None):
|
||||
"""
|
||||
Special view endpoint to re-add the last finished speaker to the list of speakers.
|
||||
"""
|
||||
list_of_speakers = self.get_object()
|
||||
|
||||
# Retrieve speaker which spoke last and next speaker
|
||||
ordered_speakers = list_of_speakers.speakers.order_by("-end_time")
|
||||
if len(ordered_speakers) == 0:
|
||||
raise ValidationError({"detail": "There is no last speaker at the moment."})
|
||||
|
||||
last_speaker = ordered_speakers[0]
|
||||
if last_speaker.end_time is None:
|
||||
raise ValidationError({"detail": "There is no last speaker at the moment."})
|
||||
|
||||
next_speaker = list_of_speakers.get_next_speaker()
|
||||
new_weight = 1
|
||||
# if there is a next speaker, insert last speaker before it
|
||||
if next_speaker:
|
||||
new_weight = next_speaker.weight - 1
|
||||
|
||||
# reset times of last speaker and prepend it to the list of active speakers
|
||||
last_speaker.begin_time = last_speaker.end_time = None
|
||||
last_speaker.weight = new_weight
|
||||
last_speaker.save()
|
||||
|
||||
return Response()
|
||||
|
@ -3,6 +3,7 @@ from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
@ -295,6 +296,14 @@ class ManageSpeaker(TestCase):
|
||||
password="test_password_e6paev4zeeh9n",
|
||||
)
|
||||
|
||||
def revoke_admin_rights(self):
|
||||
admin = get_user_model().objects.get(username="admin")
|
||||
group_admin = admin.groups.get(name="Admin")
|
||||
group_delegates = type(group_admin).objects.get(name="Delegates")
|
||||
admin.groups.add(group_delegates)
|
||||
admin.groups.remove(group_admin)
|
||||
inform_changed_data(admin)
|
||||
|
||||
def test_add_oneself_once(self):
|
||||
response = self.client.post(
|
||||
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk])
|
||||
@ -374,12 +383,7 @@ class ManageSpeaker(TestCase):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_add_someone_else_non_admin(self):
|
||||
admin = get_user_model().objects.get(username="admin")
|
||||
group_admin = admin.groups.get(name="Admin")
|
||||
group_delegates = type(group_admin).objects.get(name="Delegates")
|
||||
admin.groups.add(group_delegates)
|
||||
admin.groups.remove(group_admin)
|
||||
inform_changed_data(admin)
|
||||
self.revoke_admin_rights()
|
||||
|
||||
response = self.client.post(
|
||||
reverse("listofspeakers-manage-speaker", args=[self.list_of_speakers.pk]),
|
||||
@ -415,12 +419,7 @@ class ManageSpeaker(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_remove_someone_else_non_admin(self):
|
||||
admin = get_user_model().objects.get(username="admin")
|
||||
group_admin = admin.groups.get(name="Admin")
|
||||
group_delegates = type(group_admin).objects.get(name="Delegates")
|
||||
admin.groups.add(group_delegates)
|
||||
admin.groups.remove(group_admin)
|
||||
inform_changed_data(admin)
|
||||
self.revoke_admin_rights()
|
||||
speaker = Speaker.objects.add(self.user, self.list_of_speakers)
|
||||
|
||||
response = self.client.delete(
|
||||
@ -441,12 +440,7 @@ class ManageSpeaker(TestCase):
|
||||
self.assertTrue(Speaker.objects.get().marked)
|
||||
|
||||
def test_mark_speaker_non_admin(self):
|
||||
admin = get_user_model().objects.get(username="admin")
|
||||
group_admin = admin.groups.get(name="Admin")
|
||||
group_delegates = type(group_admin).objects.get(name="Delegates")
|
||||
admin.groups.add(group_delegates)
|
||||
admin.groups.remove(group_admin)
|
||||
inform_changed_data(admin)
|
||||
self.revoke_admin_rights()
|
||||
Speaker.objects.add(self.user, self.list_of_speakers)
|
||||
|
||||
response = self.client.patch(
|
||||
@ -456,6 +450,79 @@ class ManageSpeaker(TestCase):
|
||||
|
||||
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)
|
||||
speaker.begin_time = timezone.now()
|
||||
speaker.end_time = timezone.now()
|
||||
speaker.weight = None
|
||||
speaker.save()
|
||||
|
||||
def test_readd_last_speaker_no_speaker(self):
|
||||
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_no_last_speaker(self):
|
||||
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_has_last_speaker_no_next_speaker(self):
|
||||
self.util_add_user_as_last_speaker()
|
||||
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"listofspeakers-readd-last-speaker", args=[self.list_of_speakers.pk]
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
speaker = Speaker.objects.get()
|
||||
self.assertTrue(
|
||||
speaker.begin_time is None
|
||||
and speaker.end_time is None
|
||||
and speaker.weight is not None
|
||||
)
|
||||
|
||||
def test_readd_last_speaker_has_last_speaker_and_next_speaker(self):
|
||||
self.util_add_user_as_last_speaker()
|
||||
user2 = get_user_model().objects.create_user(
|
||||
username="test_user_KLGHjkHJKBhjJHGGJKJn",
|
||||
password="test_password_JHt678VbhjuGhj76hjGA",
|
||||
)
|
||||
Speaker.objects.add(user2, self.list_of_speakers)
|
||||
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"listofspeakers-readd-last-speaker", args=[self.list_of_speakers.pk]
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
speaker = Speaker.objects.get(user__pk=self.user.pk)
|
||||
self.assertTrue(
|
||||
speaker.begin_time is None
|
||||
and speaker.end_time is None
|
||||
and speaker.weight is not None
|
||||
)
|
||||
|
||||
def test_readd_last_speaker_no_admin(self):
|
||||
self.util_add_user_as_last_speaker()
|
||||
self.revoke_admin_rights()
|
||||
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"listofspeakers-readd-last-speaker", args=[self.list_of_speakers.pk]
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
class Speak(TestCase):
|
||||
"""
|
||||
|
Loading…
Reference in New Issue
Block a user