List of speakers slide

This commit is contained in:
FinnStutzenstein 2019-02-15 12:17:08 +01:00
parent 0975de7432
commit 7ef36e93c6
80 changed files with 640 additions and 358 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 { ViewModelStoreService } from './view-model-store.service';
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
@ -39,7 +40,8 @@ export class ProjectorService {
private DS: DataStoreService,
private http: HttpService,
private slideManager: SlideManager,
private viewModelStore: ViewModelStoreService
private viewModelStore: ViewModelStoreService,
private translate: TranslateService
) {}
/**
@ -273,6 +275,24 @@ export class ProjectorService {
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
* 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 { ViewUser } from 'app/site/users/models/view-user';
import { TranslateService } from '@ngx-translate/core';
import { BaseAgendaContentObjectRepository } from '../base-agenda-content-object-repository';
/**
* Repository service for users
@ -77,6 +78,28 @@ export class ItemRepositoryService extends BaseRepository<ViewItem, Item> {
viewItem.getVerboseName = (plural: boolean = false) => {
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;
}

View File

@ -1,7 +1,8 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
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 { Item } from 'app/shared/models/agenda/item';
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 { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
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
@ -21,7 +22,7 @@ import { TranslateService } from '@ngx-translate/core';
@Injectable({
providedIn: 'root'
})
export class TopicRepositoryService extends BaseRepository<ViewTopic, Topic> {
export class TopicRepositoryService extends BaseAgendaContentObjectRepository<ViewTopic, Topic> {
/**
* Constructor calls the parent constructor
*
@ -39,6 +40,19 @@ export class TopicRepositoryService extends BaseRepository<ViewTopic, Topic> {
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
*
@ -49,9 +63,9 @@ export class TopicRepositoryService extends BaseRepository<ViewTopic, Topic> {
const attachments = this.viewModelStoreService.getMany(ViewMediafile, topic.attachments_id);
const item = this.viewModelStoreService.get(ViewItem, topic.agenda_item_id);
const viewTopic = new ViewTopic(topic, attachments, item);
viewTopic.getVerboseName = (plural: boolean = false) => {
return this.translate.instant(plural ? 'Topics' : 'Topic');
};
viewTopic.getVerboseName = this.getVerboseName;
viewTopic.getAgendaTitle = () => this.getAgendaTitle(viewTopic);
viewTopic.getAgendaTitleWithType = () => this.getAgendaTitle(viewTopic);
return viewTopic;
}

View File

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

View File

@ -2,8 +2,8 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
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 { DataSendService } from 'app/core/core-services/data-send.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 { Item } from 'app/shared/models/agenda/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
@ -25,7 +25,7 @@ import { TranslateService } from '@ngx-translate/core';
@Injectable({
providedIn: 'root'
})
export class MotionBlockRepositoryService extends BaseRepository<ViewMotionBlock, MotionBlock> {
export class MotionBlockRepositoryService extends BaseAgendaContentObjectRepository<ViewMotionBlock, MotionBlock> {
/**
* Constructor for the motion block repository
*
@ -47,6 +47,18 @@ export class MotionBlockRepositoryService extends BaseRepository<ViewMotionBlock
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
*
@ -56,9 +68,9 @@ export class MotionBlockRepositoryService extends BaseRepository<ViewMotionBlock
protected createViewModel(block: MotionBlock): ViewMotionBlock {
const item = this.viewModelStoreService.get(ViewItem, block.agenda_item_id);
const viewMotionBlock = new ViewMotionBlock(block, item);
viewMotionBlock.getVerboseName = (plural: boolean = false) => {
return this.translate.instant(plural ? 'Motion blocks' : 'Motion block');
};
viewMotionBlock.getVerboseName = this.getVerboseName;
viewMotionBlock.getAgendaTitle = () => this.getAgendaTitle(viewMotionBlock);
viewMotionBlock.getAgendaTitleWithType = () => this.getAgendaTitleWithType(viewMotionBlock);
return viewMotionBlock;
}

View File

@ -4,7 +4,6 @@ import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { tap, map } from 'rxjs/operators';
import { BaseRepository } from '../base-repository';
import { Category } from 'app/shared/models/motions/category';
import { ChangeRecoMode, ViewMotion } from 'app/site/motions/models/view-motion';
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 { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile';
import { ViewTag } from 'app/site/tags/models/view-tag';
import { BaseAgendaContentObjectRepository } from '../base-agenda-content-object-repository';
/**
* Repository Services for motions (and potentially categories)
@ -54,7 +54,7 @@ import { ViewTag } from 'app/site/tags/models/view-tag';
@Injectable({
providedIn: 'root'
})
export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion> {
export class MotionRepositoryService extends BaseAgendaContentObjectRepository<ViewMotion, Motion> {
/**
* 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.
*
@ -127,26 +149,10 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
tags,
parent
);
viewMotion.getVerboseName = (plural: boolean = false) => {
return this.translate.instant(plural ? 'Motions' : 'Motion');
};
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.getVerboseName = this.getVerboseName;
viewMotion.getAgendaTitle = () => this.getAgendaTitle(viewMotion);
viewMotion.getProjectorTitle = viewMotion.getAgendaTitle;
viewMotion.getAgendaTitleWithType = () => {
// 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() + ')';
}
};
viewMotion.getAgendaTitleWithType = () => this.getAgendaTitleWithType(viewMotion);
return viewMotion;
}

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@
<span *ngIf="currentListOfSpeakers">Current list of speakers</span>
</h2>
</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>
</div>
</os-head-bar>
@ -131,6 +131,8 @@
</mat-card>
<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()">
<mat-icon>mic</mat-icon>
<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 { Speaker, SpeakerState } from 'app/shared/models/agenda/speaker';
import { BaseAgendaViewModel, isAgendaBaseModel } from 'app/site/base/base-agenda-view-model';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
export class ViewItem extends BaseViewModel {
public static COLLECTIONSTRING = Item.COLLECTIONSTRING;
@ -111,6 +112,19 @@ export class ViewItem extends BaseViewModel {
* This is set by the repository
*/
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) {
super(Item.COLLECTIONSTRING);
@ -118,30 +132,6 @@ export class ViewItem extends BaseViewModel {
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 {
if (
update &&

View File

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

View File

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

View File

@ -8,7 +8,7 @@ export function isProjectorElementBuildDeskriptor(obj: any): obj is ProjectorEle
!!deskriptor &&
deskriptor.slideOptions !== 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
*/
getTitle(): string;
getDialogTitle(): string;
}
export function isProjectable(obj: any): obj is Projectable {

View File

@ -97,7 +97,7 @@ export class ViewMediafile extends BaseProjectableViewModel implements Searchabl
}),
slideOptions: [],
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
*/
public getVerboseName;
public getAgendaTitle;
public getAgendaTitleWithType;
public constructor(motionBlock: MotionBlock, agendaItem?: ViewItem) {
super(MotionBlock.COLLECTIONSTRING);

View File

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

View File

@ -56,7 +56,7 @@
<div class="queue">
<h5 translate>History</h5>
<p *ngFor="let elements of projector?.elements_history">
{{ getElementDescription(elements[0]) }}
{{ getSlideTitle(elements[0]) }}
</p>
</div>
@ -70,7 +70,7 @@
<button type="button" mat-icon-button (click)="unprojectCurrent(element)">
<mat-icon>videocam</mat-icon>
</button>
{{ getElementDescription(element) }}
{{ getSlideTitle(element) }}
</mat-list-item>
</mat-list>
</div>
@ -142,7 +142,7 @@
<mat-icon>drag_indicator</mat-icon>
</div>
<div class="name">
{{ i+1 }}.&nbsp;<span>{{ getElementDescription(element) }}</span>
{{ i+1 }}.&nbsp;<span>{{ getSlideTitle(element) }}</span>
</div>
<div class="button-right">
<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);
}
public getElementDescription(element: ProjectorElement): string {
if (this.slideManager.canSlideBeMappedToModel(element.name)) {
const idElement = this.slideManager.getIdentifialbeProjectorElement(element);
const viewModel = this.projectorService.getViewModelFromProjectorElement(idElement);
if (viewModel) {
return viewModel.getProjectorTitle();
}
}
return this.slideManager.getSlideVerboseName(element.name);
public getSlideTitle(element: ProjectorElement): string {
return this.projectorService.getSlideTitle(element);
}
public isProjected(obj: Projectable): boolean {

View File

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

View File

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

View File

@ -195,7 +195,7 @@ export class ViewUser extends BaseProjectableViewModel implements Searchable {
}),
slideOptions: [],
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 { AgendaCurrentListOfSpeakersSlideComponent } from './agenda-current-list-of-speakers-slide.component';
import { CurrentListOfSpeakersSlideComponent } from './current-list-of-speakers-slide.component';
import { E2EImportsModule } from '../../../../e2e-imports.module';
describe('CoreCountdownSlideComponent', () => {
let component: AgendaCurrentListOfSpeakersSlideComponent;
let fixture: ComponentFixture<AgendaCurrentListOfSpeakersSlideComponent>;
describe('CurrentListOfSpeakersSlideComponent', () => {
let component: CurrentListOfSpeakersSlideComponent;
let fixture: ComponentFixture<CurrentListOfSpeakersSlideComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
declarations: [AgendaCurrentListOfSpeakersSlideComponent]
declarations: [CurrentListOfSpeakersSlideComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AgendaCurrentListOfSpeakersSlideComponent);
fixture = TestBed.createComponent(CurrentListOfSpeakersSlideComponent);
component = fixture.componentInstance;
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,22 @@
<div *ngIf="data">
<h1 translate>List of speakers</h1>
<h3>{{ getTitle() }}</h3>
<div *ngIf="data.data.finished.length">
<span translate>Finished</span>:
<div *ngFor="let speaker of data.data.finished">
{{ speaker.user }}
</div>
</div>
<div *ngIf="data.data.current">
<span translate>Current</span>: {{ data.data.current.user }}
</div>
<div *ngIf="data.data.finished.length">
<span translate>Waiting</span>:
<div *ngFor="let speaker of data.data.waiting">
{{ speaker.user }} {{ speaker.marked ? 'marked' : '' }}
</div>
</div>
</div>

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 { TopicsTopicSlideComponent } from './topics-topic-slide.component';
import { TopicSlideComponent } from './topic-slide.component';
import { E2EImportsModule } from 'e2e-imports.module';
describe('TopicsTopicSlideComponent', () => {
let component: TopicsTopicSlideComponent;
let fixture: ComponentFixture<TopicsTopicSlideComponent>;
describe('TopicSlideComponent', () => {
let component: TopicSlideComponent;
let fixture: ComponentFixture<TopicSlideComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
declarations: [TopicsTopicSlideComponent]
declarations: [TopicSlideComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TopicsTopicSlideComponent);
fixture = TestBed.createComponent(TopicSlideComponent);
component = fixture.componentInstance;
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 { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { ProjectorElement } from 'app/shared/models/core/projector';
export const allSlidesDynamicConfiguration: (SlideDynamicConfiguration & Slide)[] = [
{
@ -31,6 +35,23 @@ export const allSlidesDynamicConfiguration: (SlideDynamicConfiguration & Slide)[
scaleable: 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',
scaleable: true,

View File

@ -11,7 +11,7 @@ export const allSlides: SlideManifest[] = [
{
slide: 'topics/topic',
path: 'topics/topic',
loadChildren: './slides/agenda/topic/topics-topic-slide.module#TopicsTopicSlideModule',
loadChildren: './slides/agenda/topic/topic-slide.module#TopicSlideModule',
verboseName: 'Topic',
elementIdentifiers: ['name', 'id'],
canBeMappedToModel: true
@ -60,7 +60,7 @@ export const allSlides: SlideManifest[] = [
slide: 'agenda/current-list-of-speakers',
path: 'agenda/current-list-of-speakers',
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',
elementIdentifiers: ['name', 'id'],
canBeMappedToModel: false
@ -69,11 +69,19 @@ export const allSlides: SlideManifest[] = [
slide: 'agenda/current-list-of-speakers-overlay',
path: 'agenda/current-list-of-speakers-overlay',
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',
elementIdentifiers: ['name', 'id'],
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',
path: 'assignments/assignment',

View File

@ -1,8 +1,9 @@
import { Component, Input } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { MotionsMotionSlideData, MotionsMotionSlideDataAmendment } from './motions-motion-slide-data';
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 { LinenumberingService } from '../../../core/ui-services/linenumbering.service';
import { ViewUnifiedChange } from '../../../shared/models/motions/view-unified-change';

View File

@ -1,5 +1,7 @@
import { InjectionToken } from '@angular/core';
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);
@ -20,6 +22,12 @@ export interface SlideDynamicConfiguration {
* Should this slide be scaleable?
*/
scaleable: BooleanOrFunction;
getSlideTitle?: (
element: ProjectorElement,
translate: TranslateService,
viewModelStore: ViewModelStoreService
) => string;
}
/**

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.
# 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(
("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

View File

@ -283,32 +283,16 @@ class Item(RESTModelMixin, models.Model):
)
unique_together = ("content_type", "object_id")
def __str__(self):
return self.title
@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:
return self.content_object.get_agenda_title()
return self.content_object.get_agenda_title_information()
except AttributeError:
raise NotImplementedError(
"You have to provide a get_agenda_title "
"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 "
"You have to provide a get_agenda_title_information "
"method on your related model."
)

View File

@ -1,9 +1,11 @@
from collections import defaultdict
from typing import Any, Dict, List, Tuple
from ..users.projector import get_user_name
from ..utils.projector import (
AllData,
ProjectorElementException,
get_config,
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.
"""
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:
item = all_data["agenda/item"][item_id]
except KeyError:
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"]:
user_ids.append(speaker["user"])
return {"user_ids": user_ids}
user = get_user_name(all_data, speaker["user_id"])
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(

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
@ -42,13 +42,14 @@ class ItemSerializer(ModelSerializer):
content_object = RelatedItemRelatedField(read_only=True)
speakers = SpeakerSerializer(many=True, read_only=True)
title_information = JSONField(read_only=True)
class Meta:
model = Item
fields = (
"id",
"item_number",
"title",
"title_with_type",
"title_information",
"comment",
"closed",
"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
skip_autoupdate in the agenda_item_update_information container.
"""
if hasattr(instance, "get_agenda_title"):
if hasattr(instance, "get_agenda_title_information"):
if created:
attrs = {}
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] = {}
def get_agenda_title(self):
"""
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)"
def get_agenda_title_information(self):
return {"title": self.title}
@property
def agenda_item(self):

View File

@ -520,32 +520,8 @@ class Motion(RESTModelMixin, models.Model):
"""
agenda_item_update_information: Dict[str, Any] = {}
def get_agenda_title(self):
"""
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
def get_agenda_title_information(self):
return {"title": self.title, "identifier": self.identifier}
@property
def agenda_item(self):
@ -908,11 +884,8 @@ class MotionBlock(RESTModelMixin, models.Model):
"""
return self.agenda_item.pk
def get_agenda_title(self):
return self.title
def get_agenda_title_with_type(self):
return f"{self.get_agenda_title()} ({self._meta.verbose_name})"
def get_agenda_title_information(self):
return {"title": self.title}
class MotionLog(RESTModelMixin, models.Model):

View File

@ -67,14 +67,5 @@ class Topic(RESTModelMixin, models.Model):
"""
return self.agenda_item.pk
def get_agenda_title(self):
"""
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()
def get_agenda_title_information(self):
return {"title": self.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:
raise ProjectorElementException("id is required for user slide")
try:
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"])}
return {"user": get_user_name(all_data, user_id)}
def get_user_name(all_data: AllData, user_id: int) -> str:
"""
Returns the short name for an user_id.
"""
try:
user = all_data["users/user"][user_id]
except KeyError:
raise ProjectorElementException(f"user with id {user_id} does not exist")
name_parts: List[str] = []
for name_part in ("title", "first_name", "last_name"):
if user[name_part]:

View File

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

View File

@ -8,15 +8,17 @@ class TestItemTitle(TestCase):
@patch("openslides.agenda.models.Item.content_object")
def test_title_from_content_object(self, content_object):
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")
def test_title_invalid_related(self, content_object):
item = Item()
content_object.get_agenda_title.return_value = "related_title"
del content_object.get_agenda_title
content_object.get_agenda_title_information.return_value = "related_title"
del content_object.get_agenda_title_information
with self.assertRaises(NotImplementedError):
item.title
item.title_information