Merge pull request #4352 from FinnStutzenstein/losSlide

List of speakers slide
This commit is contained in:
Emanuel Schütze 2019-02-21 13:00:02 +01:00 committed by GitHub
commit e97c308747
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 667 additions and 360 deletions

View File

@ -18,6 +18,7 @@ import { SlideManager } from 'app/slides/services/slide-manager.service';
import { BaseModel } from 'app/shared/models/base/base-model'; import { BaseModel } from 'app/shared/models/base/base-model';
import { ViewModelStoreService } from './view-model-store.service'; import { ViewModelStoreService } from './view-model-store.service';
import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model'; import { BaseProjectableViewModel } from 'app/site/base/base-projectable-view-model';
import { TranslateService } from '@ngx-translate/core';
/** /**
* This service cares about Projectables being projected and manage all projection-related * This service cares about Projectables being projected and manage all projection-related
@ -39,7 +40,8 @@ export class ProjectorService {
private DS: DataStoreService, private DS: DataStoreService,
private http: HttpService, private http: HttpService,
private slideManager: SlideManager, private slideManager: SlideManager,
private viewModelStore: ViewModelStoreService private viewModelStore: ViewModelStoreService,
private translate: TranslateService
) {} ) {}
/** /**
@ -273,6 +275,24 @@ export class ProjectorService {
return viewModel; return viewModel;
} }
/**
*/
public getSlideTitle(element: ProjectorElement): string {
if (this.slideManager.canSlideBeMappedToModel(element.name)) {
const idElement = this.slideManager.getIdentifialbeProjectorElement(element);
const viewModel = this.getViewModelFromProjectorElement(idElement);
if (viewModel) {
return viewModel.getProjectorTitle();
}
}
const configuration = this.slideManager.getSlideConfiguration(element.name);
if (configuration.getSlideTitle) {
return configuration.getSlideTitle(element, this.translate, this.viewModelStore);
}
return this.translate.instant(this.slideManager.getSlideVerboseName(element.name));
}
/** /**
* Projects the next slide in the queue. Moves all currently projected * Projects the next slide in the queue. Moves all currently projected
* non-stable slides to the history. * non-stable slides to the history.

View File

@ -20,6 +20,7 @@ import { ViewModelStoreService } from 'app/core/core-services/view-model-store.s
import { BaseViewModel } from 'app/site/base/base-view-model'; import { BaseViewModel } from 'app/site/base/base-view-model';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { BaseAgendaContentObjectRepository } from '../base-agenda-content-object-repository';
/** /**
* Repository service for users * Repository service for users
@ -77,6 +78,28 @@ export class ItemRepositoryService extends BaseRepository<ViewItem, Item> {
viewItem.getVerboseName = (plural: boolean = false) => { viewItem.getVerboseName = (plural: boolean = false) => {
return this.translate.instant(plural ? 'Items' : 'Item'); return this.translate.instant(plural ? 'Items' : 'Item');
}; };
viewItem.getTitle = () => {
if (viewItem.contentObject) {
return viewItem.contentObject.getAgendaTitle();
} else {
const repo = this.collectionStringMapperService.getRepository(
viewItem.item.content_object.collection
) as BaseAgendaContentObjectRepository<any, any>;
return repo.getAgendaTitle(viewItem);
}
};
viewItem.getListTitle = () => {
const numberPrefix = viewItem.itemNumber ? `${viewItem.itemNumber} · ` : '';
if (viewItem.contentObject) {
return numberPrefix + viewItem.contentObject.getAgendaTitleWithType();
} else {
const repo = this.collectionStringMapperService.getRepository(
viewItem.item.content_object.collection
) as BaseAgendaContentObjectRepository<any, any>;
return numberPrefix + repo.getAgendaTitleWithType(viewItem);
}
};
return viewItem; return viewItem;
} }

View File

@ -1,7 +1,8 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Topic } from 'app/shared/models/topics/topic'; import { Topic } from 'app/shared/models/topics/topic';
import { BaseRepository } from 'app/core/repositories/base-repository';
import { Mediafile } from 'app/shared/models/mediafiles/mediafile'; import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
import { Item } from 'app/shared/models/agenda/item'; import { Item } from 'app/shared/models/agenda/item';
import { DataStoreService } from 'app/core/core-services/data-store.service'; import { DataStoreService } from 'app/core/core-services/data-store.service';
@ -13,7 +14,7 @@ import { CreateTopic } from 'app/site/agenda/models/create-topic';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { ViewItem } from 'app/site/agenda/models/view-item'; import { ViewItem } from 'app/site/agenda/models/view-item';
import { TranslateService } from '@ngx-translate/core'; import { BaseAgendaContentObjectRepository } from '../base-agenda-content-object-repository';
/** /**
* Repository for topics * Repository for topics
@ -21,7 +22,7 @@ import { TranslateService } from '@ngx-translate/core';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class TopicRepositoryService extends BaseRepository<ViewTopic, Topic> { export class TopicRepositoryService extends BaseAgendaContentObjectRepository<ViewTopic, Topic> {
/** /**
* Constructor calls the parent constructor * Constructor calls the parent constructor
* *
@ -39,6 +40,19 @@ export class TopicRepositoryService extends BaseRepository<ViewTopic, Topic> {
super(DS, mapperService, viewModelStoreService, Topic, [Mediafile, Item]); super(DS, mapperService, viewModelStoreService, Topic, [Mediafile, Item]);
} }
public getAgendaTitle = (topic: Partial<Topic> | Partial<ViewTopic>) => {
return topic.title;
};
public getAgendaTitleWithType = (topic: Partial<Topic> | Partial<ViewTopic>) => {
// Do not append ' (Topic)' to the title.
return topic.title;
};
public getVerboseName = (plural: boolean = false) => {
return this.translate.instant(plural ? 'Topics' : 'Topic');
};
/** /**
* Creates a new viewModel out of the given model * Creates a new viewModel out of the given model
* *
@ -49,9 +63,9 @@ export class TopicRepositoryService extends BaseRepository<ViewTopic, Topic> {
const attachments = this.viewModelStoreService.getMany(ViewMediafile, topic.attachments_id); const attachments = this.viewModelStoreService.getMany(ViewMediafile, topic.attachments_id);
const item = this.viewModelStoreService.get(ViewItem, topic.agenda_item_id); const item = this.viewModelStoreService.get(ViewItem, topic.agenda_item_id);
const viewTopic = new ViewTopic(topic, attachments, item); const viewTopic = new ViewTopic(topic, attachments, item);
viewTopic.getVerboseName = (plural: boolean = false) => { viewTopic.getVerboseName = this.getVerboseName;
return this.translate.instant(plural ? 'Topics' : 'Topic'); viewTopic.getAgendaTitle = () => this.getAgendaTitle(viewTopic);
}; viewTopic.getAgendaTitleWithType = () => this.getAgendaTitle(viewTopic);
return viewTopic; return viewTopic;
} }

View File

@ -1,10 +1,12 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { ViewAssignment } from 'app/site/assignments/models/view-assignment'; import { ViewAssignment } from 'app/site/assignments/models/view-assignment';
import { Assignment } from 'app/shared/models/assignments/assignment'; import { Assignment } from 'app/shared/models/assignments/assignment';
import { User } from 'app/shared/models/users/user'; import { User } from 'app/shared/models/users/user';
import { Tag } from 'app/shared/models/core/tag'; import { Tag } from 'app/shared/models/core/tag';
import { Item } from 'app/shared/models/agenda/item'; import { Item } from 'app/shared/models/agenda/item';
import { BaseRepository } from '../base-repository';
import { DataStoreService } from '../../core-services/data-store.service'; import { DataStoreService } from '../../core-services/data-store.service';
import { Identifiable } from 'app/shared/models/base/identifiable'; import { Identifiable } from 'app/shared/models/base/identifiable';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service'; import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
@ -12,7 +14,7 @@ import { ViewModelStoreService } from 'app/core/core-services/view-model-store.s
import { ViewItem } from 'app/site/agenda/models/view-item'; import { ViewItem } from 'app/site/agenda/models/view-item';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
import { ViewTag } from 'app/site/tags/models/view-tag'; import { ViewTag } from 'app/site/tags/models/view-tag';
import { TranslateService } from '@ngx-translate/core'; import { BaseAgendaContentObjectRepository } from '../base-agenda-content-object-repository';
/** /**
* Repository Service for Assignments. * Repository Service for Assignments.
@ -22,7 +24,7 @@ import { TranslateService } from '@ngx-translate/core';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class AssignmentRepositoryService extends BaseRepository<ViewAssignment, Assignment> { export class AssignmentRepositoryService extends BaseAgendaContentObjectRepository<ViewAssignment, Assignment> {
/** /**
* Constructor for the Assignment Repository. * Constructor for the Assignment Repository.
* *
@ -38,15 +40,27 @@ export class AssignmentRepositoryService extends BaseRepository<ViewAssignment,
super(DS, mapperService, viewModelStoreService, Assignment, [User, Item, Tag]); super(DS, mapperService, viewModelStoreService, Assignment, [User, Item, Tag]);
} }
public getAgendaTitle = (assignment: Partial<Assignment> | Partial<ViewAssignment>) => {
return assignment.title;
};
public getAgendaTitleWithType = (assignment: Partial<Assignment> | Partial<ViewAssignment>) => {
return assignment.title + ' (' + this.getVerboseName() + ')';
};
public getVerboseName = (plural: boolean = false) => {
return this.translate.instant(plural ? 'Elections' : 'Election');
};
public createViewModel(assignment: Assignment): ViewAssignment { public createViewModel(assignment: Assignment): ViewAssignment {
const relatedUser = this.viewModelStoreService.getMany(ViewUser, assignment.candidates_id); const relatedUser = this.viewModelStoreService.getMany(ViewUser, assignment.candidates_id);
const agendaItem = this.viewModelStoreService.get(ViewItem, assignment.agenda_item_id); const agendaItem = this.viewModelStoreService.get(ViewItem, assignment.agenda_item_id);
const tags = this.viewModelStoreService.getMany(ViewTag, assignment.tags_id); const tags = this.viewModelStoreService.getMany(ViewTag, assignment.tags_id);
const viewAssignment = new ViewAssignment(assignment, relatedUser, agendaItem, tags); const viewAssignment = new ViewAssignment(assignment, relatedUser, agendaItem, tags);
viewAssignment.getVerboseName = (plural: boolean = false) => { viewAssignment.getVerboseName = this.getVerboseName;
return this.translate.instant(plural ? 'Elections' : 'Election'); viewAssignment.getAgendaTitle = () => this.getAgendaTitle(viewAssignment);
}; viewAssignment.getAgendaTitleWithType = () => this.getAgendaTitleWithType(viewAssignment);
return viewAssignment; return viewAssignment;
} }

View File

@ -0,0 +1,37 @@
import { BaseViewModel } from '../../site/base/base-view-model';
import { BaseModel, ModelConstructor } from '../../shared/models/base/base-model';
import { CollectionStringMapperService } from '../core-services/collectionStringMapper.service';
import { DataStoreService } from '../core-services/data-store.service';
import { ViewModelStoreService } from '../core-services/view-model-store.service';
import { BaseRepository } from './base-repository';
export function isBaseAgendaContentObjectRepository(obj: any): obj is BaseAgendaContentObjectRepository<any, any> {
const repo = obj as BaseAgendaContentObjectRepository<any, any>;
return (
!!obj &&
repo.getVerboseName !== undefined &&
repo.getAgendaTitle !== undefined &&
repo.getAgendaTitleWithType !== undefined
);
}
export abstract class BaseAgendaContentObjectRepository<
V extends BaseViewModel,
M extends BaseModel
> extends BaseRepository<V, M> {
public abstract getAgendaTitle: (model: Partial<M> | Partial<V>) => string;
public abstract getAgendaTitleWithType: (model: Partial<M> | Partial<V>) => string;
public abstract getVerboseName: (plural?: boolean) => string;
/**
*/
public constructor(
DS: DataStoreService,
collectionStringMapperService: CollectionStringMapperService,
viewModelStoreService: ViewModelStoreService,
baseModelCtor: ModelConstructor<M>,
depsModelCtors?: ModelConstructor<BaseModel>[]
) {
super(DS, collectionStringMapperService, viewModelStoreService, baseModelCtor, depsModelCtors);
}
}

