Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
-- +micrate Up
-- SQL in section 'Up' is executed when this migration is applied

ALTER TABLE "playlists"
ADD COLUMN IF NOT EXISTS play_at_period INTEGER;

ALTER TABLE "playlists"
ADD COLUMN IF NOT EXISTS play_at_takeover BOOLEAN NOT NULL DEFAULT FALSE;

-- +micrate StatementBegin
DO
$$
BEGIN
IF NOT EXISTS (SELECT *
FROM pg_type typ
INNER JOIN pg_namespace nsp
ON nsp.oid = typ.typnamespace
WHERE nsp.nspname = current_schema()
AND typ.typname = 'signage_plugin_playback_type') THEN
CREATE TYPE signage_plugin_playback_type AS ENUM (
'STATIC',
'INTERACTIVE',
'PLAYSTHROUGH'
);
END IF;
END;
$$
LANGUAGE plpgsql;
-- +micrate StatementEnd

CREATE TABLE IF NOT EXISTS "signage_plugin"(
id TEXT NOT NULL PRIMARY KEY,
authority_id TEXT,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,

name TEXT NOT NULL,
description TEXT,
uri TEXT NOT NULL,
playback_type public.signage_plugin_playback_type NOT NULL DEFAULT 'STATIC'::public.signage_plugin_playback_type,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
params JSONB NOT NULL DEFAULT '{}'::jsonb,
defaults JSONB NOT NULL DEFAULT '{}'::jsonb,
CHECK (jsonb_typeof(params) = 'object'),
CHECK (jsonb_typeof(defaults) = 'object'),
FOREIGN KEY (authority_id) REFERENCES authority(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS signage_plugin_authority_id_index ON "signage_plugin" USING BTREE (authority_id);

ALTER TABLE "playlist_items"
ADD COLUMN IF NOT EXISTS plugin_id TEXT REFERENCES "signage_plugin"(id) ON DELETE CASCADE;

ALTER TABLE "playlist_items"
ADD COLUMN IF NOT EXISTS plugin_params JSONB NOT NULL DEFAULT '{}'::jsonb;

CREATE INDEX IF NOT EXISTS playlist_items_plugin_id_index ON "playlist_items" USING BTREE (plugin_id);

-- +micrate Down
-- SQL section 'Down' is executed when this migration is rolled back

ALTER TABLE "playlist_items" DROP COLUMN IF EXISTS plugin_params;
ALTER TABLE "playlist_items" DROP COLUMN IF EXISTS plugin_id;
DROP TABLE IF EXISTS "signage_plugin";
ALTER TABLE "playlists" DROP COLUMN IF EXISTS play_at_period;
ALTER TABLE "playlists" DROP COLUMN IF EXISTS play_at_takeover;

-- Drop the enum type
DROP TYPE IF EXISTS public.signage_plugin_playback_type;
43 changes: 43 additions & 0 deletions spec/generator.cr
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,29 @@ module PlaceOS::Model
play
end

def self.signage_plugin(
name : String = Faker::Hacker.noun,
description : String = "",
uri : String = "/plugins/default",
authority : Authority? = nil,
params : Hash(String, JSON::Any) = {"type" => JSON::Any.new("object"), "properties" => JSON::Any.new({"play_at_period" => JSON::Any.new({"type" => JSON::Any.new("integer")} of String => JSON::Any)} of String => JSON::Any)},
defaults : Hash(String, JSON::Any) = {"play_at_period" => JSON::Any.new(10_i64)},
)
unless authority
existing = Authority.find_by_domain("localhost")
authority = existing || self.authority.save!
end

SignagePlugin.new(
name: name,
description: description,
uri: uri,
authority_id: authority.id,
params: params,
defaults: defaults,
)
end

def self.revision(playlist : Playlist = playlist.save!, user : User = user.save!)
rev = Playlist::Revision.new
rev.playlist_id = playlist.id
Expand Down Expand Up @@ -155,6 +178,26 @@ module PlaceOS::Model
item
end

def self.plugin_item(
name : String = Faker::Hacker.noun,
plugin : SignagePlugin = signage_plugin.save!,
plugin_params : Hash(String, JSON::Any) = {} of String => JSON::Any,
authority : Authority? = nil,
)
unless authority
existing = Authority.find_by_domain("localhost")
authority = existing || self.authority.save!
end

item = Playlist::Item.new
item.authority_id = authority.id
item.name = name
item.media_type = Playlist::Item::MediaType::Plugin
item.plugin_id = plugin.id
item.plugin_params = plugin_params
item
end

def self.booking(tenant_id, asset_ids : Array(String), start : Time, ending : Time, booking_type = "booking", parent_id = nil, event_id = nil)
user_name = Faker::Hacker.noun
user_email = Faker::Internet.email
Expand Down
80 changes: 80 additions & 0 deletions spec/playlist_item_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,86 @@ module PlaceOS::Model
updated.should_not eq playlist.updated_at
end

it "creates a plugin item" do
plugin = Generator.signage_plugin.save!
item = Generator.plugin_item(plugin: plugin)
item.save!

found = Playlist::Item.find(item.id.as(String))
found.media_type.should eq Playlist::Item::MediaType::Plugin
found.plugin_id.should eq plugin.id
end

it "requires a plugin_id for plugin items" do
item = Generator.item
item.media_type = Playlist::Item::MediaType::Plugin
item.media_uri = nil
item.plugin_id = nil
item.save.should eq false
item.errors.any? { |e| e.field == :plugin_id }.should eq true
end

it "validates plugin_params keys exist in plugin params properties" do
plugin = Generator.signage_plugin(
params: {
"type" => JSON::Any.new("object"),
"properties" => JSON::Any.new({
"play_at_period" => JSON::Any.new({"type" => JSON::Any.new("integer")} of String => JSON::Any),
} of String => JSON::Any),
},
).save!

item = Generator.plugin_item(
plugin: plugin,
plugin_params: {"bad_key" => JSON::Any.new(5_i64)},
)
item.save.should eq false
item.errors.any? { |e| e.field == :plugin_params }.should eq true
end

it "validates required params are satisfied by defaults merged with plugin_params" do
plugin = Generator.signage_plugin(
params: {
"type" => JSON::Any.new("object"),
"properties" => JSON::Any.new({
"play_at_period" => JSON::Any.new({"type" => JSON::Any.new("integer")} of String => JSON::Any),
"color" => JSON::Any.new({"type" => JSON::Any.new("string")} of String => JSON::Any),
} of String => JSON::Any),
"required" => JSON::Any.new([JSON::Any.new("play_at_period"), JSON::Any.new("color")]),
},
defaults: {"play_at_period" => JSON::Any.new(10_i64)},
).save!

# missing "color" which is required and has no default
item = Generator.plugin_item(
plugin: plugin,
plugin_params: {} of String => JSON::Any,
)
item.save.should eq false
item.errors.any? { |e| e.field == :plugin_params && e.message.to_s.includes?("color") }.should eq true
end

it "allows plugin_params when defaults cover required params" do
plugin = Generator.signage_plugin(
params: {
"type" => JSON::Any.new("object"),
"properties" => JSON::Any.new({
"play_at_period" => JSON::Any.new({"type" => JSON::Any.new("integer")} of String => JSON::Any),
"color" => JSON::Any.new({"type" => JSON::Any.new("string")} of String => JSON::Any),
} of String => JSON::Any),
"required" => JSON::Any.new([JSON::Any.new("play_at_period"), JSON::Any.new("color")]),
},
defaults: {"play_at_period" => JSON::Any.new(10_i64)},
).save!

# "color" provided in plugin_params, "play_at_period" covered by defaults
item = Generator.plugin_item(
plugin: plugin,
plugin_params: {"color" => JSON::Any.new("red")},
)
item.save.should eq true
end

it "updates playlists when an item is modified" do
revision = Generator.revision

Expand Down
86 changes: 86 additions & 0 deletions spec/signage_plugin_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
require "./helper"

module PlaceOS::Model
describe SignagePlugin do
Spec.before_each do
SignagePlugin.clear
end

test_round_trip(SignagePlugin)

it "creates a signage plugin" do
plugin = Generator.signage_plugin
plugin.save!

found = SignagePlugin.find(plugin.id.as(String))
found.name.should eq plugin.name
found.enabled.should eq true
end

it "requires a name" do
plugin = Generator.signage_plugin(name: "")
plugin.save.should eq false
plugin.errors.first.field.should eq :name
end

it "validates defaults keys exist in params properties" do
plugin = Generator.signage_plugin(
params: {
"type" => JSON::Any.new("object"),
"properties" => JSON::Any.new({
"play_at_period" => JSON::Any.new({"type" => JSON::Any.new("integer")} of String => JSON::Any),
} of String => JSON::Any),
},
defaults: {
"play_at_period" => JSON::Any.new(10_i64),
},
)
plugin.save.should eq true
end

it "rejects defaults with keys not in params properties" do
plugin = Generator.signage_plugin(
params: {
"type" => JSON::Any.new("object"),
"properties" => JSON::Any.new({
"play_at_period" => JSON::Any.new({"type" => JSON::Any.new("integer")} of String => JSON::Any),
} of String => JSON::Any),
},
defaults: {
"nonexistent_key" => JSON::Any.new("value"),
},
)
plugin.save.should eq false
plugin.errors.first.field.should eq :defaults
end

it "allows empty defaults" do
plugin = Generator.signage_plugin(
defaults: {} of String => JSON::Any,
)
plugin.save.should eq true
end

it "allows a local resource URI" do
plugin = Generator.signage_plugin(uri: "/plugins/weather")
plugin.save.should eq true
end

it "allows an https URL" do
plugin = Generator.signage_plugin(uri: "https://example.com/plugins/weather")
plugin.save.should eq true
end

it "rejects http URLs" do
plugin = Generator.signage_plugin(uri: "http://example.com/plugins/weather")
plugin.save.should eq false
plugin.errors.any? { |e| e.field == :uri }.should eq true
end

it "requires a uri" do
plugin = Generator.signage_plugin(uri: "")
plugin.save.should eq false
plugin.errors.any? { |e| e.field == :uri }.should eq true
end
end
end
4 changes: 2 additions & 2 deletions src/placeos-models/control_system.cr
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ module PlaceOS::Model
this.validation_error(:camera_snapshot_url, "is an invalid URI") unless Validation.valid_uri?(url)
end

this.camera_snapshot_urls.each do |url|
if !Validation.valid_uri?(url)
this.camera_snapshot_urls.each do |snap_url|
if !Validation.valid_uri?(snap_url)
this.validation_error(:camera_snapshot_urls, "contains an invalid URI")
break
end
Expand Down
4 changes: 4 additions & 0 deletions src/placeos-models/playlist.cr
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ module PlaceOS::Model
attribute play_at : Int64? = nil
attribute play_cron : String? = nil

# how many minutes should a scheduled playlist play for / should it takeover the displays
attribute play_at_period : Int32? = nil
attribute play_at_takeover : Bool = false

def should_present?(now : Time = Time.utc, timezone : Bool = false) : Bool
return false unless enabled

Expand Down
36 changes: 36 additions & 0 deletions src/placeos-models/playlist/item.cr
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ module PlaceOS::Model
belongs_to Upload, foreign_key: "media_id", association_name: "media"
belongs_to Upload, foreign_key: "thumbnail_id", association_name: "thumbnail"

# plugin data for playback
belongs_to SignagePlugin, foreign_key: "plugin_id", association_name: "plugin"
attribute plugin_params : Hash(String, JSON::Any) = {} of String => JSON::Any

# other metadata
attribute play_count : Int64 = 0
attribute valid_from : Int64? = nil
Expand Down Expand Up @@ -87,6 +91,8 @@ module PlaceOS::Model
case this.media_type
when .image?, .video?
this.validation_error(:media_id, "must specify a media upload id") unless this.media
when .plugin?
this.validate_plugin
else
if media_uri = this.media_uri.presence
begin
Expand All @@ -103,6 +109,36 @@ module PlaceOS::Model
end
}

protected def validate_plugin
plugin = self.plugin
unless plugin
self.validation_error(:plugin_id, "must specify a plugin id")
return
end

params = self.plugin_params
properties = plugin.params["properties"]?.try(&.as_h?)

# ensure plugin_params keys exist in plugin params properties
params.each_key do |key|
unless properties.try(&.has_key?(key))
self.validation_error(:plugin_params, "key '#{key}' does not exist in plugin params properties")
end
end

# ensure all required params are satisfied by defaults merged with plugin_params
if required = plugin.params["required"]?.try(&.as_a?)
merged = plugin.defaults.merge(params)
required.each do |req_key|
key = req_key.as_s?
next unless key
unless merged.has_key?(key)
self.validation_error(:plugin_params, "missing required param '#{key}'")
end
end
end
end

before_destroy :cleanup_playlists

# Reject any bookings that are current
Expand Down
Loading
Loading