Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement list of speakers slides #30

Merged
merged 9 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pkg/models/speaker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package models

func (m *Speaker) IsCurrent() bool {
return m.BeginTime != nil && *m.BeginTime != 0 && (m.EndTime == nil || *m.EndTime == 0)
}
10 changes: 5 additions & 5 deletions pkg/projector/slide/assignment.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ func AssignmentSlideHandler(ctx context.Context, req *projectionRequest) (<-chan

for {
select {
case <-ctx.Done():
assignmentSub.Unsubscribe()
close(content)
return
case <-assignmentSub.Channel:
case <-ctx.Done():
assignmentSub.Unsubscribe()
close(content)
return
case <-assignmentSub.Channel:
content <- getAssignmentSlideContent(&assignment)
}
}
Expand Down
88 changes: 62 additions & 26 deletions pkg/projector/slide/current_list_of_speakers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"html/template"
"reflect"
"sort"
"strconv"
"strings"

"github.com/OpenSlides/openslides-projector-service/pkg/datastore"
Expand Down Expand Up @@ -37,15 +36,16 @@ func CurrentListOfSpeakersSlideHandler(ctx context.Context, req *projectionReque

var los models.ListOfSpeakers
losQ := datastore.Collection(req.DB, &models.ListOfSpeakers{}).With("speaker_ids", nil)
speakersQ := losQ.GetSubquery("speaker_ids")
meetingUsersQ := speakersQ.With("meeting_user_id", nil)
meetingUsersQ.With("user_id", nil)
meetingUsersQ.With("structure_level_ids", nil)

losSub, err := losQ.SubscribeOne(&los)
if err != nil {
return nil, fmt.Errorf("failed to subscribe list of speakers: %w", err)
}

speakersQ := losQ.GetSubquery("speaker_ids")
meetingUsersQ := speakersQ.With("meeting_user_id", nil)
meetingUsersQ.With("user_id", nil)

stable := false
if projection.Stable != nil {
stable = *projection.Stable
Expand All @@ -54,6 +54,12 @@ func CurrentListOfSpeakersSlideHandler(ctx context.Context, req *projectionReque
go func() {
for {
select {
case <-ctx.Done():
refProjectorSub.Unsubscribe()
projectorSub.Unsubscribe()
losSub.Unsubscribe()
close(content)
return
case <-refProjectorSub.Channel:
if referenceProjectorId > 0 {
projectorQ.SetIds(referenceProjectorId)
Expand Down Expand Up @@ -83,8 +89,9 @@ func CurrentListOfSpeakersSlideHandler(ctx context.Context, req *projectionReque
}
case <-losSub.Channel:
if los.ID != 0 {
println("send new", len(los.SpeakerIDs))
content <- getCurrentListOfSpeakersSlideContent(&los, stable)
} else {
content <- ""
}
}
}
Expand All @@ -101,45 +108,74 @@ func getCurrentListOfSpeakersSlideContent(los *models.ListOfSpeakers, overlay bo
}

type speakerListItem struct {
Number int
Name string
Weight int
}
speakers := []speakerListItem{}
waitingSpeakers := []speakerListItem{}
interposedQuestions := []speakerListItem{}
var currentSpeaker *speakerListItem
var currentInterposedQuestion *speakerListItem
for _, speaker := range los.Speakers() {
nameParts := []string{}
if firstName := speaker.MeetingUser().User().FirstName; firstName != nil {
nameParts = append(nameParts, *firstName)
}
if lastName := speaker.MeetingUser().User().LastName; lastName != nil {
nameParts = append(nameParts, *lastName)
}
name := ""
if speaker.MeetingUser() != nil {
u := speaker.MeetingUser().User()
name = u.ShortName()

if len(speaker.MeetingUser().StructureLevels()) != 0 {
structureLevelNames := []string{}
for _, sl := range speaker.MeetingUser().StructureLevels() {
structureLevelNames = append(structureLevelNames, sl.Name)
}

if len(nameParts) == 0 {
nameParts = append(nameParts, "User "+strconv.Itoa(speaker.MeetingUser().User().ID))
name = fmt.Sprintf("%s (%s)", name, strings.Join(structureLevelNames, ", "))
}
}

weight := 0
if speaker.Weight != nil {
weight = *speaker.Weight
}

speakers = append([]speakerListItem{{
Number: weight + 1,
Name: strings.Join(nameParts, " "),
speechState := ""
if speaker.SpeechState != nil {
speechState = *speaker.SpeechState
}

item := speakerListItem{
Name: name,
Weight: weight,
}}, speakers...)
}
if (speaker.BeginTime == nil) && speaker.EndTime == nil {
if speechState == "interposed_question" {
interposedQuestions = append(interposedQuestions, item)
} else {
waitingSpeakers = append(waitingSpeakers, item)
}
} else if speaker.EndTime == nil || *speaker.EndTime == 0 {
if speechState == "interposed_question" {
currentInterposedQuestion = &item
} else {
currentSpeaker = &item
}
}
}

sort.Slice(speakers, func(i, j int) bool {
return speakers[i].Weight < speakers[j].Weight
sort.Slice(waitingSpeakers, func(i, j int) bool {
return waitingSpeakers[i].Weight < waitingSpeakers[j].Weight
})

sort.Slice(interposedQuestions, func(i, j int) bool {
return interposedQuestions[i].Weight < interposedQuestions[j].Weight
})

var content bytes.Buffer
err = tmpl.Execute(&content, map[string]interface{}{
"ListOfSpeakers": los,
"Speakers": speakers,
"Overlay": overlay,
"ListOfSpeakers": los,
"CurrentSpeaker": currentSpeaker,
"Speakers": waitingSpeakers,
"InterposedQuestions": interposedQuestions,
"CurrentInterposedQuestion": currentInterposedQuestion,
"Overlay": overlay,
})
if err != nil {
log.Error().Err(err).Msg("could not execute current-list-of-speakers template")
Expand Down
184 changes: 184 additions & 0 deletions pkg/projector/slide/current_speaker_chyron.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package slide

import (
"bytes"
"context"
"encoding/json"
"fmt"
"html/template"
"reflect"
"strings"

"github.com/OpenSlides/openslides-projector-service/pkg/datastore"
"github.com/OpenSlides/openslides-projector-service/pkg/models"
"github.com/rs/zerolog/log"
)

type currentSpeakerChyronSlideOptions struct {
ChyronType string `json:"chyron_type"`
AgendaItem bool `json:"agenda_item"`
}

func CurrentSpeakerChyronSlideHandler(ctx context.Context, req *projectionRequest) (<-chan string, error) {
content := make(chan string)
projection := req.Projection

referenceProjectorId := 0
refProjectorSub, err := datastore.Collection(req.DB, &models.Meeting{}).SetFqids(projection.ContentObjectID).SetFields("reference_projector_id").SubscribeField(&referenceProjectorId)
if err != nil {
return nil, fmt.Errorf("failed to subscribe reference projector id: %w", err)
}

var projector models.Projector
projectorQ := datastore.Collection(req.DB, &models.Projector{}).With("current_projection_ids", nil)
projectorSub, err := projectorQ.SubscribeOne(&projector)
if err != nil {
return nil, fmt.Errorf("failed to subscribe reference projector: %w", err)
}

projectionsQ := projectorQ.GetSubquery("current_projection_ids")
projectionsQ.With("content_object_id", nil)

var los models.ListOfSpeakers
losQ := datastore.Collection(req.DB, &models.ListOfSpeakers{}).With("speaker_ids", nil)
losQ.With("meeting_id", []string{"list_of_speakers_default_structure_level_time"})
losContentObjectQ := losQ.With("content_object_id", nil)
losContentObjectQ.With("agenda_item_id", nil)
speakersQ := losQ.GetSubquery("speaker_ids")
sllosQ := speakersQ.With("structure_level_list_of_speakers_id", nil)
sllosQ.With("structure_level_id", []string{"name"})
meetingUsersQ := speakersQ.With("meeting_user_id", nil)
meetingUsersQ.With("user_id", nil)
meetingUsersQ.With("structure_level_ids", []string{"name"})

losSub, err := losQ.SubscribeOne(&los)
if err != nil {
return nil, fmt.Errorf("failed to subscribe list of speakers: %w", err)
}

var options currentSpeakerChyronSlideOptions
if err := json.Unmarshal(projection.Options, &options); err != nil {
return nil, fmt.Errorf("could not parse slide options: %w", err)
}

go func() {
for {
select {
case <-ctx.Done():
refProjectorSub.Unsubscribe()
projectorSub.Unsubscribe()
losSub.Unsubscribe()
close(content)
return
case <-refProjectorSub.Channel:
if referenceProjectorId > 0 {
projectorQ.SetIds(referenceProjectorId)
if err := projectorSub.Reload(); err != nil {
log.Err(err).Msg("Reference projector load failed")
}
}
case <-projectorSub.Channel:
for _, p := range projector.CurrentProjections() {
if p.ContentObjectID == "" {
continue
}

losId := p.ContentObject().Get("list_of_speakers_id")
if losId != nil {
v := reflect.ValueOf(losId)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}

losQ.SetIds(int(v.Int()))
if err := losSub.Reload(); err != nil {
log.Err(err).Msg("Reference projector load failed")
}
break
}
}
case <-losSub.Channel:
if los.ID != 0 {
content <- getSpeakerChyronSlideContent(&los, options)
} else {
content <- ""
}
}
}
}()

return content, nil
}

func getSpeakerChyronSlideContent(los *models.ListOfSpeakers, options currentSpeakerChyronSlideOptions) string {
tmpl, err := template.ParseFiles("templates/slides/current-speaker-chyron.html")
if err != nil {
log.Error().Err(err).Msg("could not load current-list-of-speakers template")
return ""
}

var currentSpeaker *models.Speaker
for _, speaker := range los.Speakers() {
if speaker.IsCurrent() {
speechState := ""
if speaker.SpeechState != nil {
speechState = *speaker.SpeechState
}

if speechState == "interposed_question" {
currentSpeaker = speaker
break
} else {
currentSpeaker = speaker
}
}
}

speakerName := ""
structureLevel := ""
agendaItem := ""
if currentSpeaker != nil && currentSpeaker.MeetingUser() != nil {
u := currentSpeaker.MeetingUser().User()
speakerName = u.ShortName()

structureLevelDefaultTime := los.Meeting().ListOfSpeakersDefaultStructureLevelTime
if structureLevelDefaultTime != nil && *structureLevelDefaultTime > 0 {
sllos := currentSpeaker.StructureLevelListOfSpeakers()
if sllos != nil {
structureLevel = sllos.StructureLevel().Name
}
} else {
structureLevelNames := []string{}
for _, sl := range currentSpeaker.MeetingUser().StructureLevels() {
structureLevelNames = append(structureLevelNames, sl.Name)
}

structureLevel = strings.Join(structureLevelNames, ", ")
}

if options.ChyronType == "new" && structureLevel != "" {
speakerName = fmt.Sprintf("%s, %s", speakerName, structureLevel)
}
}

// TODO: Also include agenda item number and number
coTitle := los.ContentObject().Get("title")
if coTitle != nil {
agendaItem = coTitle.(string)
}

slideData := map[string]interface{}{
"Options": options,
"SpeakerName": speakerName,
"StructureLevel": structureLevel,
"AgendaItem": agendaItem,
}

var content bytes.Buffer
if err := tmpl.Execute(&content, slideData); err != nil {
log.Error().Err(err).Msg("could not execute current-list-of-speakers template")
return ""
}

return content.String()
}
12 changes: 6 additions & 6 deletions pkg/projector/slide/motion.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ func MotionSlideHandler(ctx context.Context, req *projectionRequest) (<-chan str

for {
select {
case <-ctx.Done():
motionSub.Unsubscribe()
close(content)
return
case <-motionSub.Channel:
content <- getMotionSlideContent(&motion)
case <-ctx.Done():
motionSub.Unsubscribe()
close(content)
return
case <-motionSub.Channel:
content <- getMotionSlideContent(&motion)
}
}
}()
Expand Down
5 changes: 5 additions & 0 deletions pkg/projector/slide/slide.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func New(ctx context.Context, db *datastore.Datastore) *SlideRouter {
routes := make(map[string]slideHandler)
routes["topic"] = TopicSlideHandler
routes["current_list_of_speakers"] = CurrentListOfSpeakersSlideHandler
routes["current_speaker_chyron"] = CurrentSpeakerChyronSlideHandler

return &SlideRouter{
ctx: ctx,
Expand Down Expand Up @@ -102,6 +103,10 @@ func (r *SlideRouter) subscribeProjection(ctx context.Context, id int, updateCha
}
} else {
log.Warn().Msgf("unknown projection type %s", projectionType)
updateChannel <- &projectionUpdate{
ID: id,
Content: "",
}
}
}

Expand Down
Loading