View File

@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { DataSendService } from 'app/core/core-services/data-send.service'; import { DataSendService } from 'app/core/core-services/data-send.service';
import { User } from 'app/shared/models/users/user'; import { User } from 'app/shared/models/users/user';
@ -14,7 +15,6 @@ import { ViewMotionChangeRecommendation } from 'app/site/motions/models/view-cha
import { Identifiable } from 'app/shared/models/base/identifiable'; import { Identifiable } from 'app/shared/models/base/identifiable';
import { CollectionStringMapperService } from 'app/core/core-services/collectionStringMapper.service'; import { CollectionStringMapperService } from 'app/core/core-services/collectionStringMapper.service';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { TranslateService } from '@ngx-translate/core';
/** /**
* Repository Services for change recommendations * Repository Services for change recommendations

View File

@ -2,8 +2,8 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { BaseRepository } from 'app/core/repositories/base-repository';
import { CollectionStringMapperService } from 'app/core/core-services/collectionStringMapper.service'; import { CollectionStringMapperService } from 'app/core/core-services/collectionStringMapper.service';
import { DataSendService } from 'app/core/core-services/data-send.service'; import { DataSendService } from 'app/core/core-services/data-send.service';
import { DataStoreService } from 'app/core/core-services/data-store.service'; import { DataStoreService } from 'app/core/core-services/data-store.service';
@ -17,7 +17,7 @@ import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { Item } from 'app/shared/models/agenda/item'; import { Item } from 'app/shared/models/agenda/item';
import { ViewItem } from 'app/site/agenda/models/view-item'; import { ViewItem } from 'app/site/agenda/models/view-item';
import { TranslateService } from '@ngx-translate/core'; import { BaseAgendaContentObjectRepository } from '../base-agenda-content-object-repository';
/** /**
* Repository service for motion blocks * Repository service for motion blocks
@ -25,7 +25,7 @@ import { TranslateService } from '@ngx-translate/core';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class MotionBlockRepositoryService extends BaseRepository<ViewMotionBlock, MotionBlock> { export class MotionBlockRepositoryService extends BaseAgendaContentObjectRepository<ViewMotionBlock, MotionBlock> {
/** /**
* Constructor for the motion block repository * Constructor for the motion block repository
* *
@ -47,6 +47,18 @@ export class MotionBlockRepositoryService extends BaseRepository<ViewMotionBlock
super(DS, mapperService, viewModelStoreService, MotionBlock, [Item]); super(DS, mapperService, viewModelStoreService, MotionBlock, [Item]);
} }
public getAgendaTitle = (motionBlock: Partial<MotionBlock> | Partial<ViewMotionBlock>) => {
return motionBlock.title;
};
public getAgendaTitleWithType = (motionBlock: Partial<MotionBlock> | Partial<ViewMotionBlock>) => {
return motionBlock.title + ' (' + this.getVerboseName() + ')';
};
public getVerboseName = (plural: boolean = false) => {
return this.translate.instant(plural ? 'Motion blocks' : 'Motion block');
};
/** /**
* Converts a given motion block into a ViewModel * Converts a given motion block into a ViewModel
* *
@ -56,9 +68,9 @@ export class MotionBlockRepositoryService extends BaseRepository<ViewMotionBlock
protected createViewModel(block: MotionBlock): ViewMotionBlock { protected createViewModel(block: MotionBlock): ViewMotionBlock {
const item = this.viewModelStoreService.get(ViewItem, block.agenda_item_id); const item = this.viewModelStoreService.get(ViewItem, block.agenda_item_id);
const viewMotionBlock = new ViewMotionBlock(block, item); const viewMotionBlock = new ViewMotionBlock(block, item);
viewMotionBlock.getVerboseName = (plural: boolean = false) => { viewMotionBlock.getVerboseName = this.getVerboseName;
return this.translate.instant(plural ? 'Motion blocks' : 'Motion block'); viewMotionBlock.getAgendaTitle = () => this.getAgendaTitle(viewMotionBlock);
}; viewMotionBlock.getAgendaTitleWithType = () => this.getAgendaTitleWithType(viewMotionBlock);
return viewMotionBlock; return viewMotionBlock;
} }

View File

@ -4,7 +4,6 @@ import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { tap, map } from 'rxjs/operators'; import { tap, map } from 'rxjs/operators';
import { BaseRepository } from '../base-repository';
import { Category } from 'app/shared/models/motions/category'; import { Category } from 'app/shared/models/motions/category';
import { ChangeRecoMode, ViewMotion } from 'app/site/motions/models/view-motion'; import { ChangeRecoMode, ViewMotion } from 'app/site/motions/models/view-motion';
import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service'; import { CollectionStringMapperService } from '../../core-services/collectionStringMapper.service';
@ -40,6 +39,7 @@ import { ViewItem } from 'app/site/agenda/models/view-item';
import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block'; import { ViewMotionBlock } from 'app/site/motions/models/view-motion-block';
import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { ViewTag } from 'app/site/tags/models/view-tag'; import { ViewTag } from 'app/site/tags/models/view-tag';
import { BaseAgendaContentObjectRepository } from '../base-agenda-content-object-repository';
/** /**
* Repository Services for motions (and potentially categories) * Repository Services for motions (and potentially categories)
@ -54,7 +54,7 @@ import { ViewTag } from 'app/site/tags/models/view-tag';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion> { export class MotionRepositoryService extends BaseAgendaContentObjectRepository<ViewMotion, Motion> {
/** /**
* Creates a MotionRepository * Creates a MotionRepository
* *
@ -92,6 +92,28 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
]); ]);
} }
public getAgendaTitle = (motion: Partial<Motion> | Partial<ViewMotion>) => {
// if the identifier is set, the title will be 'Motion <identifier>'.
if (motion.identifier) {
return this.translate.instant('Motion') + ' ' + motion.identifier;
} else {
return motion.title;
}
};
public getAgendaTitleWithType = (motion: Partial<Motion> | Partial<ViewMotion>) => {
// Append the verbose name only, if not the special format 'Motion <identifier>' is used.
if (motion.identifier) {
return this.translate.instant('Motion') + ' ' + motion.identifier;
} else {
return motion.title + ' (' + this.getVerboseName() + ')';
}
};
public getVerboseName = (plural: boolean = false) => {
return this.translate.instant(plural ? 'Motions' : 'Motion');
};
/** /**
* Converts a motion to a ViewMotion and adds it to the store. * Converts a motion to a ViewMotion and adds it to the store.
* *
@ -127,26 +149,10 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
tags, tags,
parent parent
); );
viewMotion.getVerboseName = (plural: boolean = false) => { viewMotion.getVerboseName = this.getVerboseName;
return this.translate.instant(plural ? 'Motions' : 'Motion'); viewMotion.getAgendaTitle = () => this.getAgendaTitle(viewMotion);
};
viewMotion.getAgendaTitle = () => {
// if the identifier is set, the title will be 'Motion <identifier>'.
if (viewMotion.identifier) {
return this.translate.instant('Motion') + ' ' + viewMotion.identifier;
} else {
return viewMotion.getTitle();
}
};
viewMotion.getProjectorTitle = viewMotion.getAgendaTitle; viewMotion.getProjectorTitle = viewMotion.getAgendaTitle;
viewMotion.getAgendaTitleWithType = () => { viewMotion.getAgendaTitleWithType = () => this.getAgendaTitleWithType(viewMotion);
// Append the verbose name only, if not the special format 'Motion <identifier>' is used.
if (viewMotion.identifier) {
return this.translate.instant('Motion') + ' ' + viewMotion.identifier;
} else {
return viewMotion.getTitle() + ' (' + viewMotion.getVerboseName() + ')';
}
};
return viewMotion; return viewMotion;
} }

View File

@ -1,5 +1,5 @@
<h2 mat-dialog-title> <h2 mat-dialog-title>
<span translate>Project</span> {{ projectorElementBuildDescriptor.getTitle() }}? <span translate>Project</span> {{ projectorElementBuildDescriptor.getDialogTitle() }}?
</h2> </h2>
<mat-dialog-content> <mat-dialog-content>
<div class="projectors" <div class="projectors"

View File

@ -1,4 +1,16 @@
<button type="button" mat-mini-fab (click)="onClick($event)" <ng-container *osPerms="'core.can_manage_projector'">
[ngClass]="isProjected() ? 'projectorbutton-active' : 'projectorbutton-inactive'"> <button type="button" *ngIf="!text && !menuItem" mat-mini-fab (click)="onClick($event)"
[ngClass]="isProjected() ? 'projector-active' : 'projector-inactive'">
<mat-icon>videocam</mat-icon> <mat-icon>videocam</mat-icon>
</button> </button>
<button type="button" *ngIf="menuItem" mat-menu-item (click)="onClick($event)"
[ngClass]="isProjected() ? 'projector-active' : 'projector-inactive'">
<mat-icon>videocam</mat-icon>
<span translate>Project</span>
</button>
<button type="button" *ngIf="text && !menuItem" mat-button (click)="onClick($event)"
[ngClass]="isProjected() ? 'projector-active' : 'projector-inactive'">
<mat-icon>videocam</mat-icon>
{{ text | translate }}
</button>
</ng-container>

View File

@ -1,4 +1,18 @@
.projectorbutton-inactive { .projector-inactive {
background-color: white !important; background-color: white !important;
mat-icon {
color: grey !important; color: grey !important;
} }
}
/** TODO: Take this from the accent color. Make the hovering visible */
.projector-active {
background-color: #03a9f4;
color: white !important;
}
button.mat-menu-item.projector-active:hover {
background-color: #03a9f4;
}

