diff --git a/go.mod b/go.mod index ef29f345..6813c018 100644 --- a/go.mod +++ b/go.mod @@ -6,14 +6,11 @@ require ( github.com/OpenSlides/openslides-go v0.0.0-20251001164443-0e7e385b3730 github.com/alecthomas/kong v1.12.1 github.com/klauspost/compress v1.18.0 - github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 github.com/ostcar/topic v0.4.1 - github.com/stretchr/testify v1.11.1 github.com/zeebo/xxh3 v1.0.2 ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect github.com/goccy/go-yaml v1.18.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/gomodule/redigo v1.9.2 // indirect @@ -22,10 +19,9 @@ require ( github.com/jackc/pgx/v5 v5.7.6 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect golang.org/x/crypto v0.42.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.29.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 8d428926..5f3ed40e 100644 --- a/go.sum +++ b/go.sum @@ -53,18 +53,12 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM= -github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -79,8 +73,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -107,8 +99,6 @@ golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/autoupdate/flow.go b/internal/autoupdate/flow.go index 624ff2d4..092df34f 100644 --- a/internal/autoupdate/flow.go +++ b/internal/autoupdate/flow.go @@ -4,8 +4,6 @@ import ( "fmt" "github.com/OpenSlides/openslides-autoupdate-service/internal/metric" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/slide" "github.com/OpenSlides/openslides-go/datastore" "github.com/OpenSlides/openslides-go/datastore/cache" "github.com/OpenSlides/openslides-go/datastore/flow" @@ -16,9 +14,7 @@ import ( type Flow struct { flow.Flow - cache *cache.Cache - projector *projector.Projector - postgres *datastore.FlowPostgres + cache *cache.Cache } // NewFlow initializes a flow for the autoupdate service. @@ -29,13 +25,10 @@ func NewFlow(lookup environment.Environmenter) (*Flow, error) { } cache := cache.New(postgres) - projector := projector.NewProjector(cache, slide.Slides()) flow := Flow{ - Flow: projector, - cache: cache, - projector: projector, - postgres: postgres, + Flow: cache, + cache: cache, } metric.Register(flow.metric) @@ -46,7 +39,6 @@ func NewFlow(lookup environment.Environmenter) (*Flow, error) { // ResetCache clears the cache. func (f *Flow) ResetCache() { f.cache.Reset() - f.projector.Reset() } func (f *Flow) metric(values metric.Container) { diff --git a/internal/projector/datastore/alias.go b/internal/projector/datastore/alias.go deleted file mode 100644 index a66b8257..00000000 --- a/internal/projector/datastore/alias.go +++ /dev/null @@ -1,22 +0,0 @@ -package datastore - -import ( - "context" - - "github.com/OpenSlides/openslides-go/datastore/dsfetch" - "github.com/OpenSlides/openslides-go/datastore/dskey" -) - -// DoesNotExistError is a type alias from datastore.DoesNotExistError -type DoesNotExistError = dsfetch.DoesNotExistError - -// Key is a type alias from dskey.Key -type Key = dskey.Key - -// KeyFromString from package dskey. -var KeyFromString = dskey.FromString - -// Getter is the same as datastore.Getter -type Getter interface { - Get(ctx context.Context, keys ...dskey.Key) (map[dskey.Key][]byte, error) -} diff --git a/internal/projector/datastore/fetch.go b/internal/projector/datastore/fetch.go deleted file mode 100644 index 66f46daf..00000000 --- a/internal/projector/datastore/fetch.go +++ /dev/null @@ -1,184 +0,0 @@ -package datastore - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/OpenSlides/openslides-go/datastore/dsfetch" - "github.com/OpenSlides/openslides-go/datastore/dskey" -) - -// Fetcher is a helper to fetch many keys from the datastore. -// -// The methods do not return an error. If an error happens, it is saved -// internaly. As soon, as an error happens, all later calls to methods of that -// fetcher are noops. -// -// The method Fetcher.Err() can be used to get the error. -// -// Make sure to call Fetcher.Err() at the end to see, if an error happened. -type Fetcher struct { - getter Getter - err error -} - -// NewFetcher initializes a Fetcher object. -func NewFetcher(getter Getter) *Fetcher { - return &Fetcher{getter: getter} -} - -// Fetch gets a value from the datastore and saves it into the argument `value`. -// -// If the object, that the key belongs to does not exist, no error is thrown. -// -// To get the error, call f.Err(). -func (f *Fetcher) Fetch(ctx context.Context, value interface{}, keyFmt string, a ...interface{}) { - if f.err != nil { - return - } - - fqfield, err := dskey.FromString(fmt.Sprintf(keyFmt, a...)) - if err != nil { - f.err = err - return - } - - fields, err := f.getter.Get(ctx, fqfield) - if err != nil { - f.err = fmt.Errorf("getting data from datastore: %w", err) - return - } - - if fields[fqfield] == nil { - return - } - - if err := json.Unmarshal(fields[fqfield], value); err != nil { - f.err = fmt.Errorf("unpacking value of %q: %w", fqfield, err) - } -} - -// FetchIfExist is like Fetch but if the element that the key belongs to does -// not exist, then a DoesNotExistError is returned. -func (f *Fetcher) FetchIfExist(ctx context.Context, value interface{}, keyFmt string, a ...interface{}) { - if f.err != nil { - return - } - - fqfield, err := dskey.FromString(fmt.Sprintf(keyFmt, a...)) - if err != nil { - f.err = err - return - } - - idField := fqfield.IDField() - - fields, err := f.getter.Get(ctx, idField, fqfield) - if err != nil { - f.err = fmt.Errorf("getting data from datastore: %w", err) - return - } - - if fields[idField] == nil { - f.err = dsfetch.DoesNotExistError(idField) - return - } - if fields[fqfield] == nil { - return - } - - if err := json.Unmarshal(fields[fqfield], value); err != nil { - f.err = fmt.Errorf("unpacking value of %q: %w", fqfield, err) - } -} - -// Object returns a json object for the given fqid with all given fields. -// -// If one field does not exist in the datastore, then it is returned as nil. -// -// If the object does not exist, then a DoesNotExistError is thrown. -func (f *Fetcher) Object(ctx context.Context, fqID string, fields ...string) map[string]json.RawMessage { - if f.err != nil { - return nil - } - - keys := make([]dskey.Key, len(fields)+1) - idKey, err := dskey.FromString(fqID + "/id") - if err != nil { - f.err = err - return nil - } - keys[0] = idKey - - for i := 0; i < len(fields); i++ { - k, err := dskey.FromString(fqID + "/" + fields[i]) - if err != nil { - f.err = err - return nil - } - keys[i+1] = k - } - - vals, err := f.getter.Get(ctx, keys...) - if err != nil { - f.err = fmt.Errorf("fetching data: %w", err) - return nil - } - - if vals[idKey] == nil { - f.err = dsfetch.DoesNotExistError(idKey) - return nil - } - - object := make(map[string]json.RawMessage, len(fields)) - for i := 0; i < len(fields); i++ { - key, err := dskey.FromString(fqID + "/" + fields[i]) - if err != nil { - f.err = err - return nil - } - object[fields[i]] = vals[key] - } - return object -} - -// Err returns the error that happened at a method call. If no error happened, -// then Err() returns nil. -func (f *Fetcher) Err() error { - err := f.err - f.err = nil - return err -} - -// FetchFunc is a function that fetches a value. It has the signature of -// fetch.Fetch() or fetch.FetchIfExist(). -type FetchFunc func(ctx context.Context, value interface{}, keyFmt string, a ...interface{}) - -// Bool fetches an boolean from the datastore. -func Bool(ctx context.Context, fetch FetchFunc, keyFmt string, a ...interface{}) bool { - var value bool - fetch(ctx, &value, keyFmt, a...) - return value -} - -// Int fetches an integer from the datastore. -func Int(ctx context.Context, fetch FetchFunc, keyFmt string, a ...interface{}) int { - var value int - fetch(ctx, &value, keyFmt, a...) - return value -} - -// Ints fetches an int slice from the datastore. -func Ints(ctx context.Context, fetch FetchFunc, keyFmt string, a ...interface{}) []int { - var value []int - fetch(ctx, &value, keyFmt, a...) - return value -} - -// String fetches a string from the datastore. -func String(ctx context.Context, fetch FetchFunc, keyFmt string, a ...interface{}) string { - var value string - fetch(ctx, &value, keyFmt, a...) - return value -} diff --git a/internal/projector/projector.go b/internal/projector/projector.go deleted file mode 100644 index 37cdf25e..00000000 --- a/internal/projector/projector.go +++ /dev/null @@ -1,294 +0,0 @@ -package projector - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "log" - "strings" - "sync" - "time" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" - "github.com/OpenSlides/openslides-go/datastore/dskey" - "github.com/OpenSlides/openslides-go/datastore/dsrecorder" - "github.com/OpenSlides/openslides-go/datastore/flow" - "github.com/OpenSlides/openslides-go/oserror" -) - -const longCalculation = time.Second - -// NewProjector initializes a new Projector. -func NewProjector(ds flow.Flow, slides *SlideStore) *Projector { - return &Projector{ - hotKeys: make(map[dskey.Key]map[dskey.Key]struct{}), - cache: make(map[dskey.Key][]byte), - - flow: ds, - slides: slides, - } -} - -// Projector is a Flow that adds the field projection/content -// -// When such a key is requested with Get, it gets calculated. -// -// When keys get updated via Update, that where needed to calculate a field, the -// field is updated. -// -// Only projections with a current_projector_id get calculated. Fields from -// other projections return nil. If current_projector_id get updated to nil/0, -// then the field is removed from the cache. -type Projector struct { - mu sync.RWMutex - hotKeys map[dskey.Key]map[dskey.Key]struct{} - cache map[dskey.Key][]byte - - flow flow.Flow - slides *SlideStore -} - -// Reset clears the projector object. -func (p *Projector) Reset() { - p.mu.Lock() - defer p.mu.Unlock() - p.cache = make(map[dskey.Key][]byte) - p.hotKeys = make(map[dskey.Key]map[dskey.Key]struct{}) -} - -// Get is a Getter middleware that passes all keys though but calculates -// projection/content keys. -func (p *Projector) Get(ctx context.Context, keys ...dskey.Key) (map[dskey.Key][]byte, error) { - normalKeys, contentKeys := splitKeys(keys) - - values, err := p.flow.Get(ctx, normalKeys...) - if err != nil { - return nil, fmt.Errorf("get from flow: %w", err) - } - - if len(contentKeys) == 0 { - return values, nil - } - - p.mu.RLock() - var needCalc []dskey.Key - for _, k := range contentKeys { - v, ok := p.cache[k] - if !ok { - needCalc = append(needCalc, k) - continue - } - - values[k] = v - } - p.mu.RUnlock() - - if len(needCalc) == 0 { - return values, nil - } - - p.mu.Lock() - for _, k := range needCalc { - v := p.calculate(ctx, k) - p.cache[k] = v - values[k] = v - } - p.mu.Unlock() - - return values, nil -} - -// Update updates projection/content keys. -func (p *Projector) Update(ctx context.Context, updateFn func(map[dskey.Key][]byte, error)) { - p.flow.Update(ctx, func(data map[dskey.Key][]byte, err error) { - if err != nil { - updateFn(nil, err) - return - } - - p.mu.Lock() - defer p.mu.Unlock() - - needUpdate := p.needUpdate(data) - - if len(needUpdate) == 0 { - updateFn(data, nil) - return - } - - for _, key := range needUpdate { - value := p.calculate(ctx, key) - data[key] = value - p.cache[key] = value - } - - updateFn(data, nil) - }) -} - -func (p *Projector) needUpdate(data map[dskey.Key][]byte) []dskey.Key { - var needUpdate []dskey.Key - for calculated := range p.hotKeys { - for key := range data { - if _, ok := p.hotKeys[calculated][key]; ok { - needUpdate = append(needUpdate, calculated) - break - } - } - } - return needUpdate -} - -func splitKeys(keys []dskey.Key) ([]dskey.Key, []dskey.Key) { - var contentKeys []dskey.Key - normalKeys := make([]dskey.Key, 0, len(keys)) - for _, k := range keys { - if !(k.Collection() == "projection" && k.Field() == "content") { - normalKeys = append(normalKeys, k) - continue - } - - contentKeys = append(contentKeys, k) - } - - return normalKeys, contentKeys -} - -func (p *Projector) calculate(ctx context.Context, fqfield dskey.Key) []byte { - bs, err := p.calculateHelper(ctx, fqfield) - if err != nil { - oserror.Handle(fmt.Errorf("Error calculating key %s: %v", fqfield, err)) - msg := fmt.Sprintf("calculating key %s", fqfield) - return []byte(fmt.Sprintf(`{"error": "%s"}`, msg)) - } - - return bs -} - -func (p *Projector) calculateHelper(ctx context.Context, fqfield dskey.Key) ([]byte, error) { - recorder := dsrecorder.New(p.flow) - fetch := datastore.NewFetcher(recorder) - - defer func() { - // At the end, save all requested keys to check later if one has - // changed. - p.hotKeys[fqfield] = recorder.Keys() - }() - - data := fetch.Object( - ctx, - fqfield.FQID(), - "id", - "type", - "content_object_id", - "meeting_id", - "options", - "current_projector_id", - ) - if err := fetch.Err(); err != nil { - var errDoesNotExist datastore.DoesNotExistError - if errors.As(err, &errDoesNotExist) { - return nil, nil - } - return nil, fmt.Errorf("fetching projection %d from datastore: %w", fqfield.ID(), err) - } - - p7on, err := p7onFromMap(data) - if err != nil { - return nil, fmt.Errorf("loading p7on: %w", err) - } - - if p7on.CurrentProjectorID == 0 { - return nil, nil - } - - if p7on.ContentObjectID == "" { - // There are broken projections in the datastore. Ignore them. - log.Printf("Bug in Backend: The projection %d has an empty content_object_id", p7on.ID) - return nil, nil - } - - slideName, err := p7on.slideName() - if err != nil { - return nil, fmt.Errorf("getting slide name: %w", err) - } - - slider := p.slides.GetSlider(slideName) - if slider == nil { - return nil, fmt.Errorf("unknown slide %s", slideName) - } - - bs, err := slider.Slide(ctx, fetch, p7on) - if err != nil { - return nil, fmt.Errorf("calculating slide %s for p7on %v: %w", slideName, p7on, err) - } - - if err := fetch.Err(); err != nil { - return nil, err - } - - final, err := addCollection(bs, slideName) - if err != nil { - return nil, fmt.Errorf("adding name of collection %q to value %q: %w", slideName, bs, err) - } - return final, nil -} - -// addCollection adds the collection addribute to the given encoded json. -// -// `bs` has to be a encoded json-object. `collection` has to be a valid json -// string. -func addCollection(bs []byte, collection string) ([]byte, error) { - var decoded map[string]json.RawMessage - if err := json.Unmarshal(bs, &decoded); err != nil { - return nil, fmt.Errorf("decoding object: %w", err) - } - - decoded["collection"] = []byte(`"` + collection + `"`) - - bs, err := json.Marshal(decoded) - if err != nil { - return nil, fmt.Errorf("encoding object: %w", err) - } - return bs, nil -} - -// Projection holds the meta data to render a projection on a projecter. -type Projection struct { - ID int `json:"id"` - Type string `json:"type"` - ContentObjectID string `json:"content_object_id"` - MeetingID int `json:"meeting_id"` - Options json.RawMessage `json:"options"` - CurrentProjectorID int `json:"current_projector_id"` -} - -func p7onFromMap(in map[string]json.RawMessage) (*Projection, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding projection data: %w", err) - } - - var p Projection - if err := json.Unmarshal(bs, &p); err != nil { - return nil, fmt.Errorf("decoding projection: %w", err) - } - return &p, nil -} - -// slideName extracts the name from Projection. -// Using Type as slideName is only possible together with collection meeting, -// otherwise use always collection. -func (p *Projection) slideName() (string, error) { - parts := strings.Split(p.ContentObjectID, "/") - if len(parts) != 2 { - // TODO LAST ERROR - return "", fmt.Errorf("invalid content_object_id `%s`, expected one '/'", p.ContentObjectID) - } - - if p.Type != "" && parts[0] == "meeting" { - return p.Type, nil - } - return parts[0], nil -} diff --git a/internal/projector/projector_test.go b/internal/projector/projector_test.go deleted file mode 100644 index 7ba5cb04..00000000 --- a/internal/projector/projector_test.go +++ /dev/null @@ -1,380 +0,0 @@ -package projector_test - -import ( - "context" - "encoding/json" - "fmt" - "reflect" - "sync" - "testing" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" - "github.com/OpenSlides/openslides-go/datastore/dskey" - "github.com/OpenSlides/openslides-go/datastore/dsmock" - "github.com/nsf/jsondiff" -) - -func TestProjectionDoesNotExist(t *testing.T) { - ctx := context.Background() - - flow := dsmock.NewFlow(nil) - myKey := dskey.MustKey("projection/1/content") - - p := projector.NewProjector(flow, testSlides()) - - got, err := p.Get(ctx, myKey) - if err != nil { - t.Fatalf("Get: %v", err) - } - - expect := map[dskey.Key][]byte{myKey: nil} - if !reflect.DeepEqual(got, expect) { - t.Errorf("Got %v, expected %v", got, expect) - } -} - -func TestProjectionFromContentObject(t *testing.T) { - ctx := context.Background() - flow := dsmock.NewFlow(dsmock.YAMLData(`--- - projection/1: - content_object_id: user/1 - current_projector_id: 1 - `)) - key := dskey.MustKey("projection/1/content") - p := projector.NewProjector(flow, testSlides()) - - got, err := p.Get(ctx, key) - if err != nil { - t.Fatalf("Get: %v", err) - } - - expect := []byte(`{"collection":"user","value":"user"}` + "\n") - - if equal, explain := cmpJson(got[key], expect); !equal { - t.Errorf("got != expect: %s", explain) - } -} - -func TestProjectionFromContentObjectIfNotOnProjector(t *testing.T) { - ctx := context.Background() - flow := dsmock.NewFlow(dsmock.YAMLData(`--- - projection/1: - content_object_id: user/1 - current_projector_id: null - `)) - key := dskey.MustKey("projection/1/content") - p := projector.NewProjector(flow, testSlides()) - - got, err := p.Get(ctx, key) - if err != nil { - t.Fatalf("Get: %v", err) - } - - if got[key] != nil { - t.Errorf("got %v, expected nil", got) - } -} - -func TestProjectionFromType(t *testing.T) { - ctx := context.Background() - flow := dsmock.NewFlow(dsmock.YAMLData(` - projection/1: - content_object_id: meeting/1 - type: test1 - current_projector_id: 1 - `)) - key := dskey.MustKey("projection/1/content") - p := projector.NewProjector(flow, testSlides()) - - got, err := p.Get(ctx, key) - if err != nil { - t.Fatalf("Get: %v", err) - } - - expect := []byte(`{"collection":"test1","value":"abc"}` + "\n") - if equal, explain := cmpJson(got[key], expect); !equal { - t.Errorf("got != expect: %s", explain) - } -} - -func TestProjectionWithOptionsData(t *testing.T) { - ctx := context.Background() - flow := dsmock.NewFlow(dsmock.YAMLData(` - projection/1: - content_object_id: "meeting/6" - type: "projection" - meeting_id: 1 - options: {"only_main_items": true} - current_projector_id: 1 - `)) - key := dskey.MustKey("projection/1/content") - p := projector.NewProjector(flow, testSlides()) - - got, err := p.Get(ctx, key) - if err != nil { - t.Fatalf("Get: %v", err) - } - - expect := []byte(`{"collection":"projection","id": 1,"current_projector_id":1, "content_object_id": "meeting/6", "type":"projection", "meeting_id": 1, "options": {"only_main_items": true}}` + "\n") - if equal, expain := cmpJson(got[key], expect); !equal { - t.Errorf("got != expect: %s", expain) - } -} - -func TestProjectionTypeDoesNotExist(t *testing.T) { - ctx := context.Background() - flow := dsmock.NewFlow(dsmock.YAMLData(` - projection/1: - content_object_id: meeting/1 - type: unexistingTestSlide - - current_projector_id: 1 - `)) - key := dskey.MustKey("projection/1/content") - p := projector.NewProjector(flow, testSlides()) - - got, err := p.Get(ctx, key) - if err != nil { - t.Fatalf("Get: %v", err) - } - - var content struct { - Error string `json:"error"` - } - if err := json.Unmarshal(got[key], &content); err != nil { - t.Fatalf("Can not unmarshal field projection/1/content `%s`: %v", got[key], err) - } - - if content.Error == "" { - t.Errorf("Field has not error") - } -} - -func TestProjectionUpdateProjection(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - flow := dsmock.NewFlow(dsmock.YAMLData(`--- - projection/1: - content_object_id: meeting/1 - type: test1 - current_projector_id: 1 - `)) - key := dskey.MustKey("projection/1/content") - p := projector.NewProjector(flow, testSlides()) - - done := make(chan struct{}) - go p.Update(ctx, func(map[dskey.Key][]byte, error) { - close(done) - }) - - // Fetch data once to fill the test. - if _, err := p.Get(ctx, key); err != nil { - t.Fatalf("Get: %v", err) - } - - flow.Send(dsmock.YAMLData(`--- - projection/1: - type: null - content_object_id: user/1 - `)) - <-done - - got, err := p.Get(ctx, key) - if err != nil { - t.Fatalf("Second Get: %v", err) - } - - expect := []byte(`{"collection":"user","value":"user"}` + "\n") - if equal, expain := cmpJson(got[key], expect); !equal { - t.Errorf("got != expect: %s", expain) - } -} - -func TestProjectionUpdateProjectionMetaData(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - flow := dsmock.NewFlow(dsmock.YAMLData(`--- - projection/1: - type: projection - content_object_id: meeting/1 - current_projector_id: 1 - `)) - key := dskey.MustKey("projection/1/content") - p := projector.NewProjector(flow, testSlides()) - - done := make(chan struct{}) - go p.Update(ctx, func(map[dskey.Key][]byte, error) { - close(done) - }) - - // Fetch data once to fill the hot keys. - if _, err := p.Get(ctx, key); err != nil { - t.Fatalf("Get: %v", err) - } - - flow.Send(dsmock.YAMLData("projection/1/stable: true")) - <-done - - got, err := p.Get(ctx, key) - if err != nil { - t.Fatalf("Second get: %v", err) - } - - expect := []byte(`{"collection":"projection","id": 1, "content_object_id": "meeting/1", "meeting_id":0, "type":"projection", "options": null,"current_projector_id":1}` + "\n") - if equal, expain := cmpJson(got[key], expect); !equal { - t.Errorf("got != expect: %s", expain) - } -} - -func TestProjectionUpdateSlide(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - flow := dsmock.NewFlow(dsmock.YAMLData(`--- - projection/1: - type: user - content_object_id: meeting/6 - current_projector_id: 1 - `)) - key := dskey.MustKey("projection/1/content") - p := projector.NewProjector(flow, testSlides()) - - done := make(chan struct{}) - go p.Update(ctx, func(map[dskey.Key][]byte, error) { - close(done) - }) - - // Fetch data once to fill the hot keys. - if _, err := p.Get(ctx, key); err != nil { - t.Fatalf("Get: %v", err) - } - - flow.Send(dsmock.YAMLData("user/1/username: new value")) - <-done - - got, err := p.Get(ctx, key) - if err != nil { - t.Errorf("second Get: %v", err) - } - - expect := []byte(`{"collection":"user","value":"calculated with new value"}` + "\n") - if equal, expain := cmpJson(got[key], expect); !equal { - t.Errorf("got != expect: %s", expain) - } -} - -func TestProjectionUpdateOtherKey(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - flow := dsmock.NewFlow(dsmock.YAMLData(`--- - projection/1: - type: user - content_object_id: meeting/1 - current_projector_id: 1 - `)) - key := dskey.MustKey("projection/1/content") - p := projector.NewProjector(flow, testSlides()) - - done := make(chan struct{}) - go p.Update(ctx, func(map[dskey.Key][]byte, error) { - close(done) - }) - - // Fetch data once to fill the hot keys. - if _, err := p.Get(ctx, key); err != nil { - t.Fatalf("Get: %v", err) - } - - flow.Send(dsmock.YAMLData("group/1/name: new value")) - <-done - - got, err := p.Get(ctx, key) - if err != nil { - t.Errorf("second Get: %v", err) - } - - expect := []byte(`{"collection":"user","value":"user"}` + "\n") - if equal, expain := cmpJson(got[key], expect); !equal { - t.Errorf("got != expect: %s", expain) - } -} - -func TestOnTwoProjections(t *testing.T) { - // Test that when reading two different projections at the same time in - // different goroutines, there is no race condition. - // - // This test is only usefull, when the race detector is enabled. - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - key1 := dskey.MustKey("projection/1/content") - key2 := dskey.MustKey("projection/2/content") - - ds := dsmock.NewFlow(dsmock.YAMLData(`--- - projection: - 1: - content_object_id: meeting/1 - type: user - - 2: - content_object_id: meeting/1 - type: user - `)) - - p := projector.NewProjector(ds, testSlides()) - - var wg sync.WaitGroup - wg.Add(2) - - go func() { - defer wg.Done() - - if _, err := p.Get(ctx, key1); err != nil { - t.Errorf("Get returned unexpected error: %v", err) - } - }() - - go func() { - defer wg.Done() - - if _, err := p.Get(ctx, key2); err != nil { - t.Errorf("Get returned unexpected error: %v", err) - } - }() - - wg.Wait() -} - -func testSlides() *projector.SlideStore { - s := new(projector.SlideStore) - s.RegisterSliderFunc("test1", func(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection) (encoded []byte, err error) { - return []byte(`{"value":"abc"}`), nil - }) - - s.RegisterSliderFunc("user", func(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection) (encoded []byte, err error) { - var field json.RawMessage - fetch.Fetch(ctx, &field, "user/1/username") - if field == nil { - return []byte(`{"value":"user"}`), nil - } - return []byte(fmt.Sprintf(`{"value":"calculated with %s"}`, string(field[1:len(field)-1]))), nil - }) - - s.RegisterSliderFunc("projection", func(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection) (encoded []byte, err error) { - bs, err := json.Marshal(p7on) - return bs, err - }) - return s -} - -func cmpJson(got, expect []byte) (bool, string) { - options := jsondiff.DefaultJSONOptions() - if cmp, explain := jsondiff.Compare(got, []byte(expect), &options); cmp != jsondiff.FullMatch { - return false, explain - } - return true, "" -} diff --git a/internal/projector/slide/agenda.go b/internal/projector/slide/agenda.go deleted file mode 100644 index 05c6989c..00000000 --- a/internal/projector/slide/agenda.go +++ /dev/null @@ -1,186 +0,0 @@ -package slide - -import ( - "context" - "encoding/json" - "fmt" - "sort" - "strings" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" -) - -type dbAgendaItem struct { - ID int `json:"id"` - ItemNumber string `json:"item_number"` - ContentObjectID string `json:"content_object_id"` - MeetingID int `json:"meeting_id"` - IsHidden bool `json:"is_hidden"` - IsInternal bool `json:"is_internal"` - Depth int `json:"level"` - Weight int `json:"weight"` - ParentID int `json:"parent_id"` -} - -func agendaItemFromMap(in map[string]json.RawMessage) (*dbAgendaItem, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding agenda item data: %w", err) - } - - var ai dbAgendaItem - if err := json.Unmarshal(bs, &ai); err != nil { - return nil, fmt.Errorf("decoding agenda item data: %w", err) - } - return &ai, nil -} - -type dbAgendaItemList struct { - AgendaItemIDs []int `json:"agenda_item_ids"` - AgendaShowInternal bool `json:"agenda_show_internal_items_on_projector"` -} - -func agendaItemListFromMap(in map[string]json.RawMessage) (*dbAgendaItemList, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding agenda item list data: %w", err) - } - - var ail dbAgendaItemList - if err := json.Unmarshal(bs, &ail); err != nil { - return nil, fmt.Errorf("decoding agenda item list data: %w", err) - } - return &ail, nil -} - -type outAgendaItem struct { - TitleInformation json.RawMessage `json:"title_information"` - Depth int `json:"depth"` - weight int - parent int - id int -} - -// AgendaItemList renders the agenda_item_list slide. -func AgendaItemList(store *projector.SlideStore) { - store.RegisterSliderFunc("agenda_item_list", func(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection) (encoded []byte, err error) { - data := fetch.Object( - ctx, - p7on.ContentObjectID, - "agenda_item_ids", - "agenda_show_internal_items_on_projector", - ) - agendaItemList, err := agendaItemListFromMap(data) - if err != nil { - return nil, fmt.Errorf("get agenda item list: %w", err) - } - - var options struct { - OnlyMainItems bool `json:"only_main_items"` - } - if p7on.Options != nil { - if err := json.Unmarshal(p7on.Options, &options); err != nil { - return nil, fmt.Errorf("decoding projection options: %w", err) - } - } - var allAgendaItems []*outAgendaItem - for _, aiID := range agendaItemList.AgendaItemIDs { - data = fetch.Object( - ctx, - fmt.Sprintf("agenda_item/%d", aiID), - "id", - "item_number", - "content_object_id", - "meeting_id", - "is_hidden", - "is_internal", - "level", - "weight", - "parent_id", - ) - agendaItem, err := agendaItemFromMap(data) - if err != nil { - return nil, fmt.Errorf("get agenda item: %w", err) - } - - if agendaItem.IsHidden || (agendaItem.IsInternal && !agendaItemList.AgendaShowInternal) { - continue - } - - if options.OnlyMainItems && agendaItem.Depth > 0 { - continue - } - - collection := strings.Split(agendaItem.ContentObjectID, "/")[0] - titler := store.GetTitleInformationFunc(collection) - if titler == nil { - return nil, fmt.Errorf("no titler function registered for %s", collection) - } - - titleInfo, err := titler.GetTitleInformation(ctx, fetch, agendaItem.ContentObjectID, agendaItem.ItemNumber, p7on.MeetingID) - if err != nil { - return nil, fmt.Errorf("get title func: %w", err) - } - - allAgendaItems = append( - allAgendaItems, - &outAgendaItem{ - TitleInformation: titleInfo, - Depth: agendaItem.Depth, - weight: agendaItem.Weight, - parent: agendaItem.ParentID, - id: agendaItem.ID, - }, - ) - } - - sort.Slice(allAgendaItems, func(i, j int) bool { - // sort by parent is not necessary, but helps to understand - if allAgendaItems[i].parent == allAgendaItems[j].parent { - if allAgendaItems[i].weight == allAgendaItems[j].weight { - return allAgendaItems[i].id < allAgendaItems[j].id - } - return allAgendaItems[i].weight < allAgendaItems[j].weight - } - return allAgendaItems[i].parent < allAgendaItems[j].parent - }) - - out := struct { - Items []*outAgendaItem `json:"items"` - }{getFlatTree(allAgendaItems)} - - responseValue, err := json.Marshal(out) - if err != nil { - return nil, fmt.Errorf("encoding response for slide agenda item list: %w", err) - } - if err := fetch.Err(); err != nil { - return nil, err - } - return responseValue, nil - }) -} - -// getFlatTree expects the allAgendaItems to be sorted in preorder, -// see https://en.wikipedia.org/wiki/Tree_traversal#Pre-order. -func getFlatTree(allAgendaItems []*outAgendaItem) []*outAgendaItem { - children := make(map[int][]int) - allItemMap := make(map[int]*outAgendaItem) - for _, item := range allAgendaItems { - children[item.parent] = append(children[item.parent], item.id) - allItemMap[item.id] = item - } - - flatTree := make([]*outAgendaItem, 0, len(allAgendaItems)) - var buildTree func(itemIDS []int, depth int) - buildTree = func(itemIDS []int, depth int) { - for _, itemID := range itemIDS { - item := allItemMap[itemID] - item.Depth = depth - flatTree = append(flatTree, item) - buildTree(children[itemID], depth+1) - } - } - buildTree(children[0], 0) - return flatTree -} diff --git a/internal/projector/slide/agenda_test.go b/internal/projector/slide/agenda_test.go deleted file mode 100644 index 59088d32..00000000 --- a/internal/projector/slide/agenda_test.go +++ /dev/null @@ -1,362 +0,0 @@ -package slide_test - -import ( - "context" - "testing" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/slide" - "github.com/OpenSlides/openslides-go/datastore/dskey" - "github.com/OpenSlides/openslides-go/datastore/dsmock" - "github.com/stretchr/testify/assert" -) - -func TestAgendaItemListAllContentObjectTypes(t *testing.T) { - s := new(projector.SlideStore) - slide.AgendaItemList(s) - slide.Assignment(s) - slide.Motion(s) - slide.MotionBlock(s) - slide.Topic(s) - - ailSlide := s.GetSlider("agenda_item_list") - assert.NotNilf(t, ailSlide, "Slide with name `agenda_item_list` not found.") - - data := dsmock.YAMLData(` - meeting: - 1: - agenda_show_internal_items_on_projector: false - agenda_item_ids: [1,2,3,4,5,6,7] - agenda_item: - 1: - item_number: Ino1.2 - content_object_id: assignment/1 - meeting_id: 1 - is_hidden: false - is_internal: false - weight: 8 - level: 0 - 2: - item_number: Ino1.1 - content_object_id: motion/1 - meeting_id: 1 - is_hidden: false - is_internal: false - weight: 4 - level: 0 - 3: - item_number: Ino1 - content_object_id: topic/1 - meeting_id: 1 - is_hidden: false - is_internal: false - weight: 2 - level: 0 - 4: - item_number: Ino1.1.1 - content_object_id: motion_block/1 - meeting_id: 1 - is_hidden: false - is_internal: false - weight: 6 - level: 0 - 5: - item_number: Ino5 misses because of level - content_object_id: topic/2 - meeting_id: 1 - is_hidden: false - is_internal: false - weight: 10 - level: 1 - 6: - item_number: Ino6 misses because of hidden - content_object_id: topic/3 - meeting_id: 1 - is_hidden: true - is_internal: false - weight: 12 - level: 0 - 7: - item_number: Ino7 misses because of internal - content_object_id: topic/4 - meeting_id: 1 - is_hidden: false - is_internal: True - weight: 14 - level: 0 - motion/1: - title: motion title 1 - number: motion number 1 - assignment/1/title: assignment title 1 - motion_block/1/title: motion_block title 1 - topic/1/title: topic title 1 - topic/2/title: topic title 2 - topic/3/title: topic title 3 - topic/4/title: topic title 4 - `) - - for _, tt := range []struct { - name string - data map[dskey.Key][]byte - expect string - }{ - { - "Starter AgendaItemList", - data, - `{ - "items": [ - { - "depth": 0, - "title_information": { - "collection": "topic", - "agenda_item_number": "Ino1", - "content_object_id": "topic/1", - "title": "topic title 1" - } - }, - { - "depth": 0, - "title_information": { - "collection": "motion", - "agenda_item_number": "Ino1.1", - "content_object_id": "motion/1", - "number": "motion number 1", - "title": "motion title 1" - } - }, - { - "depth": 0, - "title_information": { - "collection": "motion_block", - "agenda_item_number": "Ino1.1.1", - "content_object_id": "motion_block/1", - "title": "motion_block title 1" - } - }, - { - "depth": 0, - "title_information": { - "collection": "assignment", - "agenda_item_number": "Ino1.2", - "content_object_id": "assignment/1", - "title": "assignment title 1" - } - } - ] - } - `, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ds := dsmock.NewFlow(tt.data) - fetch := datastore.NewFetcher(ds) - - p7on := &projector.Projection{ - ContentObjectID: "meeting/1", - Type: "agenda_item_list", - MeetingID: 1, - Options: []byte(`{"only_main_items":true}`), - } - - bs, err := ailSlide.Slide(context.Background(), fetch, p7on) - assert.NoError(t, err) - assert.NoError(t, fetch.Err()) - assert.JSONEq(t, tt.expect, string(bs)) - }) - } -} - -// TestAgendaItemListWithDepthItems tests the sorting and delivery -// with weights per level -func TestAgendaItemListWithDepthItems(t *testing.T) { - s := new(projector.SlideStore) - slide.AgendaItemList(s) - slide.Topic(s) - - ailSlide := s.GetSlider("agenda_item_list") - assert.NotNilf(t, ailSlide, "Slide with name `agenda_item_list` not found.") - - data := dsmock.YAMLData(` - meeting: - 1: - agenda_show_internal_items_on_projector: false - agenda_item_ids: [1, 2, 3 ,4 ,5, 6, 7, 8] - agenda_item: - 1: - item_number: Ino1 - content_object_id: topic/1 - meeting_id: 1 - level: 0 - weight: 2 - child_ids: [2, 3] - 2: - item_number: Ino1.1 - content_object_id: topic/2 - meeting_id: 1 - level: 1 - weight: 3 - parent_id: 1 - child_ids: [4, 5] - 3: - item_number: Ino1.2 - content_object_id: topic/3 - meeting_id: 1 - level: 1 - weight: 4 - parent_id: 1 - child_ids: [] - 4: - item_number: Ino1.1.1 - content_object_id: topic/4 - meeting_id: 1 - level: 3 - parent_id: 2 - child_ids: [] - 5: - item_number: Ino1.1.2 - content_object_id: topic/5 - meeting_id: 1 - level: 2 - parent_id: 2 - child_ids: [] - 6: - item_number: Ino2 - content_object_id: topic/6 - meeting_id: 1 - level: 0 - weight: 3 - parent_id: 0 - child_ids: [7] - 7: - item_number: Ino2.1 - content_object_id: topic/7 - meeting_id: 1 - level: 1 - weight: 4 - parent_id: 6 - child_ids: [8] - 8: - item_number: Ino2.1.1 - content_object_id: topic/8 - meeting_id: 1 - level: 2 - weight: 5 - parent_id: 7 - child_ids: [] - - topic/1/title: topic title 1 - topic/2/title: topic title 1.1 - topic/3/title: topic title 1.2 - topic/4/title: topic title 1.1.1 - topic/5/title: topic title 1.1.2 - topic/6/title: topic title 2 - topic/7/title: topic title 2.1 - topic/8/title: topic title 2.1.1 - - `) - - for _, tt := range []struct { - name string - data map[dskey.Key][]byte - expect string - }{ - { - "with_leveled_item", - data, - `{ - "items": [ - { - "depth": 0, - "title_information": { - "collection": "topic", - "agenda_item_number": "Ino1", - "content_object_id": "topic/1", - "title": "topic title 1" - } - }, - { - "depth": 1, - "title_information": { - "collection": "topic", - "agenda_item_number": "Ino1.1", - "content_object_id": "topic/2", - "title": "topic title 1.1" - } - }, - { - "depth": 2, - "title_information": { - "collection": "topic", - "agenda_item_number": "Ino1.1.1", - "content_object_id": "topic/4", - "title": "topic title 1.1.1" - } - }, - { - "depth": 2, - "title_information": { - "collection": "topic", - "agenda_item_number": "Ino1.1.2", - "content_object_id": "topic/5", - "title": "topic title 1.1.2" - } - }, - { - "depth": 1, - "title_information": { - "collection": "topic", - "agenda_item_number": "Ino1.2", - "content_object_id": "topic/3", - "title": "topic title 1.2" - } - }, - { - "depth": 0, - "title_information": { - "collection": "topic", - "agenda_item_number": "Ino2", - "content_object_id": "topic/6", - "title": "topic title 2" - } - }, - { - "depth": 1, - "title_information": { - "collection": "topic", - "agenda_item_number": "Ino2.1", - "content_object_id": "topic/7", - "title": "topic title 2.1" - } - }, - { - "depth": 2, - "title_information": { - "collection": "topic", - "agenda_item_number": "Ino2.1.1", - "content_object_id": "topic/8", - "title": "topic title 2.1.1" - } - } - ] - } - `, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ds := dsmock.NewFlow(tt.data) - fetch := datastore.NewFetcher(ds) - - p7on := &projector.Projection{ - ContentObjectID: "meeting/1", - Type: "agenda_item_list", - MeetingID: 1, - } - - bs, err := ailSlide.Slide(context.Background(), fetch, p7on) - assert.NoError(t, err) - assert.NoError(t, fetch.Err()) - assert.JSONEq(t, tt.expect, string(bs)) - }) - } -} diff --git a/internal/projector/slide/assignment.go b/internal/projector/slide/assignment.go deleted file mode 100644 index f2937919..00000000 --- a/internal/projector/slide/assignment.go +++ /dev/null @@ -1,157 +0,0 @@ -package slide - -import ( - "context" - "encoding/json" - "fmt" - "sort" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" -) - -type dbAssignment struct { - ID int `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - NumberPollCandidates bool `json:"number_poll_candidates"` - CandidateIDs []int `json:"candidate_ids"` - AgendaItemID int `json:"agenda_item_id"` -} - -type dbAssignmentCandidate struct { - MeetingUserID int `json:"meeting_user_id"` - Weight int `json:"weight"` -} - -func assignmentFromMap(in map[string]json.RawMessage) (*dbAssignment, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding assignment data: %w", err) - } - - var a dbAssignment - if err := json.Unmarshal(bs, &a); err != nil { - return nil, fmt.Errorf("decoding assignment data: %w", err) - } - return &a, nil -} - -func assignmentCandidateFromMap(in map[string]json.RawMessage) (*dbAssignmentCandidate, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding assignment candidate data: %w", err) - } - - var ac dbAssignmentCandidate - if err := json.Unmarshal(bs, &ac); err != nil { - return nil, fmt.Errorf("decoding assignment candidate data: %w", err) - } - return &ac, nil -} - -// Assignment renders the assignment slide. -func Assignment(store *projector.SlideStore) { - store.RegisterSliderFunc("assignment", func(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection) (encoded []byte, err error) { - data := fetch.Object( - ctx, - p7on.ContentObjectID, - "id", - "title", - "description", - "number_poll_candidates", - "candidate_ids", - ) - - assignment, err := assignmentFromMap(data) - if err != nil { - return nil, fmt.Errorf("get assignment: %w", err) - } - - var allUsers []*dbAssignmentCandidate - for _, ac := range assignment.CandidateIDs { - data = fetch.Object(ctx, fmt.Sprintf("assignment_candidate/%d", ac), "meeting_user_id", "weight") - userWeight, err := assignmentCandidateFromMap(data) - if err != nil { - return nil, fmt.Errorf("get assignment candidate: %w", err) - } - allUsers = append(allUsers, userWeight) - } - - sort.SliceStable(allUsers, func(i, j int) bool { return allUsers[i].Weight < allUsers[j].Weight }) - - titler := store.GetTitleInformationFunc("user") - if titler == nil { - return nil, fmt.Errorf("no titler function registered for user") - } - - var users []string - for _, candidate := range allUsers { - var userID int - fetch.FetchIfExist(ctx, &userID, "meeting_user/%d/user_id", candidate.MeetingUserID) - if err := fetch.Err(); err != nil { - return nil, fmt.Errorf("getting user for meeting user %d: %w", candidate.MeetingUserID, err) - } - - user, err := NewUser(ctx, fetch, userID, p7on.MeetingID) - if err != nil { - return nil, fmt.Errorf("getting new user id: %w", err) - } - users = append(users, user.UserRepresentation(p7on.MeetingID)) - } - - out := struct { - Title string `json:"title"` - Description string `json:"description"` - NumberPollCandidates bool `json:"number_poll_candidates"` - Candidates []string `json:"candidates"` - }{ - Title: assignment.Title, - Description: assignment.Description, - NumberPollCandidates: assignment.NumberPollCandidates, - Candidates: users, - } - - responseValue, err := json.Marshal(out) - if err != nil { - return nil, fmt.Errorf("encoding response slide assignment: %w", err) - } - if err := fetch.Err(); err != nil { - return nil, err - } - return responseValue, nil - }) - - store.RegisterGetTitleInformationFunc("assignment", func(ctx context.Context, fetch *datastore.Fetcher, fqid string, itemNumber string, meetingID int) (json.RawMessage, error) { - data := fetch.Object(ctx, fqid, "id", "title", "agenda_item_id") - assignment, err := assignmentFromMap(data) - if err != nil { - return nil, fmt.Errorf("get assignment: %w", err) - } - - if itemNumber == "" && assignment.AgendaItemID > 0 { - itemNumber = datastore.String(ctx, fetch.FetchIfExist, "agenda_item/%d/item_number", assignment.AgendaItemID) - } - - title := struct { - Collection string `json:"collection"` - ContentObjectID string `json:"content_object_id"` - Title string `json:"title"` - AgendaItemNumber string `json:"agenda_item_number"` - }{ - "assignment", - fqid, - assignment.Title, - itemNumber, - } - - bs, err := json.Marshal(title) - if err != nil { - return nil, fmt.Errorf("encoding title: %w", err) - } - if err := fetch.Err(); err != nil { - return nil, err - } - return bs, nil - }) -} diff --git a/internal/projector/slide/assignment_test.go b/internal/projector/slide/assignment_test.go deleted file mode 100644 index 0f5e24cf..00000000 --- a/internal/projector/slide/assignment_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package slide_test - -import ( - "context" - "testing" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/slide" - "github.com/OpenSlides/openslides-go/datastore/dskey" - "github.com/OpenSlides/openslides-go/datastore/dsmock" - "github.com/stretchr/testify/assert" -) - -func TestAssignment(t *testing.T) { - s := new(projector.SlideStore) - slide.Assignment(s) - slide.User(s) - - assignmentSlide := s.GetSlider("assignment") - assert.NotNilf(t, assignmentSlide, "Slide with name `assignment` not found.") - - for _, tt := range []struct { - name string - data map[dskey.Key][]byte - expect string - }{ - { - "Assignment Complete", - dsmock.YAMLData(`--- - assignment/1: - id: 1 - title: "title 1" - description: "description 1" - number_poll_candidates: true - candidate_ids: [10,11] - - assignment_candidate: - 10: - id: 10 - meeting_user_id: 1100 - weight: 10 - 11: - id: 11 - meeting_user_id: 1110 - weight: 3 - - meeting_user: - 1100: - user_id: 110 - 1110: - user_id: 111 - - user: - 110: - id: 110 - username: "user110" - 111: - id: 111 - username: "user111" - `), - `{"title":"title 1", "description":"description 1","number_poll_candidates":true, "candidates":["user111", "user110"]}`, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - ds := dsmock.NewFlow(tt.data) - fetch := datastore.NewFetcher(ds) - - p7on := &projector.Projection{ - ContentObjectID: "assignment/1", - } - - bs, err := assignmentSlide.Slide(ctx, fetch, p7on) - assert.NoError(t, err) - assert.NoError(t, fetch.Err()) - assert.JSONEq(t, tt.expect, string(bs)) - }) - } -} diff --git a/internal/projector/slide/list_of_speakers.go b/internal/projector/slide/list_of_speakers.go deleted file mode 100644 index 3e802a1e..00000000 --- a/internal/projector/slide/list_of_speakers.go +++ /dev/null @@ -1,760 +0,0 @@ -package slide - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "sort" - "strconv" - "strings" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" - "github.com/OpenSlides/openslides-go/datastore/dskey" -) - -type dbListOfSpeakers struct { - SpeakerIDs []int `json:"speaker_ids"` - ContentObjectID string `json:"content_object_id"` - Closed bool `json:"closed"` -} - -func losFromMap(in map[string]json.RawMessage) (*dbListOfSpeakers, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding list of speakers data: %w", err) - } - - var los dbListOfSpeakers - if err := json.Unmarshal(bs, &los); err != nil { - return nil, fmt.Errorf("decoding list of speakers data: %w", err) - } - return &los, nil -} - -type dbSpeakerWork struct { - MeetingUserID int `json:"meeting_user_id"` - Weight int `json:"weight"` - EndTime int `json:"end_time"` - TotalPause int `json:"total_pause"` - StructureLevelListOfSpeakersID int `json:"structure_level_list_of_speakers_id"` -} -type dbSpeaker struct { - User string `json:"user"` - SpeechState string `json:"speech_state"` - Note string `json:"note"` - BeginTime int `json:"begin_time,omitempty"` - PauseTime int `json:"pause_time,omitempty"` - PointOfOrder bool `json:"point_of_order"` - Answer bool `json:"answer"` - SpeakerWork *dbSpeakerWork `json:",omitempty"` -} - -func speakerFromMap(in map[string]json.RawMessage) (*dbSpeaker, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding speaker data: %w", err) - } - - var speaker dbSpeaker - var work dbSpeakerWork - speaker.SpeakerWork = &work - if err := json.Unmarshal(bs, &speaker); err != nil { - return nil, fmt.Errorf("decoding speaker data: %w", err) - } - if err := json.Unmarshal(bs, &work); err != nil { - return nil, fmt.Errorf("decoding speaker work data: %w", err) - } - - return &speaker, nil -} - -type dbChyronProjector struct { - ChyronBackgroundColor string `json:"chyron_background_color"` - ChyronFontColor string `json:"chyron_font_color"` -} - -func chyronProjectorFromMap(in map[string]json.RawMessage) (*dbChyronProjector, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding chyron projector data: %w", err) - } - - var projector dbChyronProjector - if err := json.Unmarshal(bs, &projector); err != nil { - return nil, fmt.Errorf("decoding chyron projector data: %w", err) - } - return &projector, nil -} - -// ListOfSpeaker renders current list of speaker slide. -func ListOfSpeaker(store *projector.SlideStore) { - store.RegisterSliderFunc("list_of_speakers", func(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection) (encoded []byte, err error) { - return renderListOfSpeakers(ctx, fetch, p7on.ContentObjectID, p7on.MeetingID, store) - }) -} - -// CurrentListOfSpeakers renders the current_los slide. -func CurrentListOfSpeakers(store *projector.SlideStore) { - store.RegisterSliderFunc("current_los", func(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection) (encoded []byte, err error) { - losID, _, err := getLosID(ctx, p7on.ContentObjectID, fetch) - if err != nil { - return nil, fmt.Errorf("error in getLosID: %w", err) - } - if losID == 0 { - return []byte("{}"), nil - } - - if err := fetch.Err(); err != nil { - return nil, err - } - - content, err := renderListOfSpeakers(ctx, fetch, fmt.Sprintf("list_of_speakers/%d", losID), p7on.MeetingID, store) - if err != nil { - return nil, fmt.Errorf("render list of speakers %d: %w", losID, err) - } - if err := fetch.Err(); err != nil { - return nil, err - } - return content, nil - }) -} - -// CurrentSpeakerChyron renders the current_speaker_chyron slide. -func CurrentSpeakerChyron(store *projector.SlideStore) { - store.RegisterSliderFunc("current_speaker_chyron", func(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection) (encoded []byte, err error) { - losID, referenceProjectorID, err := getLosID(ctx, p7on.ContentObjectID, fetch) - if err != nil { - return nil, fmt.Errorf("error in getLosID: %w", err) - } - - projectorID := p7on.CurrentProjectorID - if projectorID <= 0 { - projectorID = referenceProjectorID - } - - meetingID, err := strconv.Atoi(strings.Split(p7on.ContentObjectID, "/")[1]) - if err != nil { - return nil, fmt.Errorf("error in Atoi with ContentObjectID: %w", err) - } - - projector := &dbChyronProjector{} - if projectorID > 0 { - data := fetch.Object(ctx, fmt.Sprintf("projector/%d", projectorID), "chyron_background_color", "chyron_font_color") - projector, err = chyronProjectorFromMap(data) - if err != nil { - return nil, fmt.Errorf("error in get chyron projector: %w", err) - } - } - - var shortName, structureLevel string - var titleInfo json.RawMessage - if losID > 0 { - shortName, structureLevel, err = getCurrentSpeakerData(ctx, fetch, losID, meetingID) - if err != nil { - return nil, fmt.Errorf("get CurrentSpeakerData: %w", err) - } - if err := fetch.Err(); err != nil { - return nil, err - } - - var losContentObject string - fetch.FetchIfExist(ctx, &losContentObject, "list_of_speakers/%d/content_object_id", losID) - if err := fetch.Err(); err != nil { - return nil, fmt.Errorf("getting los content object %d: %w", losID, err) - } - - parts := strings.Split(losContentObject, "/") - if len(parts) != 2 { - return nil, fmt.Errorf("invalid ContentObjectID %s. Expected two parts", losContentObject) - } - collection := parts[0] - - titler := store.GetTitleInformationFunc(collection) - if titler != nil { - titleInfo, err = titler.GetTitleInformation(ctx, fetch, losContentObject, "", meetingID) - if err != nil { - return nil, fmt.Errorf("get title func: %w", err) - } - } - } - - out := struct { - BackgroundColor string `json:"background_color"` - FontColor string `json:"font_color"` - TitleInformation json.RawMessage `json:"title_information,omitempty"` - CurrentSpeakerName string `json:"current_speaker_name"` - CurrentSpeakerLevel string `json:"current_speaker_level"` - }{ - projector.ChyronBackgroundColor, - projector.ChyronFontColor, - titleInfo, - shortName, - structureLevel, - } - - responseValue, err := json.Marshal(out) - if err != nil { - return nil, fmt.Errorf("encoding response slide current_speaker_chyron: %w", err) - } - return responseValue, nil - }) -} - -type dbStructureLevel struct { - Name string `json:"name"` - Color string `json:"color"` -} - -func structureLevelFromMap(in map[string]json.RawMessage) (*dbStructureLevel, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding motion data: %w", err) - } - - var m dbStructureLevel - if err := json.Unmarshal(bs, &m); err != nil { - return nil, fmt.Errorf("decoding motion: %w", err) - } - return &m, nil -} - -type dbStructureLevelListOfSpeakers struct { - SpeakerIDs []int `json:"speaker_ids"` - StructureLevelID int `json:"structure_level_id"` - InitialTime int `json:"initial_time"` - RemainingTime int `json:"remaining_time"` - AdditionalTime int `json:"additional_time"` - CurrentStartTime int `json:"current_start_time"` -} - -func structureLevelListOfSpeakersFromMap(in map[string]json.RawMessage) (*dbStructureLevelListOfSpeakers, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding motion data: %w", err) - } - - var m dbStructureLevelListOfSpeakers - if err := json.Unmarshal(bs, &m); err != nil { - return nil, fmt.Errorf("decoding motion: %w", err) - } - return &m, nil -} - -type structureLevelRepr struct { - ID int `json:"id"` - Name string `json:"name"` - Color string `json:"color"` - SpeechState string `json:"speech_state"` - PointOfOrder bool `json:"point_of_order"` - Answer bool `json:"answer"` - RemainingTime *int `json:"remaining_time,omitempty"` - CurrentStartTime int `json:"current_start_time"` -} - -// CurrentStructureLevelList renders the current_structure_level_list slide. -func CurrentStructureLevelList(store *projector.SlideStore) { - store.RegisterSliderFunc("current_structure_level_list", func(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection) (encoded []byte, err error) { - losID, _, err := getLosID(ctx, p7on.ContentObjectID, fetch) - if err != nil { - return nil, fmt.Errorf("error in getLosID: %w", err) - } - - var losContentObject string - fetch.Fetch(ctx, &losContentObject, "list_of_speakers/%d/content_object_id", losID) - if err := fetch.Err(); err != nil { - return nil, fmt.Errorf("getting content object for list of speakers %d: %w", losID, err) - } - - var title string - fetch.Fetch(ctx, &title, "%s/%s", losContentObject, "title") - if err := fetch.Err(); err != nil { - return nil, fmt.Errorf("getting title for list of speakers content object %s: %w", losContentObject, err) - } - - var structureLevelListOfSpeakersIds []int - fetch.Fetch(ctx, &structureLevelListOfSpeakersIds, "list_of_speakers/%d/structure_level_list_of_speakers_ids", losID) - if err := fetch.Err(); err != nil { - return nil, fmt.Errorf("getting structure_level_list_of_speakers_ids for list of speakers %d: %w", losID, err) - } - - structureLevels := []structureLevelRepr{} - for _, slsID := range structureLevelListOfSpeakersIds { - hasSpeaker, err := structureLevelHasSpeaker(ctx, fetch, slsID) - if err != nil { - return nil, fmt.Errorf("checking speakers structure level los %d for list of speakers %d: %w", slsID, losID, err) - } - - if !hasSpeaker { - continue - } - - slsData := fetch.Object(ctx, fmt.Sprintf("structure_level_list_of_speakers/%d", slsID), "structure_level_id", "remaining_time", "current_start_time") - sls, err := structureLevelListOfSpeakersFromMap(slsData) - if err != nil { - return nil, fmt.Errorf("parsing structure level los %d for list of speakers %d: %w", slsID, losID, err) - } - - slData := fetch.Object(ctx, fmt.Sprintf("structure_level/%d", sls.StructureLevelID), "name", "color") - sl, err := structureLevelFromMap(slData) - if err != nil { - return nil, fmt.Errorf("parsing structure level %d for list of speakers %d: %w", sls.StructureLevelID, losID, err) - } - - structureLevel := structureLevelRepr{ - ID: sls.StructureLevelID, - Name: sl.Name, - Color: sl.Color, - RemainingTime: &sls.RemainingTime, - CurrentStartTime: sls.CurrentStartTime, - } - structureLevels = append(structureLevels, structureLevel) - } - - out := struct { - Title string `json:"title"` - StructureLevels []structureLevelRepr `json:"structure_levels"` - }{ - title, - structureLevels, - } - - responseValue, err := json.Marshal(out) - if err != nil { - return nil, fmt.Errorf("encoding response slide current_speaker_chyron: %w", err) - } - return responseValue, nil - }) -} - -// CurrentSpeakingStructureLevel renders the current_speaking_structure_level slide. -func CurrentSpeakingStructureLevel(store *projector.SlideStore) { - store.RegisterSliderFunc("current_speaking_structure_level", func(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection) (encoded []byte, err error) { - losID, _, err := getLosID(ctx, p7on.ContentObjectID, fetch) - if err != nil { - return nil, fmt.Errorf("error in getLosID: %w", err) - } - - speaker, err := getStructureLevelData(ctx, fetch, losID) - if err != nil { - return nil, fmt.Errorf("error in getStructureLevelData: %w", err) - } - - if speaker != nil { - slsID := speaker.SpeakerWork.StructureLevelListOfSpeakersID - out := structureLevelRepr{ - SpeechState: speaker.SpeechState, - PointOfOrder: speaker.PointOfOrder, - Answer: speaker.Answer, - } - - if slsID != 0 { - slsData := fetch.Object(ctx, fmt.Sprintf("structure_level_list_of_speakers/%d", slsID), "structure_level_id", "remaining_time", "current_start_time") - sls, err := structureLevelListOfSpeakersFromMap(slsData) - if err != nil { - return nil, fmt.Errorf("parsing structure level los %d for list of speakers %d: %w", slsID, losID, err) - } - - slData := fetch.Object(ctx, fmt.Sprintf("structure_level/%d", sls.StructureLevelID), "name", "color") - sl, err := structureLevelFromMap(slData) - if err != nil { - return nil, fmt.Errorf("parsing structure level %d for list of speakers %d: %w", sls.StructureLevelID, losID, err) - } - - out.ID = sls.StructureLevelID - out.Name = sl.Name - out.Color = sl.Color - out.RemainingTime = &sls.RemainingTime - out.CurrentStartTime = sls.CurrentStartTime - } - - if speaker.SpeechState == "interposed_question" || speaker.SpeechState == "intervention" || speaker.PointOfOrder || slsID == 0 { - out.RemainingTime = nil - if speaker.SpeechState == "intervention" && !speaker.Answer { - meetingID := datastore.Int(ctx, fetch.FetchIfExist, "list_of_speakers/%d/meeting_id", losID) - if err := fetch.Err(); err != nil { - return nil, fmt.Errorf("Error loading meeting id from los %d %w", losID, err) - } - interventionTime := datastore.Int(ctx, fetch.FetchIfExist, "meeting/%d/list_of_speakers_intervention_time", meetingID) - if err := fetch.Err(); err != nil { - return nil, fmt.Errorf("Error loading intervention time from meeting %d %w", meetingID, err) - } - out.RemainingTime = &interventionTime - } - if speaker.PauseTime != 0 { - if out.RemainingTime == nil { - out.CurrentStartTime = 0 - if speaker.SpeechState == "intervention" { - timeSpoken := speaker.PauseTime - (speaker.BeginTime + speaker.SpeakerWork.TotalPause) - out.RemainingTime = &timeSpoken - } - } else { - out.CurrentStartTime = 0 - *out.RemainingTime -= speaker.PauseTime - (speaker.BeginTime + speaker.SpeakerWork.TotalPause) - } - } else { - out.CurrentStartTime = speaker.BeginTime + speaker.SpeakerWork.TotalPause - } - } - - responseValue, err := json.Marshal(out) - if err != nil { - return nil, fmt.Errorf("encoding response slide current_speaking_structure_level: %w", err) - } - return responseValue, nil - } - - return []byte("{}"), nil - }) -} - -func structureLevelHasSpeaker(ctx context.Context, fetch *datastore.Fetcher, structureLevelLosID int) (spoken bool, err error) { - data := fetch.Object(ctx, fmt.Sprintf("structure_level_list_of_speakers/%d", structureLevelLosID), "speaker_ids", "initial_time", "additional_time", "remaining_time", "current_start_time") - sllos, err := structureLevelListOfSpeakersFromMap(data) - if err != nil { - return false, fmt.Errorf("loading structure level list of speakers: %w", err) - } - - if sllos.InitialTime+sllos.AdditionalTime != sllos.RemainingTime || sllos.CurrentStartTime != 0 { - return true, nil - } - - for _, id := range sllos.SpeakerIDs { - speechState := datastore.String(ctx, fetch.FetchIfExist, "speaker/%d/speech_state", id) - if err := fetch.Err(); err != nil { - return false, fmt.Errorf("Error loading speach state %d %w", id, err) - } - - if speechState == "interposed_question" || speechState == "intervention" { - continue - } - - return true, nil - } - - return false, nil -} - -// getLosID determines the losID and first current_projection of the reference_projector. -func getLosID(ctx context.Context, ContentObjectID string, fetch *datastore.Fetcher) (losID int, referenceProjectorID int, err error) { - parts := strings.Split(ContentObjectID, "/") - if len(parts) != 2 || parts[0] != "meeting" { - return losID, referenceProjectorID, fmt.Errorf("invalid ContentObjectID %s. Expected a meeting-objectID", ContentObjectID) - } - meetingID, err := strconv.Atoi(parts[1]) - if err != nil { - return losID, referenceProjectorID, fmt.Errorf("invalid ContentObjectID %s. Expected a numeric meeting_id", ContentObjectID) - } - referenceProjectorID = datastore.Int(ctx, fetch.FetchIfExist, "meeting/%d/reference_projector_id", meetingID) - referenceP7onIDs := datastore.Ints(ctx, fetch.FetchIfExist, "projector/%d/current_projection_ids", referenceProjectorID) - if err := fetch.Err(); err != nil { - return losID, referenceProjectorID, err - } - - for _, pID := range referenceP7onIDs { - contentObjectID := datastore.String(ctx, fetch.FetchIfExist, "projection/%d/content_object_id", pID) - if err := fetch.Err(); err != nil { - return 0, 0, fmt.Errorf("fetching projection/%d/content_object_id: %w", pID, err) - } - - if contentObjectID == "" { - continue - } - losID = datastore.Int(ctx, fetch.FetchIfExist, "%s/list_of_speakers_id", contentObjectID) - if err := fetch.Err(); err != nil { - var errInvalidKey dskey.InvalidKeyError - if !errors.As(err, &errInvalidKey) { - return 0, 0, fmt.Errorf("%s/content_object_id: %w", contentObjectID, err) - } - } - - if losID != 0 { - break - } - } - - return losID, referenceProjectorID, nil -} - -func getStructureLevelData(ctx context.Context, fetch *datastore.Fetcher, losID int) (speaker *dbSpeaker, err error) { - data := fetch.Object(ctx, fmt.Sprintf("list_of_speakers/%d", losID), "speaker_ids", "content_object_id", "closed") - los, err := losFromMap(data) - if err != nil { - return nil, fmt.Errorf("loading list of speakers: %w", err) - } - - fields := []string{ - "begin_time", - "pause_time", - "end_time", - "total_pause", - "point_of_order", - "weight", - "speech_state", - "structure_level_list_of_speakers_id", - "answer", - } - - var fallbackSpeaker *dbSpeaker - for _, id := range los.SpeakerIDs { - speaker, err := speakerFromMap(fetch.Object(ctx, fmt.Sprintf("speaker/%d", id), fields...)) - if err != nil { - return nil, fmt.Errorf("loading speaker %d: %w", id, err) - } - - if (fallbackSpeaker == nil || fallbackSpeaker.SpeakerWork.Weight > speaker.SpeakerWork.Weight) && speaker.BeginTime != 0 && speaker.SpeakerWork.EndTime == 0 { - fallbackSpeaker = speaker - } - - if speaker.BeginTime == 0 || speaker.PauseTime != 0 || (speaker.BeginTime != 0 && speaker.SpeakerWork.EndTime != 0) { - continue - } - - return speaker, nil - } - - if fallbackSpeaker != nil { - return fallbackSpeaker, nil - } - - return nil, nil -} - -func getCurrentSpeakerData(ctx context.Context, fetch *datastore.Fetcher, losID int, meetingID int) (shortName string, structureLevel string, err error) { - data := fetch.Object(ctx, fmt.Sprintf("list_of_speakers/%d", losID), "speaker_ids", "content_object_id", "closed") - los, err := losFromMap(data) - if err != nil { - return "", "", fmt.Errorf("loading list of speakers: %w", err) - } - - fields := []string{ - "meeting_user_id", - "begin_time", - "pause_time", - "end_time", - } - - for _, id := range los.SpeakerIDs { - speaker, err := speakerFromMap(fetch.Object(ctx, fmt.Sprintf("speaker/%d", id), fields...)) - if err != nil { - return "", "", fmt.Errorf("loading speaker %d: %w", id, err) - } - - if speaker.BeginTime == 0 || speaker.PauseTime != 0 || (speaker.BeginTime != 0 && speaker.SpeakerWork.EndTime != 0) { - continue - } - - if speaker.SpeakerWork.MeetingUserID != 0 { - var userID int - fetch.FetchIfExist(ctx, &userID, "meeting_user/%d/user_id", speaker.SpeakerWork.MeetingUserID) - if err := fetch.Err(); err != nil { - return "", "", fmt.Errorf("getting user for meeting user %d: %w", speaker.SpeakerWork.MeetingUserID, err) - } - - user, err := NewUser(ctx, fetch, userID, meetingID) - if err != nil { - return "", "", fmt.Errorf("getting newUser: %w", err) - } - - structureLevelTime := datastore.Int(ctx, fetch.FetchIfExist, "meeting/%d/list_of_speakers_default_structure_level_time", meetingID) - structureLevelName := "" - if structureLevelTime > 0 { - var structureLevelListOfSpeakersID int - fetch.FetchIfExist(ctx, &structureLevelListOfSpeakersID, "speaker/%d/structure_level_list_of_speakers_id", id) - if err := fetch.Err(); err != nil { - return "", "", fmt.Errorf("getting structure level for speaker %d: %w", id, err) - } - - if structureLevelListOfSpeakersID != 0 { - var structureLevelID int - fetch.FetchIfExist(ctx, &structureLevelID, "structure_level_list_of_speakers/%d/structure_level_id", structureLevelListOfSpeakersID) - if err := fetch.Err(); err != nil { - return "", "", fmt.Errorf("getting structure level for structure_level_list_of_speakers %d: %w", structureLevelListOfSpeakersID, err) - } - - fetch.Fetch(ctx, &structureLevelName, "structure_level/%d/name", structureLevelID) - if err := fetch.Err(); err != nil { - return "", "", fmt.Errorf("getting name for structure level name %d: %w", structureLevelID, err) - } - } - } else { - var structureLevelIds []int - fetch.Fetch(ctx, &structureLevelIds, "meeting_user/%d/structure_level_ids", speaker.SpeakerWork.MeetingUserID) - if len(structureLevelIds) > 0 { - structureLevelNames := make([]string, len(structureLevelIds)) - for i, id := range structureLevelIds { - structureLevelNames[i] = datastore.String(ctx, fetch.FetchIfExist, "structure_level/%d/name", id) - } - sort.Strings(structureLevelNames) - structureLevelName = strings.Join(structureLevelNames, ", ") - } - } - - return user.UserShortName(), structureLevelName, nil - } - return "", "", nil - } - - return shortName, structureLevel, nil -} - -func renderListOfSpeakers(ctx context.Context, fetch *datastore.Fetcher, losFQID string, meetingID int, store *projector.SlideStore) (encoded []byte, err error) { - data := fetch.Object(ctx, losFQID, "speaker_ids", "content_object_id", "closed") - los, err := losFromMap(data) - if err != nil { - return nil, fmt.Errorf("loading list of speakers: %w", err) - } - - var speakersWaiting []dbSpeaker - var speakersFinished []dbSpeaker - currentSpeaker, numberOfWaitingSpeakers, err := getSpeakerLists(ctx, los, meetingID, fetch, &speakersWaiting, &speakersFinished) - if err != nil { - return nil, fmt.Errorf("getSpeakersList: %w", err) - } - - if err := fetch.Err(); err != nil { - return nil, err - } - - parts := strings.Split(los.ContentObjectID, "/") - if len(parts) != 2 { - return nil, fmt.Errorf("splitting ComtentObjectID: %w", err) - } - collection := parts[0] - if err != nil { - return nil, fmt.Errorf("get ID from ContentObjectID: %w", err) - } - - titler := store.GetTitleInformationFunc(collection) - if titler == nil { - return nil, fmt.Errorf("no titler function registered for %s", collection) - } - - titleInfo, err := titler.GetTitleInformation(ctx, fetch, los.ContentObjectID, "", meetingID) - if err != nil { - return nil, fmt.Errorf("get title func: %w", err) - } - - slideData := struct { - Waiting []dbSpeaker `json:"waiting"` - Current *dbSpeaker `json:"current,"` - Finished []dbSpeaker `json:"finished"` - TitleInformation json.RawMessage `json:"title_information"` - Closed bool `json:"closed"` - NumberOfWaitingSpeakers *int `json:"number_of_waiting_speakers,omitempty"` - }{ - speakersWaiting, - currentSpeaker, - speakersFinished, - titleInfo, - los.Closed, - numberOfWaitingSpeakers, - } - b, err := json.Marshal(slideData) - if err != nil { - return nil, fmt.Errorf("encoding outgoing data: %w", err) - } - if err := fetch.Err(); err != nil { - return nil, err - } - - return b, nil -} - -func getSpeakerLists(ctx context.Context, los *dbListOfSpeakers, meetingID int, fetch *datastore.Fetcher, speakersWaiting *[]dbSpeaker, speakersFinished *[]dbSpeaker) (*dbSpeaker, *int, error) { - fields := []string{ - "meeting_user_id", - "speech_state", - "note", - "point_of_order", - "answer", - "weight", - "begin_time", - "pause_time", - "end_time", - } - - var currentSpeaker *dbSpeaker - var numberOfWaitingSpeakers *int - for _, id := range los.SpeakerIDs { - speaker, err := speakerFromMap(fetch.Object(ctx, fmt.Sprintf("speaker/%d", id), fields...)) - if err != nil { - return nil, nil, fmt.Errorf("loading speaker: %w", err) - } - - if speaker.SpeakerWork.MeetingUserID != 0 { - var userID int - fetch.FetchIfExist(ctx, &userID, "meeting_user/%d/user_id", speaker.SpeakerWork.MeetingUserID) - if err := fetch.Err(); err != nil { - return nil, nil, fmt.Errorf("getting user for meeting user %d: %w", speaker.SpeakerWork.MeetingUserID, err) - } - - user, err := NewUser(ctx, fetch, userID, meetingID) - if err != nil { - return nil, nil, fmt.Errorf("loading user: %w", err) - } - - speaker.User = user.UserRepresentation(meetingID) - } - - if (speaker.BeginTime == 0 || speaker.SpeechState == "interposed_question") && speaker.SpeakerWork.EndTime == 0 { - *speakersWaiting = append(*speakersWaiting, *speaker) - continue - } - - if speaker.SpeakerWork.EndTime == 0 { - currentSpeaker = speaker - continue - } - - *speakersFinished = append(*speakersFinished, *speaker) - } - - // Sort ascending by weight - sort.Slice(*speakersWaiting, func(i, j int) bool { - if (*speakersWaiting)[i].SpeakerWork.Weight == (*speakersWaiting)[j].SpeakerWork.Weight { - return (*speakersWaiting)[i].SpeakerWork.MeetingUserID < (*speakersWaiting)[j].SpeakerWork.MeetingUserID - } - return (*speakersWaiting)[i].SpeakerWork.Weight < (*speakersWaiting)[j].SpeakerWork.Weight - }) - - // Sort descending by endtime to get lates at top position - sort.Slice(*speakersFinished, func(i, j int) bool { - return (*speakersFinished)[i].SpeakerWork.EndTime > (*speakersFinished)[j].SpeakerWork.EndTime - }) - - meeting, err := getMeeting(ctx, fetch, meetingID, []string{"list_of_speakers_amount_next_on_projector", "list_of_speakers_amount_last_on_projector", "list_of_speakers_show_amount_of_speakers_on_slide"}) - if err != nil { - return nil, nil, fmt.Errorf("reading meeting: %w", err) - } - if err := fetch.Err(); err != nil { - return nil, nil, err - } - - if meeting.ListOfSpeakersShowAmountOfSpeakersOnSlide { - number := len(*speakersWaiting) - numberOfWaitingSpeakers = &number - } - - if len(*speakersWaiting) >= 1 || len(*speakersFinished) >= 1 { - if len(*speakersWaiting) >= 1 && meeting.ListOfSpeakersAmountNextOnProjector >= 0 && meeting.ListOfSpeakersAmountNextOnProjector < len(*speakersWaiting) && meeting.ListOfSpeakersShowAmountOfSpeakersOnSlide { - *speakersWaiting = (*speakersWaiting)[:meeting.ListOfSpeakersAmountNextOnProjector] - } - if len(*speakersFinished) >= 1 && meeting.ListOfSpeakersAmountLastOnProjector >= 0 && meeting.ListOfSpeakersAmountLastOnProjector < len(*speakersFinished) { - *speakersFinished = (*speakersFinished)[:meeting.ListOfSpeakersAmountLastOnProjector] - } - } - - // Remove SpeakerWork's - for i := range *speakersWaiting { - (*speakersWaiting)[i].SpeakerWork = nil - } - for i := range *speakersFinished { - (*speakersFinished)[i].SpeakerWork = nil - } - if currentSpeaker != nil { - currentSpeaker.SpeakerWork = nil - } - return currentSpeaker, numberOfWaitingSpeakers, nil -} diff --git a/internal/projector/slide/list_of_speakers_test.go b/internal/projector/slide/list_of_speakers_test.go deleted file mode 100644 index 0224c417..00000000 --- a/internal/projector/slide/list_of_speakers_test.go +++ /dev/null @@ -1,521 +0,0 @@ -package slide_test - -import ( - "context" - "testing" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/slide" - "github.com/OpenSlides/openslides-go/datastore/dskey" - "github.com/OpenSlides/openslides-go/datastore/dsmock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestListOfSpeakers(t *testing.T) { - s := new(projector.SlideStore) - slide.ListOfSpeaker(s) - slide.Assignment(s) - - losSlide := s.GetSlider("list_of_speakers") - assert.NotNilf(t, losSlide, "Slide with name `list_of_speakers` not found.") - - data := dsmock.YAMLData(` - meeting/1: - list_of_speakers_amount_next_on_projector: 4 - list_of_speakers_amount_last_on_projector: 2 - list_of_speakers_show_amount_of_speakers_on_slide: true - list_of_speakers/1: - content_object_id: assignment/1 - closed: true - speaker_ids: [1,2,3,4,5,6] - - assignment/1: - title: assignment1 title - agenda_item_id: 1 - agenda_item/1/item_number: ItemNr Assignment1 - - speaker: - 1: - # Waiting - meeting_user_id: 100 - speech_state: contribution - note: Seq2Waiting - point_of_order: false - weight: 10 - 2: - # Waiting - meeting_user_id: 110 - speech_state: contribution - note: Seq1Waiting - point_of_order: true - weight: 5 - - 3: - # Current - meeting_user_id: 200 - speech_state: pro - note: SeqCurrent - point_of_order: false - weight: 20 - begin_time: 100 - - - 4: - # Finished - meeting_user_id: 300 - speech_state: contra - note: Seq3Finished - point_of_order: true - weight: 30 - begin_time: 20 - end_time: 23 - - 5: - # Finished - meeting_user_id: 310 - speech_state: contra - note: Seq1Finished - point_of_order: true - weight: 30 - begin_time: 29 - end_time: 32 - 6: - # Finished - meeting_user_id: 320 - speech_state: contra - note: Seq2Finished - point_of_order: true - weight: 30 - begin_time: 24 - end_time: 28 - 7: - # Waiting - meeting_user_id: 330 - speech_state: interposed_question - note: Seq2Waiting - point_of_order: false - weight: 20 - begin_time: 150 - - meeting_user: - 100: - user_id: 10 - - 110: - user_id: 11 - - 200: - user_id: 20 - - 300: - user_id: 30 - - 310: - user_id: 31 - - 320: - user_id: 32 - - 330: - user_id: 33 - - user: - 10: - username: jonny123 - 11: - username: elenor - 20: - first_name: Jonny - 30: - last_name: Bo - 31: - username: Ernest - 32: - username: Calli - 33: - username: Joe - `) - - for _, tt := range []struct { - name string - data map[dskey.Key][]byte - expect string - }{ - { - "Starter", - data, - `{ - "waiting": [ - { - "user": "elenor", - "speech_state": "contribution", - "note": "Seq1Waiting", - "point_of_order": true, - "answer": false - }, - { - "user": "jonny123", - "speech_state": "contribution", - "note": "Seq2Waiting", - "point_of_order": false, - "answer": false - } - ], - "current": { - "user": "Jonny", - "speech_state": "pro", - "note": "SeqCurrent", - "point_of_order": false, - "begin_time": 100, - "answer": false - }, - "finished": [ - { - "user": "Ernest", - "speech_state": "contra", - "note": "Seq1Finished", - "point_of_order": true, - "begin_time": 29, - "answer": false - }, - { - "user": "Calli", - "speech_state": "contra", - "note": "Seq2Finished", - "point_of_order": true, - "begin_time": 24, - "answer": false - } - ], - "closed": true, - "title_information": { - "agenda_item_number": "ItemNr Assignment1", - "collection": "assignment", - "content_object_id": "assignment/1", - "title": "assignment1 title" - }, - "number_of_waiting_speakers": 2 - } - `, - }, - { - "No Current speaker", - changeData(data, map[dskey.Key][]byte{ - dskey.MustKey("list_of_speakers/1/speaker_ids"): []byte("[1,4]"), - dskey.MustKey("meeting/1/list_of_speakers_show_amount_of_speakers_on_slide"): []byte("false"), - }), - `{ - "waiting": [{ - "user": "jonny123", - "speech_state": "contribution", - "note": "Seq2Waiting", - "point_of_order": false, - "answer": false - }], - "current": null, - "finished": [{ - "user": "Bo", - "speech_state": "contra", - "note": "Seq3Finished", - "point_of_order": true, - "begin_time": 20, - "answer": false - }], - "closed": true, - "title_information": { - "agenda_item_number": "ItemNr Assignment1", - "collection": "assignment", - "content_object_id": "assignment/1", - "title": "assignment1 title" - } - } - `, - }, - { - "Paused current speaker with interposed", - changeData(data, map[dskey.Key][]byte{ - dskey.MustKey("list_of_speakers/1/speaker_ids"): []byte("[3,7]"), - dskey.MustKey("meeting/1/list_of_speakers_show_amount_of_speakers_on_slide"): []byte("false"), - dskey.MustKey("speaker/3/pause_time"): []byte("150"), - }), - `{ - "waiting": [{ - "user": "Joe", - "speech_state": "interposed_question", - "note": "Seq2Waiting", - "point_of_order": false, - "begin_time": 150, - "answer": false - }], - "current": { - "user": "Jonny", - "speech_state": "pro", - "note": "SeqCurrent", - "point_of_order": false, - "begin_time": 100, - "pause_time": 150, - "answer": false - }, - "finished": null, - "closed": true, - "title_information": { - "agenda_item_number": "ItemNr Assignment1", - "collection": "assignment", - "content_object_id": "assignment/1", - "title": "assignment1 title" - } - } - `, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ds := dsmock.NewFlow(tt.data) - fetch := datastore.NewFetcher(ds) - - p7on := &projector.Projection{ - ContentObjectID: "list_of_speakers/1", - MeetingID: 1, - } - - bs, err := losSlide.Slide(context.Background(), fetch, p7on) - assert.NoError(t, err) - assert.NoError(t, fetch.Err()) - assert.JSONEq(t, tt.expect, string(bs)) - }) - } -} - -func getDataForCurrentList() map[dskey.Key][]byte { - // This one is a bit complicated and will be used - // for tests current_los and, slightly modified, - // for current_speaker_chyron - // - // The slide gets a contentObjectID of meeting/6 - // meeting/6 has reference_projector 60 - // projector/60 has projection/2 - // projection/2 has content_object_id topic/5 - // motion_block/1 points list_of_speakers/7 - // list_of_speakers/7 points to speaker/8 - // speaker/8 points to user/10 - // user/10 has username jonny123 - // - // lets find out if this username is on the slide-data... - return dsmock.YAMLData(` - projector/60/current_projection_ids: [1, 2] - projection/1/content_object_id: user/10 - projection/2/content_object_id: motion_block/1 - - meeting/6: - list_of_speakers_show_amount_of_speakers_on_slide: false - reference_projector_id: 60 - motion_block/1: - list_of_speakers_id: 7 - title: motion_block1 title - agenda_item_id: 1 - - list_of_speakers/7: - content_object_id: motion_block/1 - closed: true - speaker_ids: [8] - - speaker/8: - meeting_user_id: 100 - speech_state: pro - note: Lonesome speaker - point_of_order: false - weight: 10 - - meeting_user/100/user_id: 10 - user/10/username: jonny123 - agenda_item/1/item_number: ItemNr. MotionBlock1 - `) -} - -func TestCurrentListOfSpeakers(t *testing.T) { - closed := make(chan struct{}) - defer close(closed) - - s := new(projector.SlideStore) - slide.CurrentListOfSpeakers(s) - slide.MotionBlock(s) - - slide := s.GetSlider("current_los") - require.NotNilf(t, slide, "Slide with name `current_los` not found.") - - data := getDataForCurrentList() - for _, tt := range []struct { - name string - data map[dskey.Key][]byte - expect string - }{ - { - "find second current projection with speaker list", - data, - `{ - "waiting": [{ - "user": "jonny123", - "speech_state": "pro", - "note": "Lonesome speaker", - "point_of_order": false, - "answer": false - }], - "current": null, - "finished": null, - "closed": true, - "title_information": { - "agenda_item_number": "ItemNr. MotionBlock1", - "collection": "motion_block", - "content_object_id": "motion_block/1", - "title": "motion_block1 title" - } - } - `, - }, - { - "don't find speaker list in current projections", - changeData(data, map[dskey.Key][]byte{ - dskey.MustKey("motion_block/1/list_of_speakers_id"): []byte("0"), - }), - `{}`, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ds := dsmock.NewFlow(tt.data) - fetch := datastore.NewFetcher(ds) - - p7on := &projector.Projection{ - ID: 1, - ContentObjectID: "meeting/6", - Type: "current_los", - MeetingID: 6, - } - - bs, err := slide.Slide(context.Background(), fetch, p7on) - assert.NoError(t, err) - assert.NoError(t, fetch.Err()) - assert.JSONEq(t, tt.expect, string(bs)) - }) - } -} - -func TestCurrentSpeakerChyron(t *testing.T) { - closed := make(chan struct{}) - defer close(closed) - - s := new(projector.SlideStore) - slide.CurrentSpeakerChyron(s) - - slide := s.GetSlider("current_speaker_chyron") - require.NotNilf(t, slide, "Slide with name `current_speaker_chyron` not found.") - - data := getDataForCurrentList() - for k, v := range dsmock.YAMLData(` - speaker/8/begin_time: 100 - speaker/8/end_time: 0 - speaker/8/structure_level_list_of_speakers_id: 7 - - user/10: - title: Admiral - first_name: Don - last_name: Snyder - meeting_user_ids: [100] - - meeting_user/100: - meeting_id: 6 - structure_level_ids: [4,8] - - projector/60: - chyron_background_color: green - chyron_font_color: red - - structure_level/4: - name: "Level" - - structure_level/8: - name: "Foo" - - structure_level_list_of_speakers/7: - meeting_id: 6 - structure_level_id: 4 - speaker_ids: [8] - - meeting/6/list_of_speakers_default_structure_level_time: 1 - `) { - data[k] = v - } - - for _, tt := range []struct { - name string - data map[dskey.Key][]byte - expect string - }{ - { - "current speaker chyron test find second", - data, - `{ - "background_color": "green", - "font_color": "red", - "current_speaker_name": "Admiral Don Snyder", - "current_speaker_level": "Level" - } - `, - }, - { - "current speaker structure level countdown disabled", - changeData(data, map[dskey.Key][]byte{ - dskey.MustKey("meeting/6/list_of_speakers_default_structure_level_time"): []byte("0"), - }), - `{ - "background_color": "green", - "font_color": "red", - "current_speaker_name": "Admiral Don Snyder", - "current_speaker_level": "Foo, Level" - } - `, - }, - { - "current speaker chyron test no current projection", - changeData(data, map[dskey.Key][]byte{ - dskey.MustKey("motion_block/1/list_of_speakers_id"): []byte("0"), - }), - `{ - "background_color": "green", - "font_color": "red", - "current_speaker_name": "", - "current_speaker_level": "" - } - `, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ds := dsmock.NewFlow(tt.data) - fetch := datastore.NewFetcher(ds) - - p7on := &projector.Projection{ - ID: 1, - ContentObjectID: "meeting/6", - Type: "current_speaker_chyron", - CurrentProjectorID: 60, - MeetingID: 6, - } - - bs, err := slide.Slide(context.Background(), fetch, p7on) - assert.NoError(t, err) - assert.NoError(t, fetch.Err()) - assert.JSONEq(t, tt.expect, string(bs)) - }) - } -} - -func changeData(orig, change map[dskey.Key][]byte) map[dskey.Key][]byte { - out := make(map[dskey.Key][]byte) - for k, v := range orig { - out[k] = v - } - for k, v := range change { - out[k] = v - } - return out -} diff --git a/internal/projector/slide/meeting.go b/internal/projector/slide/meeting.go deleted file mode 100644 index 22ad0cf9..00000000 --- a/internal/projector/slide/meeting.go +++ /dev/null @@ -1,89 +0,0 @@ -package slide - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - "strings" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" -) - -type dbMeeting struct { - ID int `json:"id"` - MotionsEnableTextOnProjector bool `json:"motions_enable_text_on_projector"` - MotionsEnableReasonOnProjector bool `json:"motions_enable_reason_on_projector"` - MotionsShowReferringMotions bool `json:"motions_show_referring_motions"` - MotionsEnableRecommendationOnProjector bool `json:"motions_enable_recommendation_on_projector"` - MotionsRecommendationsBy string `json:"motions_recommendations_by"` - MotionsEnableSideboxOnProjector bool `json:"motions_enable_sidebox_on_projector"` - MotionsLineLength int `json:"motions_line_length"` - MotionsPreamble string `json:"motions_preamble"` - MotionsDefaultLineNumbering string `json:"motions_default_line_numbering"` - ListOfSpeakersAmountNextOnProjector int `json:"list_of_speakers_amount_next_on_projector"` - ListOfSpeakersAmountLastOnProjector int `json:"list_of_speakers_amount_last_on_projector"` - ListOfSpeakersShowAmountOfSpeakersOnSlide bool `json:"list_of_speakers_show_amount_of_speakers_on_slide"` - UsersPdfWLANSSID string `json:"users_pdf_wlan_ssid"` - UsersPdfWLANPassword string `json:"users_pdf_wlan_password"` - UsersPdfWLANEncryption string `json:"users_pdf_wlan_encryption"` -} - -func meetingFromMap(in map[string]json.RawMessage) (*dbMeeting, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding meeting data: %w", err) - } - - var me dbMeeting - if err := json.Unmarshal(bs, &me); err != nil { - return nil, fmt.Errorf("decoding meeting data: %w", err) - } - return &me, nil -} - -func getMeeting(ctx context.Context, fetch *datastore.Fetcher, meetingID int, fetchFields []string) (meeting *dbMeeting, err error) { - data := fetch.Object(ctx, fmt.Sprintf("meeting/%d", meetingID), fetchFields...) - if err := fetch.Err(); err != nil { - return nil, err - } - - meeting, err = meetingFromMap(data) - if err != nil { - return nil, fmt.Errorf("get meeting: %w", err) - } - return meeting, nil -} - -// WiFiAccessData renders the wifi_access_data slide. -func WiFiAccessData(store *projector.SlideStore) { - store.RegisterSliderFunc("wifi_access_data", func(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection) (encoded []byte, err error) { - meetingID, err := strconv.Atoi(strings.Split(p7on.ContentObjectID, "/")[1]) - if err != nil { - return nil, fmt.Errorf("convert string to int: %w", err) - } - - meeting, err := getMeeting(ctx, fetch, meetingID, []string{ - "users_pdf_wlan_ssid", - "users_pdf_wlan_password", - "users_pdf_wlan_encryption", - }) - - out := struct { - UsersPdfWLANSSID string `json:"users_pdf_wlan_ssid,omitempty"` - UsersPdfWLANPassword string `json:"users_pdf_wlan_password,omitempty"` - UsersPdfWLANEncryption string `json:"users_pdf_wlan_encryption,omitempty"` - }{ - meeting.UsersPdfWLANSSID, - meeting.UsersPdfWLANPassword, - meeting.UsersPdfWLANEncryption, - } - - responseValue, err := json.Marshal(out) - if err != nil { - return nil, fmt.Errorf("encoding response slide current_speaker_chyron: %w", err) - } - return responseValue, nil - }) -} diff --git a/internal/projector/slide/meeting_mediafile.go b/internal/projector/slide/meeting_mediafile.go deleted file mode 100644 index 39f356be..00000000 --- a/internal/projector/slide/meeting_mediafile.go +++ /dev/null @@ -1,102 +0,0 @@ -package slide - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" -) - -type dbMeetingMediafile struct { - ID int `json:"id"` - MediafileID int `json:"mediafile_id"` -} - -type dbMediafile struct { - ID int `json:"id"` - Title string `json:"title"` - Mimetype string `json:"mimetype"` -} - -func meetingMediafileItemFromMap(in map[string]json.RawMessage) (*dbMeetingMediafile, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding mediafile item data: %w", err) - } - - var mf dbMeetingMediafile - if err := json.Unmarshal(bs, &mf); err != nil { - return nil, fmt.Errorf("decoding mediafile item data: %w", err) - } - return &mf, nil -} - -func mediafileItemFromMap(in map[string]json.RawMessage) (*dbMediafile, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding mediafile item data: %w", err) - } - - var mf dbMediafile - if err := json.Unmarshal(bs, &mf); err != nil { - return nil, fmt.Errorf("decoding mediafile item data: %w", err) - } - return &mf, nil -} - -// MeetingMediafile renders the mediafile slide. -func MeetingMediafile(store *projector.SlideStore) { - store.RegisterSliderFunc("meeting_mediafile", func(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection) (encoded []byte, err error) { - meetingMediafileData := fetch.Object(ctx, p7on.ContentObjectID, "id", "mediafile_id") - if err := fetch.Err(); err != nil { - return nil, err - } - meetingMediafile, err := meetingMediafileItemFromMap(meetingMediafileData) - data := fetch.Object(ctx, "mediafile/"+strconv.Itoa(meetingMediafile.MediafileID), "id", "mimetype") - if err := fetch.Err(); err != nil { - return nil, err - } - mediafile, err := mediafileItemFromMap(data) - if err != nil { - return nil, fmt.Errorf("get mediafile item from map: %w", err) - } - responseValue, err := json.Marshal(map[string]interface{}{"id": mediafile.ID, "mimetype": mediafile.Mimetype}) - if err != nil { - return nil, fmt.Errorf("encoding response slide mediafile item: %w", err) - } - return responseValue, err - }) - - store.RegisterGetTitleInformationFunc("meeting_mediafile", func(ctx context.Context, fetch *datastore.Fetcher, fqid string, itemNumber string, meetingID int) (json.RawMessage, error) { - meetingMediafileData := fetch.Object(ctx, fqid, "id", "mediafile_id") - if err := fetch.Err(); err != nil { - return nil, err - } - meetingMediafile, err := meetingMediafileItemFromMap(meetingMediafileData) - mediafileFqid := "mediafile/" + strconv.Itoa(meetingMediafile.MediafileID) - data := fetch.Object(ctx, mediafileFqid, "id", "mediafile_id") - mediafile, err := mediafileItemFromMap(data) - if err != nil { - return nil, fmt.Errorf("get mediafile: %w", err) - } - - mediafiletitle := struct { - Collection string `json:"collection"` - ContentObjectID string `json:"content_object_id"` - Title string `json:"title"` - }{ - "mediafile", - mediafileFqid, - mediafile.Title, - } - - bs, err := json.Marshal(mediafiletitle) - if err != nil { - return nil, fmt.Errorf("decoding title: %w", err) - } - return bs, err - }) -} diff --git a/internal/projector/slide/meeting_mediafile_test.go b/internal/projector/slide/meeting_mediafile_test.go deleted file mode 100644 index 2aa11e2d..00000000 --- a/internal/projector/slide/meeting_mediafile_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package slide_test - -import ( - "context" - "testing" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/slide" - "github.com/OpenSlides/openslides-go/datastore/dskey" - "github.com/OpenSlides/openslides-go/datastore/dsmock" - "github.com/stretchr/testify/assert" -) - -func TestMediafile(t *testing.T) { - s := new(projector.SlideStore) - slide.MeetingMediafile(s) - - mfSlide := s.GetSlider("meeting_mediafile") - assert.NotNilf(t, mfSlide, "Slide with name `mediafile` not found.") - - data := dsmock.YAMLData(` - meeting_mediafile/1/mediafile_id: 1 - mediafile/1/mimetype: application/pdf - `) - - for _, tt := range []struct { - name string - data map[dskey.Key][]byte - expect string - }{ - { - "Starter", - data, - `{ - "id": 1, - "mimetype": "application/pdf" - }`, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ds := dsmock.NewFlow(tt.data) - fetch := datastore.NewFetcher(ds) - - p7on := &projector.Projection{ - ContentObjectID: "meeting_mediafile/1", - } - - bs, err := mfSlide.Slide(context.Background(), fetch, p7on) - assert.NoError(t, err) - assert.JSONEq(t, tt.expect, string(bs)) - }) - } -} diff --git a/internal/projector/slide/meeting_test.go b/internal/projector/slide/meeting_test.go deleted file mode 100644 index 99fccec0..00000000 --- a/internal/projector/slide/meeting_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package slide_test - -import ( - "context" - "testing" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/slide" - "github.com/OpenSlides/openslides-go/datastore/dskey" - "github.com/OpenSlides/openslides-go/datastore/dsmock" - "github.com/stretchr/testify/assert" -) - -func TestMeeting(t *testing.T) { - s := new(projector.SlideStore) - slide.WiFiAccessData(s) - - wifiSlide := s.GetSlider("wifi_access_data") - assert.NotNilf(t, wifiSlide, "Slide with name `wifi_access_data` not found.") - - for _, tt := range []struct { - name string - data map[dskey.Key][]byte - expect string - }{ - { - "All data filled in", - dsmock.YAMLData(` - meeting/1: - users_pdf_wlan_encryption: WPA - users_pdf_wlan_password: Super&StrongP455Word - users_pdf_wlan_ssid: RandomWiWi - `), - `{ - "users_pdf_wlan_encryption": "WPA", - "users_pdf_wlan_password": "Super&StrongP455Word", - "users_pdf_wlan_ssid": "RandomWiWi" - } - `, - }, - { - "Password missing", - dsmock.YAMLData(` - meeting/1: - users_pdf_wlan_encryption: WPA - users_pdf_wlan_ssid: RandomWiWi - `), - `{ - "users_pdf_wlan_encryption": "WPA", - "users_pdf_wlan_ssid": "RandomWiWi" - } - `, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ds := dsmock.Stub(tt.data) - fetch := datastore.NewFetcher(ds) - - p7on := &projector.Projection{ - ContentObjectID: "meeting/1", - MeetingID: 1, - } - - bs, err := wifiSlide.Slide(context.Background(), fetch, p7on) - assert.NoError(t, err) - assert.NoError(t, fetch.Err()) - assert.JSONEq(t, tt.expect, string(bs)) - }) - } -} diff --git a/internal/projector/slide/mock_test.go b/internal/projector/slide/mock_test.go deleted file mode 100644 index 8ecbd372..00000000 --- a/internal/projector/slide/mock_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package slide_test - -import ( - "github.com/OpenSlides/openslides-go/datastore/dskey" -) - -func convertData(data map[string]string) map[dskey.Key][]byte { - converted := make(map[dskey.Key][]byte, len(data)) - for k, v := range data { - key := dskey.MustKey(k) - converted[key] = []byte(v) - } - return converted -} diff --git a/internal/projector/slide/motion.go b/internal/projector/slide/motion.go deleted file mode 100644 index f7d4a154..00000000 --- a/internal/projector/slide/motion.go +++ /dev/null @@ -1,485 +0,0 @@ -package slide - -import ( - "context" - "encoding/json" - "fmt" - "sort" - "strings" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" -) - -type dbMotionState struct { - RecommendationLabel string `json:"recommendation_label"` - CSSClass string `json:"css_class"` - MotionStateWork *dbMotionStateWork `json:",omitempty"` -} -type dbMotionStateWork struct { - ShowRecommendationExtensionField bool `json:"show_recommendation_extension_field"` -} - -func motionStateFromMap(in map[string]json.RawMessage) (*dbMotionState, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding motion state data: %w", err) - } - - var ms dbMotionState - var msWork dbMotionStateWork - ms.MotionStateWork = &msWork - if err := json.Unmarshal(bs, &ms); err != nil { - return nil, fmt.Errorf("decoding motion state data: %w", err) - } - if err := json.Unmarshal(bs, &msWork); err != nil { - return nil, fmt.Errorf("decoding motion state work data: %w", err) - } - return &ms, nil -} - -type dbMotionChangeRecommendation struct { - ID int `json:"id"` - Rejected bool `json:"rejected"` - Type string `json:"type"` - OtherDescription string `json:"other_description"` - LineFrom int `json:"line_from"` - LineTo int `json:"line_to"` - Text string `json:"text"` - CreationTime int `json:"creation_time"` -} - -type amendmentsType struct { - ID int `json:"id"` - Title string `json:"title"` - Number string `json:"number"` - AmendmentParagraph json.RawMessage `json:"amendment_paragraphs"` - ChangeRecommendations []dbMotionChangeRecommendation `json:"change_recommendations"` - MergeAmendmentIntoFinal string `json:"merge_amendment_into_final"` - MergeAmendmentIntoDiff string `json:"merge_amendment_into_diff"` -} -type leadMotionType struct { - Title string `json:"title"` - Number string `json:"number"` - Text string `json:"text,omitempty"` - StartLineNumber int `json:"start_line_number,omitempty"` -} -type dbMotionWork struct { - MeetingID int `json:"meeting_id"` - LeadMotionID int `json:"lead_motion_id"` - ChangeRecommendationIDS []int `json:"change_recommendation_ids"` - AmendmentIDS []int `json:"amendment_ids"` - SubmitterIDS []int `json:"submitter_ids"` - ReferencedInMotionRecommendationExtensionIDS []int `json:"referenced_in_motion_recommendation_extension_ids"` - RecommendationID int `json:"recommendation_id"` - RecommendationExtensionReferenceIDS []string `json:"recommendation_extension_reference_ids"` - RecommendationExtension string `json:"recommendation_extension"` - StateID int `json:"state_id"` - AgendaItemID int `json:"agenda_item_id"` -} -type dbMotion struct { - ID int `json:"id"` - Title string `json:"title"` - Number string `json:"number"` - Submitters []string `json:"submitters"` - AdditionalSubmitter string `json:"additional_submitter,omitempty"` - ShowSidebox bool `json:"show_sidebox"` - LineLength int `json:"line_length"` - Preamble string `json:"preamble,omitempty"` - LineNumbering string `json:"line_numbering"` - AmendmentParagraph json.RawMessage `json:"amendment_paragraphs,omitempty"` - LeadMotion *leadMotionType `json:"lead_motion,omitempty"` - ChangeRecommendations []dbMotionChangeRecommendation `json:"change_recommendations"` - Amendments []amendmentsType `json:"amendments,omitempty"` - RecommendationReferencingMotions []json.RawMessage `json:"recommendation_referencing_motions,omitempty"` - RecommendationLabel string `json:"recommendation_label,omitempty"` - RecommendationExtension string `json:"recommendation_extension,omitempty"` - RecommendationReferencedMotions map[string]json.RawMessage `json:"recommendation_referenced_motions,omitempty"` - Recommender string `json:"recommender,omitempty"` - Text string `json:"text,omitempty"` - Reason string `json:"reason,omitempty"` - ModifiedFinalVersion string `json:"modified_final_version,omitempty"` - StartLineNumber int `json:"start_line_number,omitempty"` - MotionWork *dbMotionWork `json:",omitempty"` -} - -func motionFromMap(in map[string]json.RawMessage) (*dbMotion, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding motion data: %w", err) - } - - var mo dbMotion - var moWork dbMotionWork - mo.MotionWork = &moWork - if err := json.Unmarshal(bs, &mo); err != nil { - return nil, fmt.Errorf("decoding motion data: %w", err) - } - if err := json.Unmarshal(bs, &moWork); err != nil { - return nil, fmt.Errorf("decoding motion work data: %w", err) - } - return &mo, nil -} - -// Motion renders the motion slide. -func Motion(store *projector.SlideStore) { - store.RegisterSliderFunc("motion", func(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection) (encoded []byte, err error) { - meeting, err := getMeeting(ctx, fetch, p7on.MeetingID, []string{ - "motions_enable_text_on_projector", - "motions_enable_reason_on_projector", - "motions_show_referring_motions", - "motions_enable_recommendation_on_projector", - "motions_recommendations_by", - "motions_enable_sidebox_on_projector", - "motions_line_length", - "motions_preamble", - "motions_default_line_numbering", - }) - if err != nil { - return nil, fmt.Errorf("getMeeting: %w", err) - } - - if !meeting.MotionsEnableTextOnProjector { - meeting.MotionsPreamble = "" - } - - var options struct { - Mode string `json:"mode"` - } - if p7on.Options != nil { - if err := json.Unmarshal(p7on.Options, &options); err != nil { - return nil, fmt.Errorf("decoding projection options: %w", err) - } - } - - fetchFields := []string{ - "id", - "title", - "number", - "meeting_id", - "lead_motion_id", - "change_recommendation_ids", - "amendment_ids", - "submitter_ids", - "referenced_in_motion_recommendation_extension_ids", - "recommendation_id", - "recommendation_extension", - "recommendation_extension_reference_ids", - "start_line_number", - "additional_submitter", - } - if meeting.MotionsEnableTextOnProjector { - fetchFields = append(fetchFields, "text", "amendment_paragraphs") - } - if meeting.MotionsEnableReasonOnProjector { - fetchFields = append(fetchFields, "reason") - } - if p7on.Options != nil && (options.Mode == "final" || options.Mode == "modified_final_version") { - fetchFields = append(fetchFields, "modified_final_version") - } - - data := fetch.Object(ctx, p7on.ContentObjectID, fetchFields...) - - motion, err := motionFromMap(data) - if err != nil { - return nil, fmt.Errorf("get motion: %w", err) - } - motion.RecommendationExtension = "" // will be (re-)filled conditionally - - fillMotionFromMeeting(motion, meeting) - - if err := fillSubmitters(ctx, fetch, motion); err != nil { - return nil, fmt.Errorf("fillSubmitters: %w", err) - } - - if err := fillLeadMotion(ctx, fetch, motion, meeting); err != nil { - return nil, fmt.Errorf("fillLeadMotion: %w", err) - } - - if err := fillChangeRecommendations(ctx, fetch, motion); err != nil { - return nil, fmt.Errorf("fillChangeRecommendations: %w", err) - } - - if err := fillAmendments(ctx, fetch, motion); err != nil { - return nil, fmt.Errorf("fillAmendments: %w", err) - } - - titlerMotion := store.GetTitleInformationFunc("motion") - if titlerMotion == nil { - return nil, fmt.Errorf("no titler function registered for motion") - } - - if meeting.MotionsShowReferringMotions && len(motion.MotionWork.ReferencedInMotionRecommendationExtensionIDS) > 0 { - err = fillRecommendationReferencingMotions(ctx, fetch, titlerMotion, motion) - if err != nil { - return nil, fmt.Errorf("FillRecommendationReferencingMotions: %w", err) - } - } - - if meeting.MotionsEnableRecommendationOnProjector && motion.MotionWork.RecommendationID > 0 { - err = fillRecommendationLabelEtc(ctx, fetch, titlerMotion, motion, meeting) - if err != nil { - return nil, fmt.Errorf("RecommendationLabelEtc: %w", err) - } - } - if err := fetch.Err(); err != nil { - return nil, err - } - - motion.MotionWork = nil // do not export worker fields - responseValue, err := json.Marshal(motion) - if err != nil { - return nil, fmt.Errorf("encoding response for slide motion: %w", err) - } - return responseValue, err - }) - - store.RegisterGetTitleInformationFunc("motion", func(ctx context.Context, fetch *datastore.Fetcher, fqid string, itemNumber string, meetingID int) (json.RawMessage, error) { - data := fetch.Object(ctx, fqid, "id", "number", "title", "agenda_item_id") - motion, err := motionFromMap(data) - if err != nil { - return nil, fmt.Errorf("get motion: %w", err) - } - - if itemNumber == "" && motion.MotionWork.AgendaItemID > 0 { - itemNumber = datastore.String(ctx, fetch.FetchIfExist, "agenda_item/%d/item_number", motion.MotionWork.AgendaItemID) - } - if err := fetch.Err(); err != nil { - return nil, err - } - - title := struct { - Collection string `json:"collection"` - ContentObjectID string `json:"content_object_id"` - Title string `json:"title"` - Number string `json:"number"` - AgendaItemNumber string `json:"agenda_item_number"` - }{ - "motion", - fqid, - motion.Title, - motion.Number, - itemNumber, - } - - bs, err := json.Marshal(title) - if err != nil { - return nil, fmt.Errorf("encoding title: %w", err) - } - return bs, err - }) -} - -// fillMotionFrom Meeting transfers the needed values from meeting object to motion object. -func fillMotionFromMeeting(motion *dbMotion, meeting *dbMeeting) { - motion.ShowSidebox = meeting.MotionsEnableSideboxOnProjector - motion.LineLength = meeting.MotionsLineLength - motion.Preamble = meeting.MotionsPreamble - motion.LineNumbering = meeting.MotionsDefaultLineNumbering -} - -func fillLeadMotion(ctx context.Context, fetch *datastore.Fetcher, motion *dbMotion, meeting *dbMeeting) error { - if motion.MotionWork.LeadMotionID == 0 { - return nil - } - fields := []string{ - "title", - "number", - "start_line_number", - } - if meeting.MotionsEnableTextOnProjector { - fields = append(fields, "text") - } - data := fetch.Object(ctx, fmt.Sprintf("motion/%d", motion.MotionWork.LeadMotionID), fields...) - bs, err := json.Marshal(data) - if err != nil { - return fmt.Errorf("encoding LeadMotion data: %w", err) - } - var mo leadMotionType - if err := json.Unmarshal(bs, &mo); err != nil { - return fmt.Errorf("decoding LeadMotion data: %w", err) - } - motion.LeadMotion = &mo - return nil -} - -func fillChangeRecommendations(ctx context.Context, fetch *datastore.Fetcher, motion *dbMotion) error { - if len(motion.MotionWork.ChangeRecommendationIDS) == 0 { - return nil - } - for _, id := range motion.MotionWork.ChangeRecommendationIDS { - data := fetch.Object( - ctx, - fmt.Sprintf("motion_change_recommendation/%d", id), - "id", - "rejected", - "type", - "other_description", - "line_from", - "line_to", - "text", - "creation_time", - "internal", - ) - var internal bool - if err := json.Unmarshal(data["internal"], &internal); err != nil { - return fmt.Errorf("decoding internal from ChangeRecommendations: %w", err) - } - if internal { - continue - } - bs, err := json.Marshal(data) - if err != nil { - return fmt.Errorf("encoding ChangeRecommendations data: %w", err) - } - var mo dbMotionChangeRecommendation - if err := json.Unmarshal(bs, &mo); err != nil { - return fmt.Errorf("decoding ChangeRecommendations data: %w", err) - } - motion.ChangeRecommendations = append(motion.ChangeRecommendations, mo) - } - return nil -} - -func fillRecommendationReferencingMotions(ctx context.Context, fetch *datastore.Fetcher, titler projector.Titler, motion *dbMotion) error { - for _, id := range motion.MotionWork.ReferencedInMotionRecommendationExtensionIDS { - fqid := fmt.Sprintf("motion/%d", id) - title, err := titler.GetTitleInformation(ctx, fetch, fqid, "", motion.MotionWork.MeetingID) - if err != nil { - return fmt.Errorf("encoding GetTitleInformation data: %w", err) - } - motion.RecommendationReferencingMotions = append(motion.RecommendationReferencingMotions, title) - } - return nil -} - -func fillRecommendationLabelEtc(ctx context.Context, fetch *datastore.Fetcher, titler projector.Titler, motion *dbMotion, meeting *dbMeeting) error { - data := fetch.Object( - ctx, - fmt.Sprintf("motion_state/%d", motion.MotionWork.RecommendationID), - "recommendation_label", - "show_recommendation_extension_field", - ) - st, err := motionStateFromMap(data) - if err != nil { - return fmt.Errorf("get motion state: %w", err) - } - motion.RecommendationLabel = st.RecommendationLabel - if st.MotionStateWork.ShowRecommendationExtensionField { - motion.RecommendationExtension = motion.MotionWork.RecommendationExtension - motion.RecommendationReferencedMotions = make(map[string]json.RawMessage, len(motion.MotionWork.RecommendationExtensionReferenceIDS)) - for _, fqid := range motion.MotionWork.RecommendationExtensionReferenceIDS { - parts := strings.Split(fqid, "/") - collection := parts[0] - if collection != "motion" { - return fmt.Errorf("implementation of RecommendationReferencedMotions includes only motion-collection, but not %s", collection) - } - title, err := titler.GetTitleInformation(ctx, fetch, fqid, "", motion.MotionWork.MeetingID) - if err != nil { - return fmt.Errorf("encoding GetTitleInformation data: %w", err) - } - motion.RecommendationReferencedMotions[fqid] = title - } - - } - - motion.Recommender = meeting.MotionsRecommendationsBy - - return nil -} - -func fillSubmitters(ctx context.Context, fetch *datastore.Fetcher, motion *dbMotion) error { - type submitterSort struct { - MeetingUserID int `json:"meeting_user_id"` - Weight int `json:"weight"` - } - var submitterToSort []*submitterSort - - for _, id := range motion.MotionWork.SubmitterIDS { - data := fetch.Object(ctx, fmt.Sprintf("motion_submitter/%d", id), "meeting_user_id", "weight") - bs, err := json.Marshal(data) - if err != nil { - return fmt.Errorf("encoding MotionSubmitter data: %w", err) - } - - var su submitterSort - if err := json.Unmarshal(bs, &su); err != nil { - return fmt.Errorf("decoding MotionSubmitter data: %w", err) - } - submitterToSort = append(submitterToSort, &su) - } - - sort.Slice(submitterToSort, func(i, j int) bool { - if submitterToSort[i].Weight == submitterToSort[j].Weight { - return submitterToSort[i].MeetingUserID < submitterToSort[j].MeetingUserID - } - return submitterToSort[i].Weight < submitterToSort[j].Weight - }) - - for _, sortedSub := range submitterToSort { - var userID int - fetch.Fetch(ctx, &userID, "meeting_user/%d/user_id", sortedSub.MeetingUserID) - if err := fetch.Err(); err != nil { - return fmt.Errorf("getting user for meeting user %d: %w", sortedSub.MeetingUserID, err) - } - - user, err := NewUser(ctx, fetch, userID, motion.MotionWork.MeetingID) - if err != nil { - return fmt.Errorf("getting new user id: %w", err) - } - motion.Submitters = append(motion.Submitters, user.UserRepresentation(motion.MotionWork.MeetingID)) - } - return nil -} - -func fillAmendments(ctx context.Context, fetch *datastore.Fetcher, motion *dbMotion) error { - fetchFields := []string{ - "id", - "title", - "number", - "meeting_id", - "amendment_paragraphs", - "state_id", - "recommendation_id", - "change_recommendation_ids", - } - for _, id := range motion.MotionWork.AmendmentIDS { - data := fetch.Object(ctx, fmt.Sprintf("motion/%d", id), fetchFields...) - motionAmend, err := motionFromMap(data) - if err != nil { - return fmt.Errorf("motionFromMap: %w", err) - } - - if err := fillChangeRecommendations(ctx, fetch, motionAmend); err != nil { - return fmt.Errorf("fill change recommendations: %w", err) - } - - var amendment amendmentsType - amendment.ID = id - amendment.Title = motionAmend.Title - amendment.Number = motionAmend.Number - amendment.AmendmentParagraph = motionAmend.AmendmentParagraph - amendment.ChangeRecommendations = motionAmend.ChangeRecommendations - - maif := datastore.String(ctx, fetch.FetchIfExist, "motion_state/%d/merge_amendment_into_final", motionAmend.MotionWork.StateID) - if maif == "do_merge" { - amendment.MergeAmendmentIntoFinal = maif - amendment.MergeAmendmentIntoDiff = maif - } else { - amendment.MergeAmendmentIntoFinal = "undefined" - if maif == "do_not_merge" || motionAmend.MotionWork.RecommendationID == 0 { - amendment.MergeAmendmentIntoDiff = "undefined" - } else { - maifReco := datastore.String(ctx, fetch.FetchIfExist, "motion_state/%d/merge_amendment_into_final", motionAmend.MotionWork.RecommendationID) - if maifReco == "do_merge" { - amendment.MergeAmendmentIntoDiff = maifReco - } else { - amendment.MergeAmendmentIntoDiff = "undefined" - } - } - } - - motion.Amendments = append(motion.Amendments, amendment) - } - return nil -} diff --git a/internal/projector/slide/motion_block.go b/internal/projector/slide/motion_block.go deleted file mode 100644 index e551db24..00000000 --- a/internal/projector/slide/motion_block.go +++ /dev/null @@ -1,164 +0,0 @@ -package slide - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" -) - -type dbMotionBlock struct { - ID int `json:"id"` - Title string `json:"title"` - AgendaItemID int `json:"agenda_item_id"` - MotionIDS []int `json:"motion_ids"` -} - -func motionBlockFromMap(in map[string]json.RawMessage) (*dbMotionBlock, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding motion data: %w", err) - } - - var m dbMotionBlock - if err := json.Unmarshal(bs, &m); err != nil { - return nil, fmt.Errorf("decoding motion: %w", err) - } - return &m, nil -} - -type motionRepr struct { - Title string `json:"title"` - Number string `json:"number"` - AgendaItemNumber string `json:"agenda_item_number,omitempty"` - Recommendation *dbMotionState `json:"recommendation,omitempty"` - RecommendationExtension *string `json:"recommendation_extension,omitempty"` -} - -// MotionBlock renders the motion_block slide. -func MotionBlock(store *projector.SlideStore) { - store.RegisterSliderFunc("motion_block", func(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection) (encoded []byte, err error) { - titlerMotion := store.GetTitleInformationFunc("motion") - if titlerMotion == nil { - return nil, fmt.Errorf("no titler function registered for motion") - } - - data := fetch.Object(ctx, p7on.ContentObjectID, "title", "motion_ids") - motionBlock, err := motionBlockFromMap(data) - if err != nil { - return nil, fmt.Errorf("get motionBlock: %w", err) - } - var motions []motionRepr - referenced := map[string]json.RawMessage{} - for _, motionID := range motionBlock.MotionIDS { - data := fetch.Object( - ctx, - fmt.Sprintf("motion/%d", motionID), - "title", - "number", - "agenda_item_id", - "recommendation_id", - "recommendation_extension", - "recommendation_extension_reference_ids", - "meeting_id", - ) - motion, err := motionFromMap(data) - if err != nil { - return nil, fmt.Errorf("get motion: %w", err) - } - - var recommendation *dbMotionState - var recommendationExtension *string - if motion.MotionWork.RecommendationID > 0 { - data := fetch.Object( - ctx, - fmt.Sprintf("motion_state/%d", motion.MotionWork.RecommendationID), - "recommendation_label", - "css_class", - "show_recommendation_extension_field", - ) - recommendation, err = motionStateFromMap(data) - if err != nil { - return nil, fmt.Errorf("get motion: %w", err) - } - if recommendation.MotionStateWork.ShowRecommendationExtensionField { - recommendationExtension = &motion.RecommendationExtension - } - for _, referenceObjectID := range motion.MotionWork.RecommendationExtensionReferenceIDS { - title, err := titlerMotion.GetTitleInformation(ctx, fetch, referenceObjectID, "", motion.MotionWork.MeetingID) - if err != nil { - return nil, fmt.Errorf("encoding GetTitleInformation data: %w", err) - } - referenced[referenceObjectID] = title - } - recommendation.MotionStateWork = nil // don't export - } - - var itemNumber string - if motion.MotionWork.AgendaItemID > 0 { - itemNumber = datastore.String(ctx, fetch.FetchIfExist, "agenda_item/%d/item_number", motion.MotionWork.AgendaItemID) - if err := fetch.Err(); err != nil { - return nil, err - } - } - - motions = append(motions, motionRepr{ - Title: motion.Title, - Number: motion.Number, - AgendaItemNumber: itemNumber, - Recommendation: recommendation, - RecommendationExtension: recommendationExtension, - }) - } - - out := struct { - Title string `json:"title"` - Motions []motionRepr `json:"motions"` - Referenced map[string]json.RawMessage `json:"referenced"` - }{ - motionBlock.Title, - motions, - referenced, - } - bs, err := json.Marshal(out) - if err != nil { - return nil, fmt.Errorf("encoding motion_block: %w", err) - } - return bs, nil - }) - - store.RegisterGetTitleInformationFunc("motion_block", func(ctx context.Context, fetch *datastore.Fetcher, fqid string, itemNumber string, meetingID int) (json.RawMessage, error) { - data := fetch.Object(ctx, fqid, "id", "title", "agenda_item_id") - motionBlock, err := motionBlockFromMap(data) - if err != nil { - return nil, fmt.Errorf("get motion block: %w", err) - } - - if itemNumber == "" && motionBlock.AgendaItemID > 0 { - itemNumber = datastore.String(ctx, fetch.FetchIfExist, "agenda_item/%d/item_number", motionBlock.AgendaItemID) - } - if err := fetch.Err(); err != nil { - return nil, err - } - - title := struct { - Collection string `json:"collection"` - ContentObjectID string `json:"content_object_id"` - Title string `json:"title"` - AgendaItemNumber string `json:"agenda_item_number"` - }{ - "motion_block", - fqid, - motionBlock.Title, - itemNumber, - } - - bs, err := json.Marshal(title) - if err != nil { - return nil, fmt.Errorf("encoding title: %w", err) - } - return bs, err - }) -} diff --git a/internal/projector/slide/motion_block_test.go b/internal/projector/slide/motion_block_test.go deleted file mode 100644 index e746f9a6..00000000 --- a/internal/projector/slide/motion_block_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package slide_test - -import ( - "context" - "testing" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/slide" - "github.com/OpenSlides/openslides-go/datastore/dskey" - "github.com/OpenSlides/openslides-go/datastore/dsmock" - "github.com/stretchr/testify/assert" -) - -func TestMotionBlock(t *testing.T) { - s := new(projector.SlideStore) - slide.MotionBlock(s) - slide.Motion(s) - - motionBlockSlide := s.GetSlider("motion_block") - assert.NotNilf(t, motionBlockSlide, "Slide with name `motion_block` not found.") - - data := dsmock.YAMLData(` - motion_block/1: - title: MotionBlock1 Title - motion_ids: [1,2] - motion: - 1: - title: Motion Title 1 - number: MNr 123 - recommendation_id: 1 - recommendation_extension: RecommendationExtension_motion1 - recommendation_extension_reference_ids: ["motion/3", "motion/4"] - meeting_id: 1 - agenda_item_id: 1 - 2: - title: Motion Title 2 - number: MNR 456 - meeting_id: 1 - 3: - title: RecommendationExtensionReferenceMotion3 title - number: RecommendationExtensionReferenceMotion3 number - meeting_id: 1 - agenda_item_id: 3 - 4: - title: RecommendationExtensionReferenceMotion4 title - number: RecommendationExtensionReferenceMotion4 number - meeting_id: 1 - agenda_item_id: 4 - motion_state/1: - recommendation_label: RecommendationLabel_state1 - css_class: Css-Class1 - show_recommendation_extension_field: true - agenda_item/1/item_number: ItemNr Motion1 - agenda_item/3/item_number: ItemNr Motion3 - agenda_item/4/item_number: ItemNr Motion4 - `) - - for _, tt := range []struct { - name string - data map[dskey.Key][]byte - expect string - }{ - { - "MotionBlock Standard", - data, - `{ - "title":"MotionBlock1 Title", - "motions":[ - { - "title": "Motion Title 1", - "number": "MNr 123", - "agenda_item_number": "ItemNr Motion1", - "recommendation": { - "recommendation_label": "RecommendationLabel_state1", - "css_class": "Css-Class1" - }, - "recommendation_extension": "RecommendationExtension_motion1" - }, - { - "title": "Motion Title 2", - "number": "MNR 456" - } - ], - "referenced": { - "motion/3": { - "agenda_item_number": "ItemNr Motion3", - "title": "RecommendationExtensionReferenceMotion3 title", - "number": "RecommendationExtensionReferenceMotion3 number", - "collection": "motion", - "content_object_id": "motion/3" - }, - "motion/4": { - "agenda_item_number": "ItemNr Motion4", - "title": "RecommendationExtensionReferenceMotion4 title", - "number": "RecommendationExtensionReferenceMotion4 number", - "collection": "motion", - "content_object_id": "motion/4" - } - } - } - `, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - ds := dsmock.Stub(tt.data) - fetch := datastore.NewFetcher(ds) - - p7on := &projector.Projection{ - ContentObjectID: "motion_block/1", - MeetingID: 1, - } - - bs, err := motionBlockSlide.Slide(ctx, fetch, p7on) - assert.NoError(t, err) - assert.JSONEq(t, tt.expect, string(bs)) - }) - } -} diff --git a/internal/projector/slide/motion_test.go b/internal/projector/slide/motion_test.go deleted file mode 100644 index 7f8150cf..00000000 --- a/internal/projector/slide/motion_test.go +++ /dev/null @@ -1,458 +0,0 @@ -package slide_test - -import ( - "context" - "testing" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/slide" - "github.com/OpenSlides/openslides-go/datastore/dskey" - "github.com/OpenSlides/openslides-go/datastore/dsmock" - "github.com/stretchr/testify/assert" -) - -func TestMotion(t *testing.T) { - s := new(projector.SlideStore) - slide.Motion(s) - - motionSlide := s.GetSlider("motion") - assert.NotNilf(t, motionSlide, "Slide with name `motion` not found.") - - data := dsmock.YAMLData(` - projection: - 1: - content_object_id: motion/1 - meeting_id: 1 - options: { - mode: final - } - meeting: - 1: - motions_enable_text_on_projector: false - motions_enable_reason_on_projector: false - motions_show_referring_motions: false - motions_enable_recommendation_on_projector: false - motions_recommendations_by: Meeting not used variant - motions_enable_sidebox_on_projector: true - motions_line_length: 85 - motions_preamble: The assembly may decide - motions_default_line_numbering: outside - motion: - 1: - title: Motion Title 1 - number: MNr1234 - text:
Motion1 Text HTML
- reason:Motion1 reason HTML
- modified_final_version:Motion1 modifiedFinalVersion HTML
- submitter_ids: [1,2,3] - amendment_paragraphs: {"1": "amendmentParagraph1", "2": "amendmentParagraph2"} - change_recommendation_ids: [1,2,3] - amendment_ids: [3,4,5,6] - referenced_in_motion_recommendation_extension_ids: [7,8] - recommendation_id: 4 - recommendation_extension: RecommendationExtension_motion1 - recommendation_extension_reference_ids: ["motion/9", "motion/10"] - meeting_id: 1 - agenda_item_id: 1 - start_line_number: 24 - 2: - title: Lead Motion Title - number: Lead Motion 111 - text:Lead Motion Text HTML
- agenda_item_id: 2 - start_line_number: 24 - 3: - title: Amendment3 title - number: Amendment3 123 - amendment_paragraphs: {"31": "amendmentParagraph31", "32": "amendmentParagraph32"} - change_recommendation_ids: [4, 5] - state_id: 1 - agenda_item_id: 3 - 4: - title: Amendment4 title - number: Amendment4 4123 - amendment_paragraphs: null - state_id: 2 - agenda_item_id: 4 - 5: - title: Amendment5 title - number: Amendment5 5123 - amendment_paragraphs: null - state_id: 3 - recommendation_id: 1 - agenda_item_id: 5 - 6: - title: Amendment6 title - number: Amendment6 6123 - amendment_paragraphs: null - state_id: 3 - recommendation_id: 2 - agenda_item_id: 6 - 7: - title: ReferencedInMotionRecommendationExtension7 title - number: RIMRE7 number - agenda_item_id: 7 - 8: - title: ReferencedInMotionRecommendationExtension8 title - number: RIMRE8 number - agenda_item_id: 8 - 9: - title: RecommendationExtensionReferenceMotion9 title - number: RecommendationExtensionReferenceMotion9 number - agenda_item_id: 9 - 10: - title: RecommendationExtensionReferenceMotion10 title - number: RecommendationExtensionReferenceMotion10 number - agenda_item_id: 10 - motion_state: - 1: - merge_amendment_into_final: do_merge - 2: - merge_amendment_into_final: do_not_merge - 3: - merge_amendment_into_final: undefined - 4: - recommendation_label: RecommendationLabel_state4 - show_recommendation_extension_field: true - - motion_submitter: - 1: - weight: 100 - meeting_user_id: 130 - motion_id: 1 - 2: - weight: 2 - meeting_user_id: 110 - motion_id: 1 - 3: - weight: 30 - meeting_user_id: 120 - motion_id: 1 - - meeting_user: - 130: - user_id: 13 - 110: - user_id: 11 - 120: - user_id: 12 - - user: - 11: - username: user11 - 12: - username: user12 - 13: - username: user13 - - motion_change_recommendation: - 1: - internal: false - rejected: true - type: replacement - other_description: Other Description1 - line_from: 1 - line_to: 3 - text:text1 HTML
- creation_time: 12345 - 2: - internal: true - 3: - internal: false - rejected: false - type: insertion - other_description: Other Description3 - line_from: 5 - line_to: 5 - text:text3 HTML
- creation_time: 32345 - 4: - internal: false - rejected: true - type: replacement - other_description: ChangeRecommendation4 for amendment3 - line_from: 4 - line_to: 5 - text:text4 HTML
- creation_time: 42345 - 5: - internal: true - agenda_item/1/item_number: ItemNr Motion1 - agenda_item/2/item_number: ItemNr Motion2 - agenda_item/3/item_number: ItemNr Motion3 - agenda_item/4/item_number: ItemNr Motion4 - agenda_item/5/item_number: ItemNr Motion5 - agenda_item/6/item_number: ItemNr Motion6 - agenda_item/7/item_number: ItemNr Motion7 - agenda_item/8/item_number: ItemNr Motion8 - agenda_item/9/item_number: ItemNr Motion9 - agenda_item/10/item_number: ItemNr Motion10 - `) - - for _, tt := range []struct { - name string - options []byte - data map[dskey.Key][]byte - expect string - }{ - { - "Motion only non conditional", - nil, - data, - `{ - "id":1, - "title":"Motion Title 1", - "number":"MNr1234", - "submitters":[ - "user11", - "user12", - "user13" - ], - "show_sidebox": true, - "line_length": 85, - "start_line_number": 24, - "line_numbering": "outside", - "change_recommendations":[ - { - "id": 1, - "rejected": true, - "type": "replacement", - "other_description": "Other Description1", - "line_from": 1, - "line_to": 3, - "text": "text1 HTML
", - "creation_time": 12345 - }, - { - "id": 3, - "rejected": false, - "type": "insertion", - "other_description": "Other Description3", - "line_from": 5, - "line_to": 5, - "text": "text3 HTML
", - "creation_time": 32345 - } - ], - "amendments":[ - { - "id": 3, - "title": "Amendment3 title", - "number": "Amendment3 123", - "amendment_paragraphs":{ - "31":"amendmentParagraph31", - "32":"amendmentParagraph32" - }, - "change_recommendations":[ - { - "id": 4, - "rejected": true, - "type": "replacement", - "other_description": "ChangeRecommendation4 for amendment3", - "line_from": 4, - "line_to": 5, - "text": "text4 HTML
", - "creation_time": 42345 - } - ], - "merge_amendment_into_final": "do_merge", - "merge_amendment_into_diff": "do_merge" - }, - { - "id": 4, - "title": "Amendment4 title", - "number": "Amendment4 4123", - "amendment_paragraphs": null, - "change_recommendations": null, - "merge_amendment_into_final": "undefined", - "merge_amendment_into_diff": "undefined" - }, - { - "id": 5, - "title": "Amendment5 title", - "number": "Amendment5 5123", - "amendment_paragraphs": null, - "change_recommendations": null, - "merge_amendment_into_final": "undefined", - "merge_amendment_into_diff": "do_merge" - }, - { - "id": 6, - "title": "Amendment6 title", - "number": "Amendment6 6123", - "amendment_paragraphs": null, - "change_recommendations": null, - "merge_amendment_into_final": "undefined", - "merge_amendment_into_diff": "undefined" - } - ] - } `, - }, - { - "motion including conditional fields", - []byte(`{"mode":"final"}`), - changeData(data, map[dskey.Key][]byte{ - dskey.MustKey("meeting/1/motions_enable_text_on_projector"): []byte(`true`), - dskey.MustKey("meeting/1/motions_enable_reason_on_projector"): []byte(`true`), - dskey.MustKey("meeting/1/motions_show_referring_motions"): []byte(`true`), - dskey.MustKey("meeting/1/motions_enable_recommendation_on_projector"): []byte(`true`), - dskey.MustKey("motion/1/lead_motion_id"): []byte(`2`), - }), - `{ - "id":1, - "title":"Motion Title 1", - "number":"MNr1234", - "submitters":[ - "user11", - "user12", - "user13" - ], - "show_sidebox": true, - "line_length": 85, - "start_line_number": 24, - "preamble": "The assembly may decide", - "line_numbering": "outside", - "amendment_paragraphs":{ - "1":"amendmentParagraph1", - "2":"amendmentParagraph2" - }, - "change_recommendations":[ - { - "id": 1, - "rejected": true, - "type": "replacement", - "other_description": "Other Description1", - "line_from": 1, - "line_to": 3, - "text": "text1 HTML
", - "creation_time": 12345 - }, - { - "id": 3, - "rejected": false, - "type": "insertion", - "other_description": "Other Description3", - "line_from": 5, - "line_to": 5, - "text": "text3 HTML
", - "creation_time": 32345 - } - ], - "amendments":[ - { - "id": 3, - "title": "Amendment3 title", - "number": "Amendment3 123", - "amendment_paragraphs":{ - "31":"amendmentParagraph31", - "32":"amendmentParagraph32" - }, - "change_recommendations":[ - { - "id": 4, - "rejected": true, - "type": "replacement", - "other_description": "ChangeRecommendation4 for amendment3", - "line_from": 4, - "line_to": 5, - "text": "text4 HTML
", - "creation_time": 42345 - } - ], - "merge_amendment_into_final": "do_merge", - "merge_amendment_into_diff": "do_merge" - }, - { - "id": 4, - "title": "Amendment4 title", - "number": "Amendment4 4123", - "amendment_paragraphs": null, - "change_recommendations": null, - "merge_amendment_into_final": "undefined", - "merge_amendment_into_diff": "undefined" - }, - { - "id": 5, - "title": "Amendment5 title", - "number": "Amendment5 5123", - "amendment_paragraphs": null, - "change_recommendations": null, - "merge_amendment_into_final": "undefined", - "merge_amendment_into_diff": "do_merge" - }, - { - "id": 6, - "title": "Amendment6 title", - "number": "Amendment6 6123", - "amendment_paragraphs": null, - "change_recommendations": null, - "merge_amendment_into_final": "undefined", - "merge_amendment_into_diff": "undefined" - } - ], - "text":"Motion1 Text HTML
", - "reason": "Motion1 reason HTML
", - "modified_final_version":"Motion1 modifiedFinalVersion HTML
", - "lead_motion":{ - "title":"Lead Motion Title", - "number":"Lead Motion 111", - "text":"Lead Motion Text HTML
", - "start_line_number": 24 - }, - "recommendation_referencing_motions":[ - { - "agenda_item_number":"ItemNr Motion7", - "collection":"motion", - "content_object_id":"motion/7", - "title": "ReferencedInMotionRecommendationExtension7 title", - "number": "RIMRE7 number" - }, - { - "agenda_item_number":"ItemNr Motion8", - "collection":"motion", - "content_object_id":"motion/8", - "title": "ReferencedInMotionRecommendationExtension8 title", - "number": "RIMRE8 number" - } - ], - "recommendation_label":"RecommendationLabel_state4", - "recommendation_extension":"RecommendationExtension_motion1", - "recommendation_referenced_motions":{ - "motion/9":{ - "agenda_item_number":"ItemNr Motion9", - "collection":"motion", - "content_object_id":"motion/9", - "title": "RecommendationExtensionReferenceMotion9 title", - "number": "RecommendationExtensionReferenceMotion9 number" - }, - "motion/10":{ - "agenda_item_number":"ItemNr Motion10", - "collection":"motion", - "content_object_id":"motion/10", - "title": "RecommendationExtensionReferenceMotion10 title", - "number": "RecommendationExtensionReferenceMotion10 number" - } - }, - "recommender": "Meeting not used variant" - } - `, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - ds := dsmock.Stub(tt.data) - fetch := datastore.NewFetcher(ds) - - p7on := &projector.Projection{ - ContentObjectID: "motion/1", - MeetingID: 1, - Options: tt.options, - } - - bs, err := motionSlide.Slide(ctx, fetch, p7on) - assert.NoError(t, err) - assert.JSONEq(t, tt.expect, string(bs)) - }) - } -} diff --git a/internal/projector/slide/poll.go b/internal/projector/slide/poll.go deleted file mode 100644 index 098c1194..00000000 --- a/internal/projector/slide/poll.go +++ /dev/null @@ -1,542 +0,0 @@ -package slide - -import ( - "context" - "encoding/json" - "fmt" - "slices" - "sort" - "strconv" - "strings" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" -) - -type pollVoteRepr struct { - UserID *int `json:"user_id"` - User *pollUserRepr `json:"user"` - Value string `json:"value"` -} - -func pollVoteFromMap(in map[string]json.RawMessage) (*pollVoteRepr, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding vote data: %w", err) - } - - var vr pollVoteRepr - if err := json.Unmarshal(bs, &vr); err != nil { - return nil, fmt.Errorf("decoding vote data: %w", err) - } - - return &vr, nil -} - -type pollUserRepr struct { - ID int `json:"id"` - Title *string `json:"title,omitempty"` - FirstName *string `json:"first_name,omitempty"` - LastName *string `json:"last_name,omitempty"` -} - -func pollUserFromMap(in map[string]json.RawMessage) (*pollUserRepr, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding user data: %w", err) - } - - var ur pollUserRepr - if err := json.Unmarshal(bs, &ur); err != nil { - return nil, fmt.Errorf("decoding user data: %w", err) - } - - return &ur, nil -} - -type optionRepr struct { - Text string `json:"text,omitempty"` - ContentObjectID string `json:"content_object_id,omitempty"` - ContentObject json.RawMessage `json:"content_object,omitempty"` - Yes *string `json:"yes,omitempty"` // Python-DecimalField - No *string `json:"no,omitempty"` // Python-DecimalField - Abstain *string `json:"abstain,omitempty"` // Python-DecimalField - Votes []*pollVoteRepr `json:"votes,omitempty"` - id *int - weight *int -} - -func optionFromMap(in map[string]json.RawMessage) (*optionRepr, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding option data: %w", err) - } - - var or optionRepr - if err := json.Unmarshal(bs, &or); err != nil { - return nil, fmt.Errorf("decoding option data: %w", err) - } - if err := json.Unmarshal(in["weight"], &or.weight); err != nil { - return nil, fmt.Errorf("decoding option weight: %w", err) - } - if err := json.Unmarshal(in["id"], &or.id); err != nil { - return nil, fmt.Errorf("decoding option id: %w", err) - } - - if in["text"] != nil { - if err := json.Unmarshal(in["text"], &or.Text); err != nil { - return nil, fmt.Errorf("decoding option text: %w", err) - } - } - return &or, nil -} - -type optionGlobRepr struct { - Yes string `json:"yes"` // Python-DecimalField - No string `json:"no"` // Python-DecimalField - Abstain string `json:"abstain"` // Python-DecimalField -} - -func optionGlobFromMap(in map[string]json.RawMessage) (*optionGlobRepr, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding option global data: %w", err) - } - - var og optionGlobRepr - if err := json.Unmarshal(bs, &og); err != nil { - return nil, fmt.Errorf("decoding option glob data: %w", err) - } - return &og, nil -} - -// Contains fields to be read, but never exported -type dbPollWork struct { - OptionIDS []int `json:"option_ids"` - MeetingID int `json:"meeting_id"` - GlobalOptionID int `json:"global_option_id"` - LiveVotes *json.RawMessage `json:"live_votes"` -} - -type dbPoll struct { - ID int `json:"id"` - ContentObjectID string `json:"content_object_id"` - TitleInformation json.RawMessage `json:"title_information"` - Title string `json:"title"` - Description string `json:"description"` - Type string `json:"type"` - State string `json:"state"` - GlobalYes bool `json:"global_yes"` - GlobalNo bool `json:"global_no"` - GlobalAbstain bool `json:"global_abstain"` - Options []*optionRepr `json:"options"` - EntitledUsers *json.RawMessage `json:"entitled_users,omitempty"` - EntitledUsersAtStop *json.RawMessage `json:"entitled_users_at_stop,omitempty"` - EntitledStructureLevels map[int]string `json:"entitled_structure_levels,omitempty"` - LiveVotingEnabled bool `json:"live_voting_enabled"` - IsPseudoanonymized *bool `json:"is_pseudoanonymized,omitempty"` - Pollmethod *string `json:"pollmethod,omitempty"` - OnehundredPercentBase *string `json:"onehundred_percent_base,omitempty"` - Votesvalid *string `json:"votesvalid,omitempty"` // Python-DecimalField - Votesinvalid *string `json:"votesinvalid,omitempty"` // Python-DecimalField - Votescast *string `json:"votescast,omitempty"` // Python-DecimalField - GlobalOption *optionGlobRepr `json:"global_option,omitempty"` - PollWork *dbPollWork `json:"-"` -} - -func pollFromMap(in map[string]json.RawMessage, state string) (*dbPoll, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding poll data: %w", err) - } - - var po dbPoll - var poWork dbPollWork - po.PollWork = &poWork - if err := json.Unmarshal(bs, &po); err != nil { - return nil, fmt.Errorf("decoding poll data: %w", err) - } - if err := json.Unmarshal(bs, &poWork); err != nil { - return nil, fmt.Errorf("decoding poll work data: %w", err) - } - return &po, nil -} - -func pollSlideDataFunction(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection, store *projector.SlideStore) (*dbPoll, error) { - fetchFields := []string{ - "id", - "content_object_id", - "title", - "description", - "type", - "live_voting_enabled", - "state", - "global_yes", - "global_no", - "global_abstain", - "option_ids", - "meeting_id", - } - state := datastore.String(ctx, fetch.FetchIfExist, "%s/%s", p7on.ContentObjectID, "state") - if state == "published" { - fetchFields = append(fetchFields, []string{ - "entitled_users_at_stop", - "is_pseudoanonymized", - "pollmethod", - "onehundred_percent_base", - "votesvalid", - "votesinvalid", - "votescast", - "global_option_id", - }...) - } else if datastore.Bool(ctx, fetch.FetchIfExist, "%s/%s", p7on.ContentObjectID, "live_voting_enabled") { - fetchFields = append(fetchFields, []string{ - "live_votes", - "is_pseudoanonymized", - "pollmethod", - "onehundred_percent_base", - }...) - } - - data := fetch.Object(ctx, p7on.ContentObjectID, fetchFields...) - - poll, err := pollFromMap(data, state) - if err != nil { - return nil, fmt.Errorf("get poll: %w", err) - } - - poll.TitleInformation, err = getTitleInfoFromContentObject(ctx, fetch, store, poll.ContentObjectID, "", p7on.MeetingID) - if err != nil { - return nil, fmt.Errorf("getTitleInfoFromContentObject: %w", err) - } - - poll.Options, err = getOptions(ctx, fetch, store, poll.PollWork.OptionIDS, state, p7on.MeetingID) - if err != nil { - return nil, fmt.Errorf("get Options func: %w", err) - } - if state == "published" { - poll.GlobalOption, err = getGlobalOption(ctx, fetch, store, poll.PollWork.GlobalOptionID) - if err != nil { - return nil, fmt.Errorf("get GlobalOption func: %w", err) - } - } - if err := fetch.Err(); err != nil { - return nil, err - } - - return poll, err -} - -func getOptions(ctx context.Context, fetch *datastore.Fetcher, store *projector.SlideStore, optionIDS []int, state string, meetingID int) (options []*optionRepr, err error) { - fetchFields := []string{ - "text", - "content_object_id", - "weight", - "id", - } - if state == "published" { - fetchFields = append(fetchFields, []string{ - "yes", - "no", - "abstain", - }...) - } - - for _, optionID := range optionIDS { - data := fetch.Object(ctx, fmt.Sprintf("option/%d", optionID), fetchFields...) - option, err := optionFromMap(data) - if err != nil { - return nil, fmt.Errorf("get option data: %w", err) - } - - if option.ContentObjectID != "" { - option.ContentObject, err = getTitleInfoFromContentObject(ctx, fetch, store, option.ContentObjectID, "", meetingID) - if err != nil { - return nil, fmt.Errorf("getTitleInfoFromContentObject: %w", err) - } - } - - options = append(options, option) - } - if err := fetch.Err(); err != nil { - return nil, err - } - - sort.Slice(options, func(i, j int) bool { - if *options[i].weight == *options[j].weight { - return *options[i].id < *options[j].id - } - return *options[i].weight < *options[j].weight - }) - - return options, nil -} - -func getPollUser(ctx context.Context, fetch *datastore.Fetcher, userID int) (user *pollUserRepr, err error) { - data := fetch.Object(ctx, fmt.Sprintf("user/%d", userID), "id", "title", "first_name", "last_name") - user, err = pollUserFromMap(data) - if err != nil { - return nil, fmt.Errorf("get user data: %w", err) - } - - return user, nil -} - -func getVotes(ctx context.Context, fetch *datastore.Fetcher, optionID int) (votes []*pollVoteRepr, err error) { - voteIDs := datastore.Ints(ctx, fetch.FetchIfExist, "option/%d/vote_ids", optionID) - - fetchFields := []string{ - "id", - "user_id", - "value", - } - - for _, voteID := range voteIDs { - data := fetch.Object(ctx, fmt.Sprintf("vote/%d", voteID), fetchFields...) - vote, err := pollVoteFromMap(data) - if err != nil { - return nil, fmt.Errorf("get option data: %w", err) - } - - if vote.UserID != nil { - vote.User, err = getPollUser(ctx, fetch, *vote.UserID) - if err != nil { - return nil, fmt.Errorf("get user data: %w", err) - } - } - - votes = append(votes, vote) - } - if err := fetch.Err(); err != nil { - return nil, err - } - - return votes, nil -} - -func getGlobalOption(ctx context.Context, fetch *datastore.Fetcher, store *projector.SlideStore, globalOptionID int) (*optionGlobRepr, error) { - data := fetch.Object(ctx, fmt.Sprintf("option/%d", globalOptionID), "yes", "no", "abstain") - globalOption, err := optionGlobFromMap(data) - if err != nil { - return nil, fmt.Errorf("get option data: %w", err) - } - return globalOption, nil -} - -// getTitleInfoFromContentObject gets GetTitleInformation from ContentObject -func getTitleInfoFromContentObject(ctx context.Context, fetch *datastore.Fetcher, store *projector.SlideStore, contentObjectID string, itemNumber string, meetingID int) (json.RawMessage, error) { - collection := strings.Split(contentObjectID, "/")[0] - titler := store.GetTitleInformationFunc(collection) - if titler == nil { - return nil, fmt.Errorf("no titler function registered for %s", collection) - } - titleInfo, err := titler.GetTitleInformation(ctx, fetch, contentObjectID, "", meetingID) - if err != nil { - return nil, fmt.Errorf("get title func: %w", err) - } - return titleInfo, nil -} - -// Poll renders the poll slide. -func Poll(store *projector.SlideStore) { - store.RegisterSliderFunc("poll", func(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection) (encoded []byte, err error) { - poll, err := pollSlideDataFunction(ctx, fetch, p7on, store) - - var options struct { - SingleVotes bool `json:"single_votes"` - } - if p7on.Options != nil { - if err := json.Unmarshal(p7on.Options, &options); err != nil { - return nil, fmt.Errorf("decoding projection options: %w", err) - } - - if options.SingleVotes { - if err := PollSingleVotes(ctx, store, fetch, p7on, poll); err != nil { - return nil, fmt.Errorf("adding single votes additional info : %w", err) - } - } - } - - responseValue, err := json.Marshal(poll) - if err != nil { - return nil, fmt.Errorf("encoding response slide poll: %w", err) - } - return responseValue, err - }) -} - -// PollSingleVotes renders the poll_single_votes slide. -func PollSingleVotes(ctx context.Context, store *projector.SlideStore, fetch *datastore.Fetcher, p7on *projector.Projection, poll *dbPoll) error { - for i, option := range poll.Options { - votes, err := getVotes(ctx, fetch, *option.id) - if err != nil { - return fmt.Errorf("reading option votes: %w", err) - } - - poll.Options[i].Votes = votes - } - - if poll.EntitledUsersAtStop != nil { - meetingUserIDs := map[int]int{} - entitledGroupIDs := datastore.Ints(ctx, fetch.FetchIfExist, "poll/%d/entitled_group_ids", poll.ID) - for _, groupID := range entitledGroupIDs { - gMeetingUserIDs := datastore.Ints(ctx, fetch.FetchIfExist, "group/%d/meeting_user_ids", groupID) - for _, meetingUserID := range gMeetingUserIDs { - userID := datastore.Int(ctx, fetch.FetchIfExist, "meeting_user/%d/user_id", meetingUserID) - meetingUserIDs[userID] = meetingUserID - } - } - - var pollUserData []map[string]json.RawMessage - if err := json.Unmarshal(*poll.EntitledUsersAtStop, &pollUserData); err != nil { - return fmt.Errorf("reading entitled users: %w", err) - } - - structureLevels := map[int]string{} - var newUserData []map[string]interface{} - for _, userDate := range pollUserData { - entry := make(map[string]interface{}, len(userDate)) - for key, val := range userDate { - if i, err := strconv.ParseInt(string(val), 10, 64); err == nil { - entry[key] = int(i) - } else { - entry[key] = val - } - } - - var userID int - if _, ok := entry["user_merged_into_id"]; ok { - userID = entry["user_merged_into_id"].(int) - } else if _, ok := entry["user_id"]; ok { - userID = entry["user_id"].(int) - } else { - continue - } - - muID := meetingUserIDs[userID] - if muID != 0 { - structureLevelIDs := datastore.Ints(ctx, fetch.FetchIfExist, "meeting_user/%d/structure_level_ids", muID) - if len(structureLevelIDs) > 0 { - entry["structure_level_id"] = &structureLevelIDs[0] - if _, ok := structureLevels[structureLevelIDs[0]]; !ok { - structureLevels[structureLevelIDs[0]] = datastore.String(ctx, fetch.FetchIfExist, "structure_level/%d/name", structureLevelIDs[0]) - } - } - entry["meeting_user_id"] = muID - } - - user, err := getPollUser(ctx, fetch, userID) - if err != nil { - return fmt.Errorf("encoding entitled users interpretation: %w", err) - } - - entry["user"] = user - newUserData = append(newUserData, entry) - } - - var pollUserDataJSON, err = json.Marshal(newUserData) - if err != nil { - return fmt.Errorf("encoding entitled users interpretation: %w", err) - } - - var pollUserDataJSONRaw = json.RawMessage(pollUserDataJSON) - poll.EntitledUsersAtStop = &pollUserDataJSONRaw - poll.EntitledStructureLevels = structureLevels - } else if poll.LiveVotingEnabled { - err := PollNominalLiveVoting(ctx, store, fetch, p7on, poll) - if err != nil { - return fmt.Errorf("encoding live vote data: %w", err) - } - } - - return nil -} - -// PollNominalLiveVoting renders the poll_single_votes slide for live voting. -func PollNominalLiveVoting(ctx context.Context, store *projector.SlideStore, fetch *datastore.Fetcher, p7on *projector.Projection, poll *dbPoll) error { - meetingUserIDs := map[int]struct{}{} - entitledGroupIDs := datastore.Ints(ctx, fetch.FetchIfExist, "poll/%d/entitled_group_ids", poll.ID) - for _, groupID := range entitledGroupIDs { - gMeetingUserIDs := datastore.Ints(ctx, fetch.FetchIfExist, "group/%d/meeting_user_ids", groupID) - for _, meetingUserID := range gMeetingUserIDs { - meetingUserIDs[meetingUserID] = struct{}{} - } - } - - type liveVoteEntitledUser struct { - User *pollUserRepr `json:"user_data"` - Vote *json.RawMessage `json:"votes,omitempty"` - StructureLevel *int `json:"structure_level_id,omitempty"` - Present bool `json:"present"` - Weight *string `json:"weight,omitempty"` - } - - liveVotingEntitledUsers := map[int]*liveVoteEntitledUser{} - structureLevels := map[int]string{} - for muID := range meetingUserIDs { - meetingID := datastore.Int(ctx, fetch.FetchIfExist, "meeting_user/%d/meeting_id", muID) - userID := datastore.Int(ctx, fetch.FetchIfExist, "meeting_user/%d/user_id", muID) - liveVotingEntitledUsers[userID] = &liveVoteEntitledUser{ - Present: false, - } - - presentMeetingIDs := datastore.Ints(ctx, fetch.FetchIfExist, "user/%d/is_present_in_meeting_ids", userID) - if slices.Contains(presentMeetingIDs, meetingID) { - liveVotingEntitledUsers[userID].Present = true - } - - structureLevelIDs := datastore.Ints(ctx, fetch.FetchIfExist, "meeting_user/%d/structure_level_ids", muID) - if len(structureLevelIDs) > 0 { - liveVotingEntitledUsers[userID].StructureLevel = &structureLevelIDs[0] - if _, ok := structureLevels[structureLevelIDs[0]]; !ok { - structureLevels[structureLevelIDs[0]] = datastore.String(ctx, fetch.FetchIfExist, "structure_level/%d/name", structureLevelIDs[0]) - } - } - - user, err := getPollUser(ctx, fetch, userID) - if err != nil { - return fmt.Errorf("encoding live votes interpretation: %w", err) - } - - liveVotingEntitledUsers[userID].User = user - } - - if poll.PollWork != nil && poll.PollWork.LiveVotes != nil { - var pollLiveVoteData map[int]string - if err := json.Unmarshal(*poll.PollWork.LiveVotes, &pollLiveVoteData); err != nil { - return fmt.Errorf("reading live vote data: %w", err) - } - - for userID, data := range pollLiveVoteData { - if len(data) == 0 { - continue - } - - var vote struct { - Value json.RawMessage `json:"value"` - Weight string `json:"weight"` - } - - if err := json.Unmarshal([]byte(data), &vote); err != nil { - return fmt.Errorf("parsing vote data: %w", err) - } - - if entry, ok := liveVotingEntitledUsers[userID]; ok { - entry.Weight = &vote.Weight - entry.Vote = &vote.Value - } - } - } - - poll.EntitledStructureLevels = structureLevels - - var liveVotesDataJSON, err = json.Marshal(liveVotingEntitledUsers) - if err != nil { - return fmt.Errorf("encoding entitled users interpretation") - } - - var liveVotesDataJSONRaw = json.RawMessage(liveVotesDataJSON) - poll.EntitledUsers = &liveVotesDataJSONRaw - - return nil -} diff --git a/internal/projector/slide/poll_candidate_list.go b/internal/projector/slide/poll_candidate_list.go deleted file mode 100644 index aa4cdcb8..00000000 --- a/internal/projector/slide/poll_candidate_list.go +++ /dev/null @@ -1,74 +0,0 @@ -package slide - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - "strings" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" -) - -// DbPollCandidateList is the class with methods to get needed PollCandidateList Informations -type DbPollCandidateList struct { - PollCandidateIDs []int `json:"poll_candidate_ids"` -} - -// NewPollCandidateList gets the poll_candidate_list from datastore and return it as DbPollCandidateList struct -// together with keys and error. -func NewPollCandidateList(ctx context.Context, fetch *datastore.Fetcher, id int) (*DbPollCandidateList, error) { - fields := []string{ - "poll_candidate_ids", - } - - data := fetch.Object(ctx, fmt.Sprintf("poll_candidate_list/%d", id), fields...) - if err := fetch.Err(); err != nil { - return nil, fmt.Errorf("getting poll_candidate_list object: %w", err) - } - - bs, err := json.Marshal(data) - if err != nil { - return nil, fmt.Errorf("encoding poll_candidate_list data: %w", err) - } - - var u DbPollCandidateList - if err := json.Unmarshal(bs, &u); err != nil { - return nil, fmt.Errorf("decoding poll_candidate_list data: %w", err) - } - return &u, nil -} - -// PollCandidateList renders the poll_candidate_list slide. -func PollCandidateList(store *projector.SlideStore) { - store.RegisterGetTitleInformationFunc("poll_candidate_list", func(ctx context.Context, fetch *datastore.Fetcher, fqid string, itemNumber string, meetingID int) (json.RawMessage, error) { - id, err := strconv.Atoi(strings.Split(fqid, "/")[1]) - if err != nil { - return nil, fmt.Errorf("getting poll_candidate_list id: %w", err) - } - - pollCandidateList, err := NewPollCandidateList(ctx, fetch, id) - if err != nil { - return nil, fmt.Errorf("loading poll_candidate_list: %w", err) - } - if err := fetch.Err(); err != nil { - return nil, err - } - - out := struct { - Collection string `json:"collection"` - ContentObjectID string `json:"content_object_id"` - EntriesAmount int `json:"entries_amount"` - }{ - "poll_candidate_list", - fqid, - len(pollCandidateList.PollCandidateIDs), - } - responseValue, err := json.Marshal(out) - if err != nil { - return nil, fmt.Errorf("encoding title: %w", err) - } - return responseValue, err - }) -} diff --git a/internal/projector/slide/poll_candidate_list_test.go b/internal/projector/slide/poll_candidate_list_test.go deleted file mode 100644 index 160fe3b0..00000000 --- a/internal/projector/slide/poll_candidate_list_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package slide_test - -import ( - "context" - "testing" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/slide" - "github.com/OpenSlides/openslides-go/datastore/dsmock" - "github.com/stretchr/testify/assert" -) - -func TestPollCandidateList(t *testing.T) { - s := new(projector.SlideStore) - slide.PollCandidateList(s) - - pollCandidateListTitler := s.GetTitleInformationFunc("poll_candidate_list") - - for _, tt := range []struct { - name string - data map[string]string - expect string - }{ - { - "No Candidates", - map[string]string{ - "poll_candidate_list/1/id": "1", - "poll_candidate_list/1/poll_candidate_ids": `[]`, - }, - `{"collection":"poll_candidate_list","entries_amount":0,"content_object_id":"poll_candidate_list/1"}`, - }, - { - "One Candidate", - map[string]string{ - "poll_candidate_list/1/id": "1", - "poll_candidate_list/1/poll_candidate_ids": `[1]`, - }, - `{"collection":"poll_candidate_list","entries_amount":1,"content_object_id":"poll_candidate_list/1"}`, - }, - { - "Two Candidates", - map[string]string{ - "poll_candidate_list/1/id": "1", - "poll_candidate_list/1/poll_candidate_ids": `[1, 3]`, - }, - `{"collection":"poll_candidate_list","entries_amount":2,"content_object_id":"poll_candidate_list/1"}`, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ds := dsmock.Stub(convertData(tt.data)) - fetch := datastore.NewFetcher(ds) - - bs, err := pollCandidateListTitler.GetTitleInformation(context.Background(), fetch, "poll_candidate_list/1", "1", 222) - assert.NoError(t, err) - assert.JSONEq(t, tt.expect, string(bs)) - }) - } -} diff --git a/internal/projector/slide/poll_test.go b/internal/projector/slide/poll_test.go deleted file mode 100644 index 5f6f456f..00000000 --- a/internal/projector/slide/poll_test.go +++ /dev/null @@ -1,701 +0,0 @@ -package slide_test - -import ( - "context" - "testing" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/slide" - "github.com/OpenSlides/openslides-go/datastore/dskey" - "github.com/OpenSlides/openslides-go/datastore/dsmock" - "github.com/stretchr/testify/assert" -) - -func TestPoll(t *testing.T) { - s := new(projector.SlideStore) - slide.Poll(s) - slide.Motion(s) - slide.Topic(s) - - pollSlide := s.GetSlider("poll") - assert.NotNilf(t, pollSlide, "Slide with name `poll` not found.") - - data := dsmock.YAMLData(` - poll: - 1: - content_object_id: motion/1 - title: Poll Title 1 - description: Poll description 1 - type: analog - state: published - global_yes: false - global_no: true - global_abstain: false - option_ids: [1, 2] - is_pseudoanonymized: false - pollmethod: YNA - onehundred_percent_base: YNA - votesvalid: "2.000000" - votesinvalid: "9.000000" - votescast: "2.000000" - global_option_id: 3 - meeting_id: 111 - entitled_users_at_stop: {"A": "bcd", "B":"def"} - motion: - 1: - title: Motion title 1 - number: motion number 1234 - agenda_item_id: 1 - option: - 1: - text: Option text - content_object_id: topic/1 - yes: "4.000000" - no: "5.000000" - abstain: "6.000000" - weight: 10 - 2: - text: Option text - content_object_id: topic/2 - yes: "5.000000" - no: "4.000000" - abstain: "3.000000" - weight: 3 - 3: - yes: "14.000000" - no: "15.000000" - abstain: "16.000000" - topic: - 1: - title: Topic title 1 - text: Topic text 1 - agenda_item_id: 2 - 2: - title: Topic title 2 - text: Topic text 2 - agenda_item_id: 3 - agenda_item/1/item_number: itemNr. Motion1 - agenda_item/2/item_number: itemNr. Topic1 - agenda_item/3/item_number: itemNr. Topic2 - `) - - for _, tt := range []struct { - name string - data map[dskey.Key][]byte - expect string - }{ - { - "Poll state published", - data, - `{ - "id":1, - "content_object_id":"motion/1", - "title_information": { - "content_object_id":"motion/1", - "collection":"motion", - "title":"Motion title 1", - "number":"motion number 1234", - "agenda_item_number":"itemNr. Motion1" - }, - "title":"Poll Title 1", - "description":"Poll description 1", - "type":"analog", - "state":"published", - "global_yes":false, - "global_no":true, - "global_abstain":false, - "global_abstain":false, - "live_voting_enabled":false, - "options": [ - { - "content_object_id":"topic/2", - "text":"Option text", - "content_object":{ - "content_object_id":"topic/2", - "collection":"topic", - "title":"Topic title 2", - "agenda_item_number":"itemNr. Topic2" - }, - "yes":"5.000000", - "no":"4.000000", - "abstain":"3.000000" - }, - { - "content_object_id":"topic/1", - "text":"Option text", - "content_object":{ - "content_object_id":"topic/1", - "collection":"topic", - "title":"Topic title 1", - "agenda_item_number":"itemNr. Topic1" - }, - "yes":"4.000000", - "no":"5.000000", - "abstain":"6.000000" - } - ], - "entitled_users_at_stop": { - "A":"bcd", - "B":"def" - }, - "is_pseudoanonymized":false, - "pollmethod":"YNA", - "onehundred_percent_base":"YNA", - "votesvalid": "2.000000", - "votesinvalid": "9.000000", - "votescast": "2.000000", - "global_option":{ - "yes":"14.000000", - "no":"15.000000", - "abstain":"16.000000" - } - } - `, - }, - { - "Poll state finished", - changeData(data, map[dskey.Key][]byte{ - dskey.MustKey("poll/1/state"): []byte(`"finished"`), - }), - `{ - "id":1, - "content_object_id":"motion/1", - "title_information": { - "content_object_id":"motion/1", - "collection":"motion", - "title":"Motion title 1", - "number":"motion number 1234", - "agenda_item_number":"itemNr. Motion1" - }, - "title":"Poll Title 1", - "description":"Poll description 1", - "type":"analog", - "state":"finished", - "global_yes":false, - "global_no":true, - "global_abstain":false, - "live_voting_enabled":false, - "options": [ - { - "content_object_id":"topic/2", - "text":"Option text", - "content_object":{ - "content_object_id":"topic/2", - "collection":"topic", - "title":"Topic title 2", - "agenda_item_number":"itemNr. Topic2" - } - }, - { - "content_object_id":"topic/1", - "text":"Option text", - "content_object":{ - "content_object_id":"topic/1", - "collection":"topic", - "title":"Topic title 1", - "agenda_item_number":"itemNr. Topic1" - } - } - ] - } - `, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ds := dsmock.Stub(tt.data) - fetch := datastore.NewFetcher(ds) - - p7on := &projector.Projection{ - ContentObjectID: "poll/1", - } - - bs, err := pollSlide.Slide(context.Background(), fetch, p7on) - assert.NoError(t, err) - assert.JSONEq(t, tt.expect, string(bs)) - }) - } -} - -func TestPollSingleVotes(t *testing.T) { - s := new(projector.SlideStore) - slide.Poll(s) - slide.Motion(s) - slide.Topic(s) - - pollSlide := s.GetSlider("poll") - assert.NotNilf(t, pollSlide, "Slide with name `poll` not found.") - - data := dsmock.YAMLData(` - user: - 1: - title: Billy - first_name: Bob - last_name: Buckingham - username: BillyBobBuckingham - member_number: 123456789abcdef - meeting_user_ids: [1111] - meeting_ids: [1] - 2: - first_name: Johnny - last_name: Johnson - username: JohnnySonOfJohn - meeting_user_ids: [1112] - meeting_ids: [1] - 6: - title: Sir - first_name: Shawn Stanley Sheldon - last_name: Sinclair - username: SirSinclair - meeting_user_ids: [1111] - meeting_ids: [1] - structure_level: - 1: - name: Birmingham - meeting_id: 111 - meeting: - 111: - poll_ids: [1] - meeting_user_ids: [1111, 1112, 1116] - user_ids: [1,2,6] - structure_level_ids: [1] - poll: - 1: - content_object_id: motion/1 - title: Poll Title 1 - description: Poll description 1 - type: analog - state: published - global_yes: false - global_no: true - global_abstain: false - option_ids: [1, 2] - is_pseudoanonymized: false - pollmethod: YNA - onehundred_percent_base: YNA - votesvalid: "2.000000" - votesinvalid: "9.000000" - votescast: "2.000000" - global_option_id: 3 - meeting_id: 111 - entitled_users_at_stop: [{"voted": false, "present": true, "user_id": 1, "vote_delegated_to_user_id": 2}, {"voted": false, "present": true, "user_id": 4, "user_merged_into_id": 6}] - motion: - 1: - title: Motion title 1 - number: motion number 1234 - agenda_item_id: 1 - option: - 1: - text: Option text - content_object_id: topic/1 - yes: "4.000000" - no: "5.000000" - abstain: "6.000000" - weight: 10 - 2: - text: Option text - content_object_id: topic/2 - yes: "5.000000" - no: "4.000000" - abstain: "3.000000" - weight: 3 - 3: - yes: "14.000000" - no: "15.000000" - abstain: "16.000000" - topic: - 1: - title: Topic title 1 - text: Topic text 1 - agenda_item_id: 2 - 2: - title: Topic title 2 - text: Topic text 2 - agenda_item_id: 3 - agenda_item/1/item_number: itemNr. Motion1 - agenda_item/2/item_number: itemNr. Topic1 - agenda_item/3/item_number: itemNr. Topic2 - `) - - for _, tt := range []struct { - name string - data map[dskey.Key][]byte - expect string - }{ - { - "Poll state published", - data, - `{ - "id":1, - "content_object_id":"motion/1", - "title_information": { - "content_object_id":"motion/1", - "collection":"motion", - "title":"Motion title 1", - "number":"motion number 1234", - "agenda_item_number":"itemNr. Motion1" - }, - "title":"Poll Title 1", - "description":"Poll description 1", - "type":"analog", - "state":"published", - "global_yes":false, - "global_no":true, - "global_abstain":false, - "live_voting_enabled":false, - "options": [ - { - "content_object_id":"topic/2", - "text":"Option text", - "content_object":{ - "content_object_id":"topic/2", - "collection":"topic", - "title":"Topic title 2", - "agenda_item_number":"itemNr. Topic2" - }, - "yes":"5.000000", - "no":"4.000000", - "abstain":"3.000000" - }, - { - "content_object_id":"topic/1", - "text":"Option text", - "content_object":{ - "content_object_id":"topic/1", - "collection":"topic", - "title":"Topic title 1", - "agenda_item_number":"itemNr. Topic1" - }, - "yes":"4.000000", - "no":"5.000000", - "abstain":"6.000000" - } - ], - "entitled_users_at_stop": [ - { - "voted": false, - "present": true, - "user_id": 1, - "vote_delegated_to_user_id": 2, - "user": { - "id": 1, - "title": "Billy", - "first_name": "Bob", - "last_name": "Buckingham" - } - }, - { - "voted": false, - "present": true, - "user_id": 4, - "user_merged_into_id": 6, - "user": { - "id": 6, - "title": "Sir", - "first_name": "Shawn Stanley Sheldon", - "last_name": "Sinclair" - } - } - ], - "is_pseudoanonymized":false, - "pollmethod":"YNA", - "onehundred_percent_base":"YNA", - "votesvalid": "2.000000", - "votesinvalid": "9.000000", - "votescast": "2.000000", - "global_option":{ - "yes":"14.000000", - "no":"15.000000", - "abstain":"16.000000" - } - } - `, - }, - { - "Poll state finished", - changeData(data, map[dskey.Key][]byte{ - dskey.MustKey("poll/1/state"): []byte(`"finished"`), - }), - `{ - "id":1, - "content_object_id":"motion/1", - "title_information": { - "content_object_id":"motion/1", - "collection":"motion", - "title":"Motion title 1", - "number":"motion number 1234", - "agenda_item_number":"itemNr. Motion1" - }, - "title":"Poll Title 1", - "description":"Poll description 1", - "type":"analog", - "state":"finished", - "global_yes":false, - "global_no":true, - "global_abstain":false, - "live_voting_enabled":false, - "options": [ - { - "content_object_id":"topic/2", - "text":"Option text", - "content_object":{ - "content_object_id":"topic/2", - "collection":"topic", - "title":"Topic title 2", - "agenda_item_number":"itemNr. Topic2" - } - }, - { - "content_object_id":"topic/1", - "text":"Option text", - "content_object":{ - "content_object_id":"topic/1", - "collection":"topic", - "title":"Topic title 1", - "agenda_item_number":"itemNr. Topic1" - } - } - ] - } - `, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ds := dsmock.Stub(tt.data) - fetch := datastore.NewFetcher(ds) - - p7on := &projector.Projection{ - ContentObjectID: "poll/1", - Options: []byte(`{"single_votes":true}`), - } - - bs, err := pollSlide.Slide(context.Background(), fetch, p7on) - assert.NoError(t, err) - assert.JSONEq(t, tt.expect, string(bs)) - }) - } -} - -func TestPollLiveVote(t *testing.T) { - s := new(projector.SlideStore) - slide.Poll(s) - slide.Motion(s) - slide.Topic(s) - - pollSlide := s.GetSlider("poll") - assert.NotNilf(t, pollSlide, "Slide with name `poll` not found.") - - data := dsmock.YAMLData(` - user: - 1: - title: Billy - first_name: Bob - last_name: Buckingham - username: BillyBobBuckingham - member_number: 123456789abcdef - meeting_user_ids: [1113] - meeting_ids: [1] - 2: - first_name: Johnny - last_name: Johnson - username: JohnnySonOfJohn - meeting_user_ids: [1112] - meeting_ids: [1] - 6: - title: Sir - first_name: Shawn Stanley Sheldon - last_name: Sinclair - username: SirSinclair - meeting_user_ids: [1111] - meeting_ids: [1] - meeting_user: - 1111: - user_id: 6 - structure_level_ids: [1] - 1112: - user_id: 2 - structure_level_ids: [2] - 1113: - user_id: 1 - structure_level: - 1: - name: Birmingham - meeting_id: 111 - 2: - name: Fooo - meeting_id: 111 - meeting: - 111: - poll_ids: [1] - meeting_user_ids: [1111, 1112, 1116] - user_ids: [1,2,6] - structure_level_ids: [1, 2] - group: - 1: - meeting_user_ids: [1111, 1112, 1113] - poll: - 1: - content_object_id: motion/1 - title: Poll Title 1 - description: Poll description 1 - type: analog - state: running - global_yes: false - global_no: true - global_abstain: false - entitled_group_ids: [1] - option_ids: [1, 2] - is_pseudoanonymized: false - pollmethod: YNA - onehundred_percent_base: YNA - votesvalid: "2.000000" - votesinvalid: "9.000000" - votescast: "2.000000" - global_option_id: 3 - meeting_id: 111 - live_votes: {1: "{\"value\": {\"31\": \"Y\"}, \"weight\": \"1.000000\"}"} - live_voting_enabled: true - motion: - 1: - title: Motion title 1 - number: motion number 1234 - agenda_item_id: 1 - option: - 1: - text: Option text - content_object_id: topic/1 - yes: "4.000000" - no: "5.000000" - abstain: "6.000000" - weight: 10 - 2: - text: Option text - content_object_id: topic/2 - yes: "5.000000" - no: "4.000000" - abstain: "3.000000" - weight: 3 - 3: - yes: "14.000000" - no: "15.000000" - abstain: "16.000000" - topic: - 1: - title: Topic title 1 - text: Topic text 1 - agenda_item_id: 2 - 2: - title: Topic title 2 - text: Topic text 2 - agenda_item_id: 3 - agenda_item/1/item_number: itemNr. Motion1 - agenda_item/2/item_number: itemNr. Topic1 - agenda_item/3/item_number: itemNr. Topic2 - `) - - for _, tt := range []struct { - name string - data map[dskey.Key][]byte - expect string - }{ - { - "Running poll", - data, - `{ - "id":1, - "content_object_id":"motion/1", - "title_information": { - "content_object_id":"motion/1", - "collection":"motion", - "title":"Motion title 1", - "number":"motion number 1234", - "agenda_item_number":"itemNr. Motion1" - }, - "title":"Poll Title 1", - "description":"Poll description 1", - "type":"analog", - "state":"running", - "global_yes":false, - "global_no":true, - "global_abstain":false, - "live_voting_enabled":true, - "options": [ - { - "content_object_id":"topic/2", - "text":"Option text", - "content_object":{ - "content_object_id":"topic/2", - "collection":"topic", - "title":"Topic title 2", - "agenda_item_number":"itemNr. Topic2" - } - }, - { - "content_object_id":"topic/1", - "text":"Option text", - "content_object":{ - "content_object_id":"topic/1", - "collection":"topic", - "title":"Topic title 1", - "agenda_item_number":"itemNr. Topic1" - } - } - ], - "entitled_structure_levels": { - "1": "Birmingham", - "2": "Fooo" - }, - "entitled_users": { - "1": { - "present": false, - "votes": {"31": "Y"}, - "weight": "1.000000", - "user_data": { - "id": 1, - "title": "Billy", - "first_name": "Bob", - "last_name": "Buckingham" - } - }, - "2": { - "present": false, - "structure_level_id": 2, - "user_data": { - "id": 2, - "first_name": "Johnny", - "last_name": "Johnson" - } - }, - "6": { - "present": false, - "structure_level_id": 1, - "user_data": { - "id": 6, - "title": "Sir", - "first_name": "Shawn Stanley Sheldon", - "last_name": "Sinclair" - } - } - }, - "is_pseudoanonymized":false, - "pollmethod":"YNA", - "onehundred_percent_base":"YNA" - } - `, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ds := dsmock.Stub(tt.data) - fetch := datastore.NewFetcher(ds) - - p7on := &projector.Projection{ - ContentObjectID: "poll/1", - Options: []byte(`{"single_votes":true}`), - } - - bs, err := pollSlide.Slide(context.Background(), fetch, p7on) - assert.NoError(t, err) - assert.JSONEq(t, tt.expect, string(bs)) - }) - } -} diff --git a/internal/projector/slide/projector.go b/internal/projector/slide/projector.go deleted file mode 100644 index a5f0a816..00000000 --- a/internal/projector/slide/projector.go +++ /dev/null @@ -1,91 +0,0 @@ -package slide - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" -) - -type dbProjectorCountdown struct { - ID int `json:"id"` - Description string `json:"description"` - Running bool `json:"running"` - CountdownTime json.RawMessage `json:"countdown_time"` - DefaultTime json.RawMessage `json:"default_time"` - MeetingID int `json:"meeting_id"` -} - -func projectorCountdownFromMap(in map[string]json.RawMessage) (*dbProjectorCountdown, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding projector countdown data: %w", err) - } - - var pc dbProjectorCountdown - if err := json.Unmarshal(bs, &pc); err != nil { - return nil, fmt.Errorf("decoding projector countdown data: %w", err) - } - return &pc, nil -} - -type dbProjectorMessage struct { - ID int `json:"id"` - Message string `json:"message"` -} - -func projectorMessageFromMap(in map[string]json.RawMessage) (*dbProjectorMessage, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding projector message data: %w", err) - } - - var pm dbProjectorMessage - if err := json.Unmarshal(bs, &pm); err != nil { - return nil, fmt.Errorf("decoding projector message data: %w", err) - } - return &pm, nil -} - -// ProjectorCountdown renders the projector_countdown slide. -func ProjectorCountdown(store *projector.SlideStore) { - store.RegisterSliderFunc("projector_countdown", func(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection) (encoded []byte, err error) { - data := fetch.Object(ctx, p7on.ContentObjectID, "id", "description", "running", "default_time", "countdown_time", "meeting_id") - pc, err := projectorCountdownFromMap(data) - if err != nil { - return nil, fmt.Errorf("get projector countdown from map: %w", err) - } - pcwarningTime := datastore.Int(ctx, fetch.FetchIfExist, "meeting/%d/projector_countdown_warning_time", pc.MeetingID) - if err := fetch.Err(); err != nil { - return nil, err - } - - responseValue, err := json.Marshal(map[string]interface{}{"description": pc.Description, "running": pc.Running, "default_time": pc.DefaultTime, "countdown_time": pc.CountdownTime, "warning_time": pcwarningTime}) - if err != nil { - return nil, fmt.Errorf("encoding response for projector countdown slide: %w", err) - } - return responseValue, err - }) -} - -// ProjectorMessage renders the projector_message slide. -func ProjectorMessage(store *projector.SlideStore) { - store.RegisterSliderFunc("projector_message", func(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection) (encoded []byte, err error) { - data := fetch.Object(ctx, p7on.ContentObjectID, "id", "message") - if err := fetch.Err(); err != nil { - return nil, err - } - - projectorMessage, err := projectorMessageFromMap(data) - if err != nil { - return nil, fmt.Errorf("get projector message from map: %w", err) - } - responseValue, err := json.Marshal(map[string]interface{}{"message": projectorMessage.Message}) - if err != nil { - return nil, fmt.Errorf("encoding response for projector message slide: %w", err) - } - return responseValue, err - }) -} diff --git a/internal/projector/slide/projector_test.go b/internal/projector/slide/projector_test.go deleted file mode 100644 index 92881d9e..00000000 --- a/internal/projector/slide/projector_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package slide_test - -import ( - "context" - "testing" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/slide" - "github.com/OpenSlides/openslides-go/datastore/dskey" - "github.com/OpenSlides/openslides-go/datastore/dsmock" - "github.com/stretchr/testify/assert" -) - -func TestProjectorCountdown(t *testing.T) { - s := new(projector.SlideStore) - slide.ProjectorCountdown(s) - - pcSlide := s.GetSlider("projector_countdown") - assert.NotNilf(t, pcSlide, "Slide with name `projector_countdown` not found.") - - data := dsmock.YAMLData(` - projector_countdown/1: - description: description text - running: true - countdown_time: 200.3445678 - default_time: 0 - meeting_id: 1 - meeting/1/projector_countdown_warning_time: 100 - `) - - for _, tt := range []struct { - name string - data map[dskey.Key][]byte - expect string - }{ - { - "Starter", - data, - `{ - "countdown_time":200.3445678, - "default_time":0, - "description":"description text", - "running":true, - "warning_time":100}`, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ds := dsmock.Stub(tt.data) - fetch := datastore.NewFetcher(ds) - - p7on := &projector.Projection{ - ContentObjectID: "projector_countdown/1", - } - - bs, err := pcSlide.Slide(context.Background(), fetch, p7on) - assert.NoError(t, err) - assert.JSONEq(t, tt.expect, string(bs)) - }) - } -} - -func TestProjectorMessage(t *testing.T) { - s := new(projector.SlideStore) - slide.ProjectorMessage(s) - - pmSlide := s.GetSlider("projector_message") - assert.NotNilf(t, pmSlide, "Slide with name `projector_message` not found.") - - data := dsmock.YAMLData(` - projector_message/1/message: Shine on you crazy diamond - `) - - for _, tt := range []struct { - name string - data map[dskey.Key][]byte - expect string - }{ - { - "Starter", - data, - `{"message": "Shine on you crazy diamond"}`, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ds := dsmock.Stub(tt.data) - fetch := datastore.NewFetcher(ds) - - p7on := &projector.Projection{ - ContentObjectID: "projector_message/1", - } - - bs, err := pmSlide.Slide(context.Background(), fetch, p7on) - assert.NoError(t, err) - assert.JSONEq(t, tt.expect, string(bs)) - }) - } -} diff --git a/internal/projector/slide/slide.go b/internal/projector/slide/slide.go deleted file mode 100644 index edb84096..00000000 --- a/internal/projector/slide/slide.go +++ /dev/null @@ -1,28 +0,0 @@ -package slide - -import ( - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" -) - -// Slides returns all OpenSlides-Slides. -func Slides() *projector.SlideStore { - s := new(projector.SlideStore) - AgendaItemList(s) - Assignment(s) - ListOfSpeaker(s) - CurrentListOfSpeakers(s) - CurrentSpeakerChyron(s) - CurrentSpeakingStructureLevel(s) - CurrentStructureLevelList(s) - MeetingMediafile(s) - Motion(s) - MotionBlock(s) - Poll(s) - ProjectorCountdown(s) - ProjectorMessage(s) - Topic(s) - User(s) - PollCandidateList(s) - WiFiAccessData(s) - return s -} diff --git a/internal/projector/slide/topic.go b/internal/projector/slide/topic.go deleted file mode 100644 index 51ab5bee..00000000 --- a/internal/projector/slide/topic.go +++ /dev/null @@ -1,106 +0,0 @@ -package slide - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" -) - -type dbTopic struct { - ID int `json:"id"` - Title string `json:"title"` - Text string `json:"text"` - AgendaItemID int `json:"agenda_item_id"` -} - -func topicFromMap(in map[string]json.RawMessage) (*dbTopic, error) { - bs, err := json.Marshal(in) - if err != nil { - return nil, fmt.Errorf("encoding topic data: %w", err) - } - - var t dbTopic - if err := json.Unmarshal(bs, &t); err != nil { - return nil, fmt.Errorf("decoding topic data: %w", err) - } - return &t, nil -} - -// Topic renders the topic slide. -func Topic(store *projector.SlideStore) { - store.RegisterSliderFunc("topic", func(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection) (encoded []byte, err error) { - data := fetch.Object( - ctx, - p7on.ContentObjectID, - "id", - "title", - "text", - "agenda_item_id", - ) - - topic, err := topicFromMap(data) - if err != nil { - return nil, fmt.Errorf("get topic: %w", err) - } - - var itemNumber string - if topic.AgendaItemID > 0 { - itemNumber = datastore.String(ctx, fetch.FetchIfExist, "agenda_item/%d/item_number", topic.AgendaItemID) - } - if err := fetch.Err(); err != nil { - return nil, err - } - - out := struct { - Title string `json:"title"` - Text string `json:"text"` - AgendaItemNumber string `json:"item_number"` - }{ - Title: topic.Title, - Text: topic.Text, - AgendaItemNumber: itemNumber, - } - - responseValue, err := json.Marshal(out) - if err != nil { - return nil, fmt.Errorf("encoding response slide topic: %w", err) - } - return responseValue, err - }) - - store.RegisterGetTitleInformationFunc("topic", func(ctx context.Context, fetch *datastore.Fetcher, fqid string, itemNumber string, meetingID int) (json.RawMessage, error) { - data := fetch.Object(ctx, fqid, "id", "title", "agenda_item_id") - topic, err := topicFromMap(data) - if err != nil { - return nil, fmt.Errorf("get topic from map: %w", err) - } - - if itemNumber == "" && topic.AgendaItemID > 0 { - itemNumber = datastore.String(ctx, fetch.FetchIfExist, "agenda_item/%d/item_number", topic.AgendaItemID) - } - if err := fetch.Err(); err != nil { - return nil, err - } - - title := struct { - Collection string `json:"collection"` - ContentObjectID string `json:"content_object_id"` - Title string `json:"title"` - AgendaItemNumber string `json:"agenda_item_number"` - }{ - "topic", - fqid, - topic.Title, - itemNumber, - } - - bs, err := json.Marshal(title) - if err != nil { - return nil, fmt.Errorf("encoding title: %w", err) - } - return bs, err - }) -} diff --git a/internal/projector/slide/topic_test.go b/internal/projector/slide/topic_test.go deleted file mode 100644 index b1a6eab8..00000000 --- a/internal/projector/slide/topic_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package slide_test - -import ( - "context" - "testing" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/slide" - "github.com/OpenSlides/openslides-go/datastore/dsmock" - "github.com/stretchr/testify/assert" -) - -func TestTopic(t *testing.T) { - s := new(projector.SlideStore) - slide.Topic(s) - - topicSlide := s.GetSlider("topic") - assert.NotNilf(t, topicSlide, "Slide with name `topic` not found.") - - for _, tt := range []struct { - name string - data map[string]string - expect string - }{ - { - "Topic Complete", - map[string]string{ - "topic/1/id": `1`, - "topic/1/title": `"topic title 1"`, - "topic/1/text": `"topic text 1"`, - "topic/1/agenda_item_id": `1`, - "agenda_item/1/id": "1", - "agenda_item/1/item_number": `"AI-Item 1"`, - }, - `{"title":"topic title 1","text":"topic text 1","item_number":"AI-Item 1"}`, - }, - { - "Without Agenda Item", - map[string]string{ - "topic/1/id": `1`, - "topic/1/title": `"topic title 1"`, - "topic/1/text": `"topic text 1"`, - }, - `{"item_number":"", "text":"topic text 1", "title":"topic title 1"}`, - }, - { - "Agenda Item without number", - map[string]string{ - "topic/1/id": `1`, - "topic/1/title": `"topic title 1"`, - "topic/1/text": `"topic text 1"`, - "agenda_item/1/id": "1", - "topic/1/agenda_item_id": `1`, - }, - `{"title":"topic title 1","text":"topic text 1","item_number":""}`, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ds := dsmock.Stub(convertData(tt.data)) - fetch := datastore.NewFetcher(ds) - - p7on := &projector.Projection{ - ContentObjectID: "topic/1", - } - - bs, err := topicSlide.Slide(context.Background(), fetch, p7on) - assert.NoError(t, err) - assert.JSONEq(t, tt.expect, string(bs)) - }) - } -} diff --git a/internal/projector/slide/user.go b/internal/projector/slide/user.go deleted file mode 100644 index 4c2e0458..00000000 --- a/internal/projector/slide/user.go +++ /dev/null @@ -1,194 +0,0 @@ -package slide - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - "strings" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" -) - -// DbUser is the class with methods to get needed User Informations -type DbUser struct { - Username string `json:"username"` - Title string `json:"title"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Level string `json:"structure_level"` -} - -// NewUser gets the user from datastore and return the user as DbUser struct -// together with keys and error. -// The meeting_id is used only to get the user-level for this meeting. -func NewUser(ctx context.Context, fetch *datastore.Fetcher, id, meetingID int) (*DbUser, error) { - fields := []string{ - "username", - "title", - "first_name", - "last_name", - "meeting_user_ids", - } - - data := fetch.Object(ctx, fmt.Sprintf("user/%d", id), fields...) - if err := fetch.Err(); err != nil { - return nil, fmt.Errorf("getting user object for id %d: %w", id, err) - } - - bs, err := json.Marshal(data) - if err != nil { - return nil, fmt.Errorf("encoding user data: %w", err) - } - - var u DbUser - if err := json.Unmarshal(bs, &u); err != nil { - return nil, fmt.Errorf("decoding user data: %w", err) - } - - if u.FirstName == "" && u.LastName == "" && u.Username == "" { - return nil, fmt.Errorf("neither firstName, lastName nor username found") - } - - if meetingID == 0 || data["meeting_user_ids"] == nil { - return &u, nil - } - - var meetingUserIDs []int - if err := json.Unmarshal(data["meeting_user_ids"], &meetingUserIDs); err != nil { - return nil, fmt.Errorf("decoding meeting_user_ids: %w", err) - } - - for _, id := range meetingUserIDs { - var mid int - fetch.Fetch(ctx, &mid, "meeting_user/%d/meeting_id", id) - if err := fetch.Err(); err != nil { - return nil, fmt.Errorf("get meeting of meeting_user %d: %w", id, err) - } - - if mid != meetingID { - continue - } - - var structureLevelIds []int - fetch.FetchIfExist(ctx, &structureLevelIds, "meeting_user/%d/structure_level_ids", id) - if err := fetch.Err(); err != nil { - return nil, fmt.Errorf("get structure level of meeting_user %d: %w", id, err) - } - - var structureLevels []string - for _, sid := range structureLevelIds { - var level string - fetch.Fetch(ctx, &level, "structure_level/%d/name", sid) - if err := fetch.Err(); err != nil { - return nil, fmt.Errorf("get structure level %d: %w", sid, err) - } - structureLevels = append(structureLevels, level) - } - - u.Level = strings.Join(structureLevels, ", ") - break - } - - return &u, nil -} - -// UserRepresentation returns the meeting-dependent string for the given user. -func (u *DbUser) UserRepresentation(meetingID int) string { - name := u.UserShortName() - level := u.UserStructureLevel(meetingID) - if level == "" { - return name - } - return fmt.Sprintf("%s (%s)", name, level) -} - -// UserStructureLevel returns in first place the meeting specific level, -// otherwise the default level. -// It is assumed that the Level-field in DbUser-struct contains the -// meeting dependent level. -func (u *DbUser) UserStructureLevel(meetingID int) string { - return u.Level -} - -// UserShortName returns the short name as "title first_name last_name". -// Without first_name and last_name, uses username instead. -func (u *DbUser) UserShortName() string { - parts := func(sp ...string) []string { - var full []string - for _, s := range sp { - if s == "" { - continue - } - full = append(full, s) - } - return full - }(u.FirstName, u.LastName) - - if len(parts) == 0 { - parts = append(parts, u.Username) - } else if u.Title != "" { - parts = append([]string{u.Title}, parts...) - } - return strings.Join(parts, " ") -} - -// User renders the user slide. -func User(store *projector.SlideStore) { - store.RegisterSliderFunc("user", func(ctx context.Context, fetch *datastore.Fetcher, p7on *projector.Projection) (responseValue []byte, err error) { - id, err := strconv.Atoi(strings.Split(p7on.ContentObjectID, "/")[1]) - if err != nil { - return nil, fmt.Errorf("getting user id: %w", err) - } - - user, err := NewUser(ctx, fetch, id, p7on.MeetingID) - if err != nil { - return nil, fmt.Errorf("getting new user id: %w", err) - } - if err := fetch.Err(); err != nil { - return nil, err - } - - out := struct { - User string `json:"user"` - }{ - user.UserRepresentation(p7on.MeetingID), - } - responseValue, err = json.Marshal(out) - if err != nil { - return nil, fmt.Errorf("encoding response slide user: %w", err) - } - return responseValue, err - }) - - store.RegisterGetTitleInformationFunc("user", func(ctx context.Context, fetch *datastore.Fetcher, fqid string, itemNumber string, meetingID int) (json.RawMessage, error) { - id, err := strconv.Atoi(strings.Split(fqid, "/")[1]) - if err != nil { - return nil, fmt.Errorf("getting user id: %w", err) - } - - user, err := NewUser(ctx, fetch, id, meetingID) - if err != nil { - return nil, fmt.Errorf("loading user: %w", err) - } - if err := fetch.Err(); err != nil { - return nil, err - } - - out := struct { - Collection string `json:"collection"` - ContentObjectID string `json:"content_object_id"` - Username string `json:"username"` - }{ - "user", - fqid, - user.UserRepresentation(meetingID), - } - responseValue, err := json.Marshal(out) - if err != nil { - return nil, fmt.Errorf("encoding title: %w", err) - } - return responseValue, err - }) -} diff --git a/internal/projector/slide/user_test.go b/internal/projector/slide/user_test.go deleted file mode 100644 index a3d8fd92..00000000 --- a/internal/projector/slide/user_test.go +++ /dev/null @@ -1,172 +0,0 @@ -package slide_test - -import ( - "context" - "testing" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/slide" - "github.com/OpenSlides/openslides-go/datastore/dskey" - "github.com/OpenSlides/openslides-go/datastore/dsmock" - "github.com/stretchr/testify/assert" -) - -func setup(t *testing.T) projector.Slider { - s := new(projector.SlideStore) - slide.User(s) - - userSlide := s.GetSlider("user") - assert.NotNilf(t, userSlide, "Slide with name `user` not found.") - return userSlide -} - -func TestUser(t *testing.T) { - userSlide := setup(t) - - for _, tt := range []struct { - name string - data map[string]string - expect string - }{ - { - "Only Username", - map[string]string{ - "user/1/id": "1", - "user/1/username": `"jonny123"`, - }, - `{"user":"jonny123"}`, - }, - { - "Only Firstname", - map[string]string{ - "user/1/id": "1", - "user/1/first_name": `"Jonny"`, - }, - `{"user":"Jonny"}`, - }, - { - "Only Lastname", - map[string]string{ - "user/1/id": "1", - "user/1/last_name": `"Bo"`, - }, - `{"user":"Bo"}`, - }, - { - "Firstname Lastname", - map[string]string{ - "user/1/id": "1", - "user/1/first_name": `"Jonny"`, - "user/1/last_name": `"Bo"`, - }, - `{"user":"Jonny Bo"}`, - }, - { - "Title Firstname Lastname", - map[string]string{ - "user/1/id": "1", - "user/1/title": `"Dr."`, - "user/1/first_name": `"Jonny"`, - "user/1/last_name": `"Bo"`, - }, - `{"user":"Dr. Jonny Bo"}`, - }, - { - "Title Firstname Lastname Username", - map[string]string{ - "user/1/id": "1", - "user/1/username": `"jonny123"`, - "user/1/title": `"Dr."`, - "user/1/first_name": `"Jonny"`, - "user/1/last_name": `"Bo"`, - }, - `{"user":"Dr. Jonny Bo"}`, - }, - { - "Title Username", - map[string]string{ - "user/1/id": "1", - "user/1/username": `"jonny123"`, - "user/1/title": `"Dr."`, - }, - `{"user":"jonny123"}`, - }, - { - "Title Firstname Lastname Username DefaultLevel", - map[string]string{ - "user/1/id": "1", - "user/1/username": `"jonny123"`, - "user/1/title": `"Dr."`, - "user/1/first_name": `"Jonny"`, - "user/1/last_name": `"Bo"`, - }, - `{"user":"Dr. Jonny Bo"}`, - }, - { - "Username DefaultLevel", - map[string]string{ - "user/1/id": "1", - "user/1/username": `"jonny123"`, - }, - `{"user":"jonny123"}`, - }, - } { - t.Run(tt.name, func(t *testing.T) { - ds := dsmock.Stub(convertData(tt.data)) - fetch := datastore.NewFetcher(ds) - - p7on := &projector.Projection{ - ContentObjectID: "user/1", - MeetingID: 222, - } - - bs, err := userSlide.Slide(context.Background(), fetch, p7on) - assert.NoError(t, err) - assert.JSONEq(t, tt.expect, string(bs)) - }) - } -} - -func TestUserWithoutMeeting(t *testing.T) { - userSlide := setup(t) - - data := convertData(map[string]string{ - "user/1/id": "1", - "user/1/username": `"jonny123"`, - "user/1/title": `"Dr."`, - "user/1/first_name": `"Jonny"`, - "user/1/last_name": `"Bo"`, - }) - - ds := dsmock.Stub(data) - fetch := datastore.NewFetcher(ds) - - p7on := &projector.Projection{ - ContentObjectID: "user/1", - } - - bs, err := userSlide.Slide(context.Background(), fetch, p7on) - assert.NoError(t, err) - assert.JSONEq(t, `{"user":"Dr. Jonny Bo"}`, string(bs)) -} - -func TestUserWithError(t *testing.T) { - userSlide := setup(t) - data := map[dskey.Key][]byte{ - dskey.MustKey("user/1/id"): []byte(`1`), - } - - ds := dsmock.Stub(data) - fetch := datastore.NewFetcher(ds) - - p7on := &projector.Projection{ - ContentObjectID: "user/1", - MeetingID: 222, - } - - bs, err := userSlide.Slide(context.Background(), fetch, p7on) - assert.Nil(t, bs) - assert.Error(t, err) - assert.Contains(t, err.Error(), "neither firstName, lastName nor username found") -} diff --git a/internal/projector/slides.go b/internal/projector/slides.go deleted file mode 100644 index ede0ef63..00000000 --- a/internal/projector/slides.go +++ /dev/null @@ -1,79 +0,0 @@ -package projector - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/OpenSlides/openslides-autoupdate-service/internal/projector/datastore" -) - -// Slider knows how to create a slide. -type Slider interface { - Slide(ctx context.Context, fetch *datastore.Fetcher, p7on *Projection) (encoded []byte, err error) -} - -// Titler defines the interface for GetTitleInformation-function, used for individual objects. -type Titler interface { - GetTitleInformation(ctx context.Context, fetch *datastore.Fetcher, fqid string, itemNumber string, meetingID int) (json.RawMessage, error) -} - -// SliderFunc is a function that implements the Slider interface. -type SliderFunc func(ctx context.Context, fetch *datastore.Fetcher, p7on *Projection) (encoded []byte, err error) - -// Slide calls the func. -func (f SliderFunc) Slide(ctx context.Context, fetch *datastore.Fetcher, p7on *Projection) (encoded []byte, err error) { - return f(ctx, fetch, p7on) -} - -// TitlerFunc is a type that implements the Titler interface. -type TitlerFunc func(ctx context.Context, fetch *datastore.Fetcher, fqid string, itemNumber string, meetingID int) (json.RawMessage, error) - -// GetTitleInformation calls the func. -func (f TitlerFunc) GetTitleInformation(ctx context.Context, fetch *datastore.Fetcher, fqid string, itemNumber string, meetingID int) (json.RawMessage, error) { - return f(ctx, fetch, fqid, itemNumber, meetingID) -} - -// SlideStore holds the Slider- and Titler-functions by name. -type SlideStore struct { - slides map[string]Slider - titles map[string]Titler -} - -// RegisterSliderFunc adds a SliderFunc to the store. -func (s *SlideStore) RegisterSliderFunc(name string, f SliderFunc) { - if s.slides == nil { - s.slides = make(map[string]Slider) - } - - if _, ok := s.slides[name]; ok { - panic(fmt.Sprintf("Slide with name %s does already exist", name)) - } - s.slides[name] = f -} - -// GetSlider returns the Slide for the given name. -// -// Returns nil, if there if the name is unknown. -func (s *SlideStore) GetSlider(name string) Slider { - return s.slides[name] -} - -// RegisterGetTitleInformationFunc adds a function of type TitlerFunc to the store. -func (s *SlideStore) RegisterGetTitleInformationFunc(collection string, f TitlerFunc) { - if s.titles == nil { - s.titles = make(map[string]Titler) - } - - if _, ok := s.titles[collection]; ok { - panic(fmt.Sprintf("GetTitleInformation function for collection %s does already exist", collection)) - } - s.titles[collection] = f -} - -// GetTitleInformationFunc returns a Titler-function for the given name. -// -// Returns nil, if the name is unknown. -func (s *SlideStore) GetTitleInformationFunc(name string) Titler { - return s.titles[name] -}