View File

@ -39,6 +39,12 @@ export class ProjectorButtonComponent implements OnInit {
} }
} }
@Input()
public text: string | null;
@Input()
public menuItem = false;
/** /**
* The constructor * The constructor
*/ */

View File

@ -8,6 +8,8 @@
left: 0; left: 0;
transform-origin: left top; transform-origin: left top;
overflow: hidden; overflow: hidden;
font-size: 22px !important;
line-height: 24px !important;
#header { #header {
position: absolute; position: absolute;

View File

@ -29,8 +29,7 @@ export class Item extends BaseModel<Item> {
public id: number; public id: number;
public item_number: string; public item_number: string;
public title: string; public title_information: object;
public title_with_type: string;
public comment: string; public comment: string;
public closed: boolean; public closed: boolean;
public type: number; public type: number;

View File

@ -6,7 +6,7 @@
<span *ngIf="currentListOfSpeakers">Current list of speakers</span> <span *ngIf="currentListOfSpeakers">Current list of speakers</span>
</h2> </h2>
</div> </div>
<div class="menu-slot" *osPerms="'agenda.can_manage_list_of_speakers'"> <div class="menu-slot" *osPerms="['agenda.can_manage_list_of_speakers', 'core.can_manage_projector']">
<button type="button" mat-icon-button [matMenuTriggerFor]="speakerMenu"><mat-icon>more_vert</mat-icon></button> <button type="button" mat-icon-button [matMenuTriggerFor]="speakerMenu"><mat-icon>more_vert</mat-icon></button>
</div> </div>
</os-head-bar> </os-head-bar>
@ -131,6 +131,8 @@
</mat-card> </mat-card>
<mat-menu #speakerMenu="matMenu"> <mat-menu #speakerMenu="matMenu">
<os-projector-button *ngIf="viewItem" [object]="viewItem.listOfSpeakersSlide" [menuItem]="true"></os-projector-button>
<button mat-menu-item *ngIf="closedList" (click)="openSpeakerList()"> <button mat-menu-item *ngIf="closedList" (click)="openSpeakerList()">
<mat-icon>mic</mat-icon> <mat-icon>mic</mat-icon>
<span translate>Open list of speakers</span> <span translate>Open list of speakers</span>

View File

@ -2,6 +2,7 @@ import { BaseViewModel } from '../../base/base-view-model';
import { Item, itemVisibilityChoices } from 'app/shared/models/agenda/item'; import { Item, itemVisibilityChoices } from 'app/shared/models/agenda/item';
import { Speaker, SpeakerState } from 'app/shared/models/agenda/speaker'; import { Speaker, SpeakerState } from 'app/shared/models/agenda/speaker';
import { BaseAgendaViewModel, isAgendaBaseModel } from 'app/site/base/base-agenda-view-model'; import { BaseAgendaViewModel, isAgendaBaseModel } from 'app/site/base/base-agenda-view-model';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
export class ViewItem extends BaseViewModel { export class ViewItem extends BaseViewModel {
public static COLLECTIONSTRING = Item.COLLECTIONSTRING; public static COLLECTIONSTRING = Item.COLLECTIONSTRING;
@ -111,6 +112,19 @@ export class ViewItem extends BaseViewModel {
* This is set by the repository * This is set by the repository
*/ */
public getVerboseName; public getVerboseName;
public getTitle;
public getListTitle;
public listOfSpeakersSlide: ProjectorElementBuildDeskriptor = {
getBasicProjectorElement: options => ({
name: 'agenda/list-of-speakers',
id: this.id,
getIdentifiers: () => ['name', 'id']
}),
slideOptions: [],
projectionDefaultName: 'agenda_list_of_speakers',
getDialogTitle: () => this.getTitle()
};
public constructor(item: Item, contentObject: BaseAgendaViewModel) { public constructor(item: Item, contentObject: BaseAgendaViewModel) {
super(Item.COLLECTIONSTRING); super(Item.COLLECTIONSTRING);
@ -118,30 +132,6 @@ export class ViewItem extends BaseViewModel {
this._contentObject = contentObject; this._contentObject = contentObject;
} }
public getTitle = () => {
if (this.contentObject) {
return this.contentObject.getAgendaTitle();
} else {
return this.item ? this.item.title : null;
}
};
/**
* Create the list view title.
* If a number was given, 'whitespac-dot-whitespace' will be added to the prefix number
*
* @returns the agenda list title as string
*/
public getListTitle = () => {
const numberPrefix = this.itemNumber ? `${this.itemNumber} · ` : '';
if (this.contentObject) {
return numberPrefix + this.contentObject.getAgendaTitleWithType();
} else {
return numberPrefix + this.item.title_with_type;
}
};
public updateDependencies(update: BaseViewModel): boolean { public updateDependencies(update: BaseViewModel): boolean {
if ( if (
update && update &&

View File

@ -53,6 +53,8 @@ export class ViewTopic extends BaseAgendaViewModel {
* This is set by the repository * This is set by the repository
*/ */
public getVerboseName; public getVerboseName;
public getAgendaTitle;
public getAgendaTitleWithType;
public constructor(topic: Topic, attachments?: ViewMediafile[], item?: ViewItem) { public constructor(topic: Topic, attachments?: ViewMediafile[], item?: ViewItem) {
super(Topic.COLLECTIONSTRING); super(Topic.COLLECTIONSTRING);
@ -69,11 +71,6 @@ export class ViewTopic extends BaseAgendaViewModel {
return this.agendaItem; return this.agendaItem;
} }
public getAgendaTitleWithType = () => {
// Do not append ' (Topic)' to the title.
return this.getAgendaTitle();
};
/** /**
* Formats the category for search * Formats the category for search
* *
@ -104,7 +101,7 @@ export class ViewTopic extends BaseAgendaViewModel {
}), }),
slideOptions: [], slideOptions: [],
projectionDefaultName: 'topics', projectionDefaultName: 'topics',
getTitle: () => this.getTitle() getDialogTitle: () => this.getTitle()
}; };
} }

View File

@ -23,6 +23,10 @@ export class ViewAssignment extends BaseAgendaViewModel {
return this._assignment; return this._assignment;
} }
public get title(): string {
return this.assignment.title;
}
public get candidates(): ViewUser[] { public get candidates(): ViewUser[] {
return this._relatedUser; return this._relatedUser;
} }
@ -50,6 +54,8 @@ export class ViewAssignment extends BaseAgendaViewModel {
* This is set by the repository * This is set by the repository
*/ */
public getVerboseName; public getVerboseName;
public getAgendaTitle;
public getAgendaTitleWithType;
public constructor(assignment: Assignment, relatedUser?: ViewUser[], agendaItem?: ViewItem, tags?: ViewTag[]) { public constructor(assignment: Assignment, relatedUser?: ViewUser[], agendaItem?: ViewItem, tags?: ViewTag[]) {
super(Assignment.COLLECTIONSTRING); super(Assignment.COLLECTIONSTRING);
@ -68,7 +74,7 @@ export class ViewAssignment extends BaseAgendaViewModel {
} }
public getTitle = () => { public getTitle = () => {
return this.assignment.title; return this.title;
}; };
public formatForSearch(): SearchRepresentation { public formatForSearch(): SearchRepresentation {
@ -88,7 +94,7 @@ export class ViewAssignment extends BaseAgendaViewModel {
}), }),
slideOptions: [], slideOptions: [],
projectionDefaultName: 'assignments', projectionDefaultName: 'assignments',
getTitle: () => this.getTitle() getDialogTitle: () => this.getTitle()
}; };
} }
} }

View File

@ -8,7 +8,7 @@ export function isProjectorElementBuildDeskriptor(obj: any): obj is ProjectorEle
!!deskriptor && !!deskriptor &&
deskriptor.slideOptions !== undefined && deskriptor.slideOptions !== undefined &&
deskriptor.getBasicProjectorElement !== undefined && deskriptor.getBasicProjectorElement !== undefined &&
deskriptor.getTitle !== undefined deskriptor.getDialogTitle !== undefined
); );
} }
@ -20,7 +20,7 @@ export interface ProjectorElementBuildDeskriptor {
/** /**
* The title to show in the projection dialog * The title to show in the projection dialog
*/ */
getTitle(): string; getDialogTitle(): string;
} }
export function isProjectable(obj: any): obj is Projectable { export function isProjectable(obj: any): obj is Projectable {

View File

@ -97,7 +97,7 @@ export class ViewMediafile extends BaseProjectableViewModel implements Searchabl
}), }),
slideOptions: [], slideOptions: [],
projectionDefaultName: 'mediafiles', projectionDefaultName: 'mediafiles',
getTitle: () => this.getTitle() getDialogTitle: () => this.getTitle()
}; };
} }

View File

@ -40,6 +40,8 @@ export class ViewMotionBlock extends BaseAgendaViewModel implements Searchable {
* This is set by the repository * This is set by the repository
*/ */
public getVerboseName; public getVerboseName;
public getAgendaTitle;
public getAgendaTitleWithType;
public constructor(motionBlock: MotionBlock, agendaItem?: ViewItem) { public constructor(motionBlock: MotionBlock, agendaItem?: ViewItem) {
super(MotionBlock.COLLECTIONSTRING); super(MotionBlock.COLLECTIONSTRING);

View File

@ -580,7 +580,7 @@ export class ViewMotion extends BaseAgendaViewModel implements Searchable {
} }
], ],
projectionDefaultName: 'motions', projectionDefaultName: 'motions',
getTitle: this.getAgendaTitle getDialogTitle: this.getAgendaTitle
}; };
} }

View File

@ -56,7 +56,7 @@
<div class="queue"> <div class="queue">
<h5 translate>History</h5> <h5 translate>History</h5>
<p *ngFor="let elements of projector?.elements_history"> <p *ngFor="let elements of projector?.elements_history">
{{ getElementDescription(elements[0]) }} {{ getSlideTitle(elements[0]) }}
</p> </p>
</div> </div>
@ -70,7 +70,7 @@
<button type="button" mat-icon-button (click)="unprojectCurrent(element)"> <button type="button" mat-icon-button (click)="unprojectCurrent(element)">
<mat-icon>videocam</mat-icon> <mat-icon>videocam</mat-icon>
</button> </button>
{{ getElementDescription(element) }} {{ getSlideTitle(element) }}
</mat-list-item> </mat-list-item>
</mat-list> </mat-list>
</div> </div>
@ -142,7 +142,7 @@
<mat-icon>drag_indicator</mat-icon> <mat-icon>drag_indicator</mat-icon>
</div> </div>
<div class="name"> <div class="name">
{{ i+1 }}.&nbsp;<span>{{ getElementDescription(element) }}</span> {{ i+1 }}.&nbsp;<span>{{ getSlideTitle(element) }}</span>
</div> </div>
<div class="button-right"> <div class="button-right">
<div> <div>

View File

@ -116,16 +116,8 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
this.projectorService.projectPreviewSlide(this.projector.projector, elementIndex).then(null, this.raiseError); this.projectorService.projectPreviewSlide(this.projector.projector, elementIndex).then(null, this.raiseError);
} }
public getElementDescription(element: ProjectorElement): string { public getSlideTitle(element: ProjectorElement): string {
if (this.slideManager.canSlideBeMappedToModel(element.name)) { return this.projectorService.getSlideTitle(element);
const idElement = this.slideManager.getIdentifialbeProjectorElement(element);
const viewModel = this.projectorService.getViewModelFromProjectorElement(idElement);
if (viewModel) {
return viewModel.getProjectorTitle();
}
}
return this.slideManager.getSlideVerboseName(element.name);
} }
public isProjected(obj: Projectable): boolean { public isProjected(obj: Projectable): boolean {

View File

@ -64,7 +64,7 @@ export class ViewCountdown extends BaseProjectableViewModel {
} }
], ],
projectionDefaultName: 'countdowns', projectionDefaultName: 'countdowns',
getTitle: () => this.getTitle() getDialogTitle: () => this.getTitle()
}; };
} }
} }

View File

@ -47,7 +47,7 @@ export class ViewProjectorMessage extends BaseProjectableViewModel {
}), }),
slideOptions: [], slideOptions: [],
projectionDefaultName: 'messages', projectionDefaultName: 'messages',
getTitle: () => this.getTitle() getDialogTitle: () => this.getTitle()
}; };
} }

View File

@ -199,7 +199,7 @@ export class ViewUser extends BaseProjectableViewModel implements Searchable {
}), }),
slideOptions: [], slideOptions: [],
projectionDefaultName: 'users', projectionDefaultName: 'users',
getTitle: () => this.getTitle() getDialogTitle: () => this.getTitle()
}; };
} }

View File

@ -1,3 +0,0 @@
export interface AgendaCurrentListOfSpeakersSlideData {
error: string;
}

View File

@ -0,0 +1,3 @@
export interface CurrentListOfSpeakersSlideData {
error: string;
}

View File

@ -1,26 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AgendaCurrentListOfSpeakersOverlaySlideComponent } from './agenda-current-list-of-speakers-overlay-slide.component';
import { E2EImportsModule } from '../../../../e2e-imports.module';
describe('AgendaCurrentListOfSpeakersOverlaySlideComponent', () => {
let component: AgendaCurrentListOfSpeakersOverlaySlideComponent;
let fixture: ComponentFixture<AgendaCurrentListOfSpeakersOverlaySlideComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
declarations: [AgendaCurrentListOfSpeakersOverlaySlideComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AgendaCurrentListOfSpeakersOverlaySlideComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,17 +0,0 @@
import { Component } from '@angular/core';
import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { AgendaCurrentListOfSpeakersSlideData } from '../base/agenda-current-list-of-speakers-slide-data';
@Component({
selector: 'os-agenda-current-list-of-speakers-overlay-slide',
templateUrl: './agenda-current-list-of-speakers-overlay-slide.component.html',
styleUrls: ['./agenda-current-list-of-speakers-overlay-slide.component.scss']
})
export class AgendaCurrentListOfSpeakersOverlaySlideComponent extends BaseSlideComponent<
AgendaCurrentListOfSpeakersSlideData
> {
public constructor() {
super();
}
}

View File

@ -1,13 +0,0 @@
import { AgendaCurrentListOfSpeakersOverlaySlideModule } from './agenda-current-list-of-speakers-overlay-slide.module';
describe('AgendaCurrentListOfSpeakersOverlaySlideModule', () => {
let agendaCurrentListOfSpeakersOverlaySlideModule: AgendaCurrentListOfSpeakersOverlaySlideModule;
beforeEach(() => {
agendaCurrentListOfSpeakersOverlaySlideModule = new AgendaCurrentListOfSpeakersOverlaySlideModule();
});
it('should create an instance', () => {
expect(agendaCurrentListOfSpeakersOverlaySlideModule).toBeTruthy();
});
});

View File

@ -1,7 +0,0 @@
import { NgModule } from '@angular/core';
import { makeSlideModule } from 'app/slides/base-slide-module';
import { AgendaCurrentListOfSpeakersOverlaySlideComponent } from './agenda-current-list-of-speakers-overlay-slide.component';
@NgModule(makeSlideModule(AgendaCurrentListOfSpeakersOverlaySlideComponent))
export class AgendaCurrentListOfSpeakersOverlaySlideModule {}

View File

@ -0,0 +1,26 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CurrentListOfSpeakersOverlaySlideComponent } from './current-list-of-speakers-overlay-slide.component';
import { E2EImportsModule } from '../../../../e2e-imports.module';
describe('CurrentListOfSpeakersOverlaySlideComponent', () => {
let component: CurrentListOfSpeakersOverlaySlideComponent;
let fixture: ComponentFixture<CurrentListOfSpeakersOverlaySlideComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
declarations: [CurrentListOfSpeakersOverlaySlideComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CurrentListOfSpeakersOverlaySlideComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import { Component } from '@angular/core';
import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { CurrentListOfSpeakersSlideData } from '../base/current-list-of-speakers-slide-data';
@Component({
selector: 'os-current-list-of-speakers-overlay-slide',
templateUrl: './current-list-of-speakers-overlay-slide.component.html',
styleUrls: ['./current-list-of-speakers-overlay-slide.component.scss']
})
export class CurrentListOfSpeakersOverlaySlideComponent extends BaseSlideComponent<CurrentListOfSpeakersSlideData> {
public constructor() {
super();
}
}

View File

@ -0,0 +1,13 @@
import { CurrentListOfSpeakersOverlaySlideModule } from './current-list-of-speakers-overlay-slide.module';
describe('CurrentListOfSpeakersOverlaySlideModule', () => {
let currentListOfSpeakersOverlaySlideModule: CurrentListOfSpeakersOverlaySlideModule;
beforeEach(() => {
currentListOfSpeakersOverlaySlideModule = new CurrentListOfSpeakersOverlaySlideModule();
});
it('should create an instance', () => {
expect(currentListOfSpeakersOverlaySlideModule).toBeTruthy();
});
});

View File

@ -0,0 +1,7 @@
import { NgModule } from '@angular/core';
import { makeSlideModule } from 'app/slides/base-slide-module';
import { CurrentListOfSpeakersOverlaySlideComponent } from './current-list-of-speakers-overlay-slide.component';
@NgModule(makeSlideModule(CurrentListOfSpeakersOverlaySlideComponent))
export class CurrentListOfSpeakersOverlaySlideModule {}

View File

@ -1,18 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { AgendaCurrentListOfSpeakersSlideData } from '../base/agenda-current-list-of-speakers-slide-data';
@Component({
selector: 'os-agenda-current-list-of-speakers-slide',
templateUrl: './agenda-current-list-of-speakers-slide.component.html',
styleUrls: ['./agenda-current-list-of-speakers-slide.component.scss']
})
export class AgendaCurrentListOfSpeakersSlideComponent extends BaseSlideComponent<AgendaCurrentListOfSpeakersSlideData>
implements OnInit {
public constructor() {
super();
}
public ngOnInit(): void {}
}

View File

@ -1,13 +0,0 @@
import { AgendaCurrentListOfSpeakersSlideModule } from './agenda-current-list-of-speakers-slide.module';
describe('AgendaCurrentListOfSpeakersModule', () => {
let agendaCurrentListOfSpeakersSlideModule: AgendaCurrentListOfSpeakersSlideModule;
beforeEach(() => {
agendaCurrentListOfSpeakersSlideModule = new AgendaCurrentListOfSpeakersSlideModule();
});
it('should create an instance', () => {
expect(agendaCurrentListOfSpeakersSlideModule).toBeTruthy();
});
});

View File

@ -1,7 +0,0 @@
import { NgModule } from '@angular/core';
import { makeSlideModule } from 'app/slides/base-slide-module';
import { AgendaCurrentListOfSpeakersSlideComponent } from './agenda-current-list-of-speakers-slide.component';
@NgModule(makeSlideModule(AgendaCurrentListOfSpeakersSlideComponent))
export class AgendaCurrentListOfSpeakersSlideModule {}

View File

@ -1,21 +1,21 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AgendaCurrentListOfSpeakersSlideComponent } from './agenda-current-list-of-speakers-slide.component'; import { CurrentListOfSpeakersSlideComponent } from './current-list-of-speakers-slide.component';
import { E2EImportsModule } from '../../../../e2e-imports.module'; import { E2EImportsModule } from '../../../../e2e-imports.module';
describe('CoreCountdownSlideComponent', () => { describe('CurrentListOfSpeakersSlideComponent', () => {
let component: AgendaCurrentListOfSpeakersSlideComponent; let component: CurrentListOfSpeakersSlideComponent;
let fixture: ComponentFixture<AgendaCurrentListOfSpeakersSlideComponent>; let fixture: ComponentFixture<CurrentListOfSpeakersSlideComponent>;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [E2EImportsModule], imports: [E2EImportsModule],
declarations: [AgendaCurrentListOfSpeakersSlideComponent] declarations: [CurrentListOfSpeakersSlideComponent]
}).compileComponents(); }).compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(AgendaCurrentListOfSpeakersSlideComponent); fixture = TestBed.createComponent(CurrentListOfSpeakersSlideComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@ -0,0 +1,18 @@
import { Component, OnInit } from '@angular/core';
import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { CurrentListOfSpeakersSlideData } from '../base/current-list-of-speakers-slide-data';
@Component({
selector: 'os-current-list-of-speakers-slide',
templateUrl: './current-list-of-speakers-slide.component.html',
styleUrls: ['./current-list-of-speakers-slide.component.scss']
})
export class CurrentListOfSpeakersSlideComponent extends BaseSlideComponent<CurrentListOfSpeakersSlideData>
implements OnInit {
public constructor() {
super();
}
public ngOnInit(): void {}
}

View File

@ -0,0 +1,13 @@
import { CurrentListOfSpeakersSlideModule } from './current-list-of-speakers-slide.module';
describe('CurrentListOfSpeakersSlideModule', () => {
let currentListOfSpeakersSlideModule: CurrentListOfSpeakersSlideModule;
beforeEach(() => {
currentListOfSpeakersSlideModule = new CurrentListOfSpeakersSlideModule();
});
it('should create an instance', () => {
expect(currentListOfSpeakersSlideModule).toBeTruthy();
});
});

View File

@ -0,0 +1,7 @@
import { NgModule } from '@angular/core';
import { makeSlideModule } from 'app/slides/base-slide-module';
import { CurrentListOfSpeakersSlideComponent } from './current-list-of-speakers-slide.component';
@NgModule(makeSlideModule(CurrentListOfSpeakersSlideComponent))
export class CurrentListOfSpeakersSlideModule {}

View File

@ -0,0 +1,13 @@
interface SlideSpeaker {
user: string;
marked: boolean;
}
export interface ListOfSpeakersSlideData {
waiting: SlideSpeaker[];
current: SlideSpeaker;
finished: SlideSpeaker[];
title_information: object;
content_object_collection: string;
item_number: string;
}

View File

@ -0,0 +1,29 @@
<div *ngIf="data">
<div class="slidetitle">
<h1 translate>List of speakers</h1>
<h2> {{ getTitle() }}</h2>
</div>
<!-- Last speakers -->
<div *ngIf="data.data.finished.length">
<div *ngFor="let speaker of data.data.finished" class="lastSpeakers">
{{ speaker.user }}
<mat-icon *ngIf="speaker.marked">star</mat-icon>
</div>
</div>
<!-- Current speaker -->
<div *ngIf="data.data.current" class="currentSpeaker">
<mat-icon>mic</mat-icon> {{ data.data.current.user }}
</div>
<!-- Next speakers -->
<div *ngIf="data.data.finished.length">
<ol class="nextSpeakers">
<li *ngFor="let speaker of data.data.waiting">
{{ speaker.user }}
<mat-icon *ngIf="speaker.marked">star</mat-icon>
</li>
</ol>
</div>
</div>

View File

@ -0,0 +1,16 @@
.lastSpeakers {
color: #9a9898;
margin-left: 30px;
}
.currentSpeaker {
font-weight: bold;
}
.nextSpeakers {
margin-left: 13px !important;
li {
line-height: 150%;
}
}

View File

@ -0,0 +1,26 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ListOfSpeakersSlideComponent } from './list-of-speakers-slide.component';
import { E2EImportsModule } from '../../../../e2e-imports.module';
describe('ListOfSpeakersSlideComponent', () => {
let component: ListOfSpeakersSlideComponent;
let fixture: ComponentFixture<ListOfSpeakersSlideComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
declarations: [ListOfSpeakersSlideComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ListOfSpeakersSlideComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,28 @@
import { Component } from '@angular/core';
import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { ListOfSpeakersSlideData } from './list-of-speakers-slide-data';
import { CollectionStringMapperService } from 'app/core/core-services/collectionStringMapper.service';
import { isBaseAgendaContentObjectRepository } from 'app/core/repositories/base-agenda-content-object-repository';
@Component({
selector: 'os-list-of-speakers-slide',
templateUrl: './list-of-speakers-slide.component.html',
styleUrls: ['./list-of-speakers-slide.component.scss']
})
export class ListOfSpeakersSlideComponent extends BaseSlideComponent<ListOfSpeakersSlideData> {
public constructor(private collectionStringMapperService: CollectionStringMapperService) {
super();
}
public getTitle(): string {
const numberPrefix = this.data.data.item_number ? `${this.data.data.item_number} · ` : '';
const repo = this.collectionStringMapperService.getRepository(this.data.data.content_object_collection);
if (isBaseAgendaContentObjectRepository(repo)) {
return numberPrefix + repo.getAgendaTitle(this.data.data.title_information);
} else {
throw new Error('The content object has no agenda based repository!');
}
}
}

View File

@ -0,0 +1,13 @@
import { ListOfSpeakersSlideModule } from './list-of-speakers-slide.module';
describe('ListOfSpeakersSlideModule', () => {
let listOfSpeakersSlideModule: ListOfSpeakersSlideModule;
beforeEach(() => {
listOfSpeakersSlideModule = new ListOfSpeakersSlideModule();
});
it('should create an instance', () => {
expect(listOfSpeakersSlideModule).toBeTruthy();
});
});

View File

@ -0,0 +1,7 @@
import { NgModule } from '@angular/core';
import { makeSlideModule } from 'app/slides/base-slide-module';
import { ListOfSpeakersSlideComponent } from './list-of-speakers-slide.component';
@NgModule(makeSlideModule(ListOfSpeakersSlideComponent))
export class ListOfSpeakersSlideModule {}

View File

@ -0,0 +1,4 @@
export interface TopicSlideData {
title: string;
text: string;
}

View File

@ -1,21 +1,21 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TopicsTopicSlideComponent } from './topics-topic-slide.component'; import { TopicSlideComponent } from './topic-slide.component';
import { E2EImportsModule } from 'e2e-imports.module'; import { E2EImportsModule } from 'e2e-imports.module';
describe('TopicsTopicSlideComponent', () => { describe('TopicSlideComponent', () => {
let component: TopicsTopicSlideComponent; let component: TopicSlideComponent;
let fixture: ComponentFixture<TopicsTopicSlideComponent>; let fixture: ComponentFixture<TopicSlideComponent>;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [E2EImportsModule], imports: [E2EImportsModule],
declarations: [TopicsTopicSlideComponent] declarations: [TopicSlideComponent]
}).compileComponents(); }).compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(TopicsTopicSlideComponent); fixture = TestBed.createComponent(TopicSlideComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@ -0,0 +1,14 @@
import { Component } from '@angular/core';
import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { TopicSlideData } from './topic-slide-data';
@Component({
selector: 'os-topic-slide',
templateUrl: './topic-slide.component.html',
styleUrls: ['./topic-slide.component.scss']
})
export class TopicSlideComponent extends BaseSlideComponent<TopicSlideData> {
public constructor() {
super();
}
}

View File

@ -0,0 +1,13 @@
import { TopicSlideModule } from './topic-slide.module';
describe('TopicSlideModule', () => {
let topicsTopicSlideModule: TopicSlideModule;
beforeEach(() => {
topicsTopicSlideModule = new TopicSlideModule();
});
it('should create an instance', () => {
expect(topicsTopicSlideModule).toBeTruthy();
});
});

View File

@ -0,0 +1,7 @@
import { NgModule } from '@angular/core';
import { makeSlideModule } from 'app/slides/base-slide-module';
import { TopicSlideComponent } from './topic-slide.component';
@NgModule(makeSlideModule(TopicSlideComponent))
export class TopicSlideModule {}

View File

@ -1,4 +0,0 @@
export interface TopicsTopicSlideData {
title: string;
text: string;
}

View File

@ -1,14 +0,0 @@
import { Component } from '@angular/core';
import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { TopicsTopicSlideData } from './topics-topic-slide-data';
@Component({
selector: 'os-topic-slide',
templateUrl: './topics-topic-slide.component.html',
styleUrls: ['./topics-topic-slide.component.scss']
})
export class TopicsTopicSlideComponent extends BaseSlideComponent<TopicsTopicSlideData> {
public constructor() {
super();
}
}

View File

@ -1,13 +0,0 @@
import { TopicsTopicSlideModule } from './topics-topic-slide.module';
describe('TopicsTopicSlideModule', () => {
let topicsTopicSlideModule: TopicsTopicSlideModule;
beforeEach(() => {
topicsTopicSlideModule = new TopicsTopicSlideModule();
});
it('should create an instance', () => {
expect(topicsTopicSlideModule).toBeTruthy();
});
});

View File

@ -1,7 +0,0 @@
import { NgModule } from '@angular/core';
import { makeSlideModule } from 'app/slides/base-slide-module';
import { TopicsTopicSlideComponent } from './topics-topic-slide.component';
@NgModule(makeSlideModule(TopicsTopicSlideComponent))
export class TopicsTopicSlideModule {}

View File

@ -1,4 +1,8 @@
import { TranslateService } from '@ngx-translate/core';
import { SlideDynamicConfiguration, Slide } from './slide-manifest'; import { SlideDynamicConfiguration, Slide } from './slide-manifest';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ProjectorElement } from 'app/shared/models/core/projector';
export const allSlidesDynamicConfiguration: (SlideDynamicConfiguration & Slide)[] = [ export const allSlidesDynamicConfiguration: (SlideDynamicConfiguration & Slide)[] = [
{ {
@ -31,6 +35,23 @@ export const allSlidesDynamicConfiguration: (SlideDynamicConfiguration & Slide)[
scaleable: false, scaleable: false,
scrollable: false scrollable: false
}, },
{
slide: 'agenda/list-of-speakers',
scaleable: true,
scrollable: true,
getSlideTitle: (
element: ProjectorElement,
translate: TranslateService,
viewModelStore: ViewModelStoreService
) => {
const item = viewModelStore.get('agenda/item', element.id);
if (item) {
const title = translate.instant('List of speakers for');
return title + ' ' + item.getTitle();
}
return translate.instant('List of speakers');
}
},
{ {
slide: 'agenda/current-list-of-speakers', slide: 'agenda/current-list-of-speakers',
scaleable: true, scaleable: true,

View File

@ -11,7 +11,7 @@ export const allSlides: SlideManifest[] = [
{ {
slide: 'topics/topic', slide: 'topics/topic',
path: 'topics/topic', path: 'topics/topic',
loadChildren: './slides/agenda/topic/topics-topic-slide.module#TopicsTopicSlideModule', loadChildren: './slides/agenda/topic/topic-slide.module#TopicSlideModule',
verboseName: 'Topic', verboseName: 'Topic',
elementIdentifiers: ['name', 'id'], elementIdentifiers: ['name', 'id'],
canBeMappedToModel: true canBeMappedToModel: true
@ -60,7 +60,7 @@ export const allSlides: SlideManifest[] = [
slide: 'agenda/current-list-of-speakers', slide: 'agenda/current-list-of-speakers',
path: 'agenda/current-list-of-speakers', path: 'agenda/current-list-of-speakers',
loadChildren: loadChildren:
'./slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.module#AgendaCurrentListOfSpeakersSlideModule', './slides/agenda/current-list-of-speakers/current-list-of-speakers-slide.module#CurrentListOfSpeakersSlideModule',
verboseName: 'Current list of speakers', verboseName: 'Current list of speakers',
elementIdentifiers: ['name', 'id'], elementIdentifiers: ['name', 'id'],
canBeMappedToModel: false canBeMappedToModel: false
@ -69,11 +69,19 @@ export const allSlides: SlideManifest[] = [
slide: 'agenda/current-list-of-speakers-overlay', slide: 'agenda/current-list-of-speakers-overlay',
path: 'agenda/current-list-of-speakers-overlay', path: 'agenda/current-list-of-speakers-overlay',
loadChildren: loadChildren:
'./slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.module#AgendaCurrentListOfSpeakersOverlaySlideModule', './slides/agenda/current-list-of-speakers-overlay/current-list-of-speakers-overlay-slide.module#CurrentListOfSpeakersOverlaySlideModule',
verboseName: 'Current list of speakers overlay', verboseName: 'Current list of speakers overlay',
elementIdentifiers: ['name', 'id'], elementIdentifiers: ['name', 'id'],
canBeMappedToModel: false canBeMappedToModel: false
}, },
{
slide: 'agenda/list-of-speakers',
path: 'agenda/list-of-speakers',
loadChildren: './slides/agenda/list-of-speakers/list-of-speakers-slide.module#ListOfSpeakersSlideModule',
verboseName: 'List of speakers',
elementIdentifiers: ['name', 'id'],
canBeMappedToModel: false
},
{ {
slide: 'assignments/assignment', slide: 'assignments/assignment',
path: 'assignments/assignment', path: 'assignments/assignment',

View File

@ -1,8 +1,9 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { BaseSlideComponent } from 'app/slides/base-slide-component'; import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { MotionsMotionSlideData, MotionsMotionSlideDataAmendment } from './motions-motion-slide-data'; import { MotionsMotionSlideData, MotionsMotionSlideDataAmendment } from './motions-motion-slide-data';
import { ChangeRecoMode, LineNumberingMode } from '../../../site/motions/models/view-motion'; import { ChangeRecoMode, LineNumberingMode } from '../../../site/motions/models/view-motion';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { DiffLinesInParagraph, DiffService, LineRange } from '../../../core/ui-services/diff.service'; import { DiffLinesInParagraph, DiffService, LineRange } from '../../../core/ui-services/diff.service';
import { LinenumberingService } from '../../../core/ui-services/linenumbering.service'; import { LinenumberingService } from '../../../core/ui-services/linenumbering.service';
import { ViewUnifiedChange } from '../../../shared/models/motions/view-unified-change'; import { ViewUnifiedChange } from '../../../shared/models/motions/view-unified-change';

View File

@ -1,5 +1,7 @@
import { InjectionToken } from '@angular/core'; import { InjectionToken } from '@angular/core';
import { IdentifiableProjectorElement, ProjectorElement } from 'app/shared/models/core/projector'; import { IdentifiableProjectorElement, ProjectorElement } from 'app/shared/models/core/projector';
import { TranslateService } from '@ngx-translate/core';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
type BooleanOrFunction = boolean | ((element: ProjectorElement) => boolean); type BooleanOrFunction = boolean | ((element: ProjectorElement) => boolean);
@ -20,6 +22,12 @@ export interface SlideDynamicConfiguration {
* Should this slide be scaleable? * Should this slide be scaleable?
*/ */
scaleable: BooleanOrFunction; scaleable: BooleanOrFunction;
getSlideTitle?: (
element: ProjectorElement,
translate: TranslateService,
viewModelStore: ViewModelStoreService
) => string;
} }
/** /**

View File

@ -617,7 +617,7 @@ button.mat-menu-item.selected {
margin-bottom: 40px; margin-bottom: 40px;
h1 { h1 {
font-size: 2.25em; font-size: 2em;
line-height: 1.1em; line-height: 1.1em;
margin-bottom: 0; margin-bottom: 0;
padding-bottom: 0; padding-bottom: 0;
@ -625,7 +625,7 @@ button.mat-menu-item.selected {
h2 { h2 {
color: #9a9898; color: #9a9898;
margin-top: 10px; margin-top: 10px;
margin-bottom: 0px; margin-bottom: 5px;
font-size: 28px; font-size: 28px;
font-weight: normal; font-weight: normal;
display: block; display: block;

View File

@ -57,7 +57,13 @@ class ItemAccessPermissions(BaseAccessPermissions):
# so that list of speakers is provided regardless. Hidden items can only be seen by managers. # so that list of speakers is provided regardless. Hidden items can only be seen by managers.
# We know that full_data has at least one entry which can be used to parse the keys. # We know that full_data has at least one entry which can be used to parse the keys.
blocked_keys_internal_hidden_case = set(full_data[0].keys()) - set( blocked_keys_internal_hidden_case = set(full_data[0].keys()) - set(
("id", "title", "speakers", "speaker_list_closed", "content_object") (
"id",
"title_information",
"speakers",
"speaker_list_closed",
"content_object",
)
) )
# In non internal case managers see everything and non managers see # In non internal case managers see everything and non managers see

View File

@ -283,32 +283,16 @@ class Item(RESTModelMixin, models.Model):
) )
unique_together = ("content_type", "object_id") unique_together = ("content_type", "object_id")
def __str__(self):
return self.title
@property @property
def title(self): def title_information(self):
""" """
Return get_agenda_title() from the content_object. Return get_agenda_title_information() from the content_object.
""" """
try: try:
return self.content_object.get_agenda_title() return self.content_object.get_agenda_title_information()
except AttributeError: except AttributeError:
raise NotImplementedError( raise NotImplementedError(
"You have to provide a get_agenda_title " "You have to provide a get_agenda_title_information "
"method on your related model."
)
@property
def title_with_type(self):
"""
Return get_agenda_title_with_type() from the content_object.
"""
try:
return self.content_object.get_agenda_title_with_type()
except AttributeError:
raise NotImplementedError(
"You have to provide a get_agenda_title_with_type "
"method on your related model." "method on your related model."
) )

View File

@ -1,9 +1,11 @@
from collections import defaultdict from collections import defaultdict
from typing import Any, Dict, List, Tuple from typing import Any, Dict, List, Tuple
from ..users.projector import get_user_name
from ..utils.projector import ( from ..utils.projector import (
AllData, AllData,
ProjectorElementException, ProjectorElementException,
get_config,
register_projector_slide, register_projector_slide,
) )
@ -75,19 +77,56 @@ def list_of_speakers_slide(
Returns all usernames, that are on the list of speaker of a slide. Returns all usernames, that are on the list of speaker of a slide.
""" """
item_id = element.get("id") or 0 # item_id 0 means current_list_of_speakers item_id = element.get("id")
# TODO: handle item_id == 0 if item_id is None:
raise ProjectorElementException("id is required for list of speakers slide")
try: try:
item = all_data["agenda/item"][item_id] item = all_data["agenda/item"][item_id]
except KeyError: except KeyError:
raise ProjectorElementException(f"Item {item_id} does not exist") raise ProjectorElementException(f"Item {item_id} does not exist")
user_ids = [] # Partition speaker objects to waiting, current and finished
speakers_waiting = []
speakers_finished = []
current_speaker = None
for speaker in item["speakers"]: for speaker in item["speakers"]:
user_ids.append(speaker["user"]) user = get_user_name(all_data, speaker["user_id"])
return {"user_ids": user_ids} formatted_speaker = {
"user": user,
"marked": speaker["marked"],
"weight": speaker["weight"],
"end_time": speaker["end_time"],
}
if speaker["begin_time"] is None and speaker["end_time"] is None:
speakers_waiting.append(formatted_speaker)
elif speaker["begin_time"] is not None and speaker["end_time"] is None:
current_speaker = formatted_speaker
else:
speakers_finished.append(formatted_speaker)
# sort speakers
speakers_waiting = sorted(speakers_waiting, key=lambda s: s["weight"])
speakers_finished = sorted(speakers_finished, key=lambda s: s["end_time"])
number_of_last_speakers = get_config(all_data, "agenda_show_last_speakers")
if number_of_last_speakers == 0:
speakers_finished = []
else:
speakers_finished = speakers_finished[
-number_of_last_speakers:
] # Take the last speakers
return {
"waiting": speakers_waiting,
"current": current_speaker,
"finished": speakers_finished,
"content_object_collection": item["content_object"]["collection"],
"title_information": item["title_information"],
"item_number": item["item_number"],
}
def current_list_of_speakers_slide( def current_list_of_speakers_slide(

View File

@ -1,4 +1,4 @@
from openslides.utils.rest_api import ModelSerializer, RelatedField from openslides.utils.rest_api import JSONField, ModelSerializer, RelatedField
from .models import Item, Speaker from .models import Item, Speaker
@ -42,13 +42,14 @@ class ItemSerializer(ModelSerializer):
content_object = RelatedItemRelatedField(read_only=True) content_object = RelatedItemRelatedField(read_only=True)
speakers = SpeakerSerializer(many=True, read_only=True) speakers = SpeakerSerializer(many=True, read_only=True)
title_information = JSONField(read_only=True)
class Meta: class Meta:
model = Item model = Item
fields = ( fields = (
"id", "id",
"item_number", "item_number",
"title", "title_information",
"title_with_type",
"comment", "comment",
"closed", "closed",
"type", "type",

View File

@ -16,7 +16,7 @@ def listen_to_related_object_post_save(sender, instance, created, **kwargs):
Do not run caching and autoupdate if the instance has a key Do not run caching and autoupdate if the instance has a key
skip_autoupdate in the agenda_item_update_information container. skip_autoupdate in the agenda_item_update_information container.
""" """
if hasattr(instance, "get_agenda_title"): if hasattr(instance, "get_agenda_title_information"):
if created: if created:
attrs = {} attrs = {}
for attr in ("type", "parent_id", "comment", "duration", "weight"): for attr in ("type", "parent_id", "comment", "duration", "weight"):

View File

@ -318,18 +318,8 @@ class Assignment(RESTModelMixin, models.Model):
""" """
agenda_item_update_information: Dict[str, Any] = {} agenda_item_update_information: Dict[str, Any] = {}
def get_agenda_title(self): def get_agenda_title_information(self):
""" return {"title": self.title}
Returns the title for the agenda.
"""
return str(self)
def get_agenda_title_with_type(self):
"""
Return a title for the agenda with the appended assignment verbose name.
Note: It has to be the same return value like in JavaScript.
"""
return f"{self.get_agenda_title()} (self._meta.verbose_name)"
@property @property
def agenda_item(self): def agenda_item(self):

View File

@ -520,32 +520,8 @@ class Motion(RESTModelMixin, models.Model):
""" """
agenda_item_update_information: Dict[str, Any] = {} agenda_item_update_information: Dict[str, Any] = {}
def get_agenda_title(self): def get_agenda_title_information(self):
""" return {"title": self.title, "identifier": self.identifier}
Return the title string for the agenda.
If the identifier is given, the title consists of the motion verbose name
and the identifier.
Note: It has to be the same return value like in JavaScript.
"""
if self.identifier:
title = f"{self._meta.verbose_name} {self.identifier}"
else:
title = self.title
return title
def get_agenda_title_with_type(self):
"""
Return a title for the agenda with the type or the modified title if the
identifier is set..
Note: It has to be the same return value like in JavaScript.
"""
if self.identifier:
title = f"{self._meta.verbose_name} {self.identifier}"
else:
title = f"{self.title} ({self._meta.verbose_name})"
return title
@property @property
def agenda_item(self): def agenda_item(self):
@ -908,11 +884,8 @@ class MotionBlock(RESTModelMixin, models.Model):
""" """
return self.agenda_item.pk return self.agenda_item.pk
def get_agenda_title(self): def get_agenda_title_information(self):
return self.title return {"title": self.title}
def get_agenda_title_with_type(self):
return f"{self.get_agenda_title()} ({self._meta.verbose_name})"
class MotionLog(RESTModelMixin, models.Model): class MotionLog(RESTModelMixin, models.Model):

View File

@ -67,14 +67,5 @@ class Topic(RESTModelMixin, models.Model):
""" """
return self.agenda_item.pk return self.agenda_item.pk
def get_agenda_title(self): def get_agenda_title_information(self):
""" return {"title": self.title}
Returns the title for the agenda.
"""
return self.title
def get_agenda_title_with_type(self):
"""
Returns the agenda title. Topicy should not get a type postfix.
"""
return self.get_agenda_title()

View File

@ -25,19 +25,18 @@ def user_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]:
if user_id is None: if user_id is None:
raise ProjectorElementException("id is required for user slide") raise ProjectorElementException("id is required for user slide")
try: return {"user": get_user_name(all_data, user_id)}
user = all_data["users/user"][user_id]
except KeyError:
raise ProjectorElementException(f"user with id {user_id} does not exist")
return {"user": get_user_name(all_data, user["id"])}
def get_user_name(all_data: AllData, user_id: int) -> str: def get_user_name(all_data: AllData, user_id: int) -> str:
""" """
Returns the short name for an user_id. Returns the short name for an user_id.
""" """
try:
user = all_data["users/user"][user_id] user = all_data["users/user"][user_id]
except KeyError:
raise ProjectorElementException(f"user with id {user_id} does not exist")
name_parts: List[str] = [] name_parts: List[str] = []
for name_part in ("title", "first_name", "last_name"): for name_part in ("title", "first_name", "last_name"):
if user[name_part]: if user[name_part]:

View File

@ -77,7 +77,13 @@ class RetrieveItem(TestCase):
self.assertEqual( self.assertEqual(
sorted(response.data.keys()), sorted(response.data.keys()),
sorted( sorted(
("id", "title", "speakers", "speaker_list_closed", "content_object") (
"id",
"title_information",
"speakers",
"speaker_list_closed",
"content_object",
)
), ),
) )
forbidden_keys = ( forbidden_keys = (

View File

@ -8,15 +8,17 @@ class TestItemTitle(TestCase):
@patch("openslides.agenda.models.Item.content_object") @patch("openslides.agenda.models.Item.content_object")
def test_title_from_content_object(self, content_object): def test_title_from_content_object(self, content_object):
item = Item() item = Item()
content_object.get_agenda_title.return_value = "related_title" content_object.get_agenda_title_information.return_value = {
"attr": "related_title"
}
self.assertEqual(item.title, "related_title") self.assertEqual(item.title_information, {"attr": "related_title"})
@patch("openslides.agenda.models.Item.content_object") @patch("openslides.agenda.models.Item.content_object")
def test_title_invalid_related(self, content_object): def test_title_invalid_related(self, content_object):
item = Item() item = Item()
content_object.get_agenda_title.return_value = "related_title" content_object.get_agenda_title_information.return_value = "related_title"
del content_object.get_agenda_title del content_object.get_agenda_title_information
with self.assertRaises(NotImplementedError): with self.assertRaises(NotImplementedError):
item.title item.title_information