diff --git a/app/furniture/content_block.rb b/app/furniture/content_block.rb
new file mode 100644
index 000000000..91e6c7bfb
--- /dev/null
+++ b/app/furniture/content_block.rb
@@ -0,0 +1,5 @@
+class ContentBlock < ApplicationRecord
+ belongs_to :slot
+ has_one :section, through: :slot
+ has_one :space, through: :section
+end
diff --git a/app/furniture/content_block/content_block_policy.rb b/app/furniture/content_block/content_block_policy.rb
new file mode 100644
index 000000000..0f6adfe77
--- /dev/null
+++ b/app/furniture/content_block/content_block_policy.rb
@@ -0,0 +1,11 @@
+class ContentBlock
+ class ContentBlockPolicy < ApplicationPolicy
+ alias_method :content_block, :object
+ def create?
+ current_person.member_of?(content_block.space)
+ end
+
+ class Scope < ApplicationScope
+ end
+ end
+end
diff --git a/app/furniture/content_block/content_blocks_controller.rb b/app/furniture/content_block/content_blocks_controller.rb
new file mode 100644
index 000000000..b6ff520a7
--- /dev/null
+++ b/app/furniture/content_block/content_blocks_controller.rb
@@ -0,0 +1,9 @@
+class ContentBlock
+ class ContentBlocksController < ApplicationController
+ expose :content_block, scope: -> { policy_scope(ContentBlock, policy_scope_class: ContentBlockPolicy::Scope) }, model: ContentBlock
+
+ def new
+ authorize(content_block, policy_class: ContentBlockPolicy)
+ end
+ end
+end
diff --git a/app/furniture/text_block/text_blocks/new.html.erb b/app/furniture/text_block/text_blocks/new.html.erb
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/lib/appends_routes.rb b/app/lib/appends_routes.rb
new file mode 100644
index 000000000..e69de29bb
diff --git a/app/lib/space_routes.rb b/app/lib/space_routes.rb
index 6f3891ee4..3b68fe6ff 100644
--- a/app/lib/space_routes.rb
+++ b/app/lib/space_routes.rb
@@ -9,6 +9,7 @@ def self.append_routes(router)
Furniture.append_routes(router)
router.resources :furnitures, only: %i[create edit update destroy]
router.resource :hero_image, controller: "room/hero_images"
+ router.resources :content_block, controller: "content_block/content_blocks"
end
router.resources :utilities
diff --git a/app/models/furniture.rb b/app/models/furniture.rb
index 6b0f6560b..f264fafc0 100644
--- a/app/models/furniture.rb
+++ b/app/models/furniture.rb
@@ -5,9 +5,9 @@
# as JSON, so that {Furniture} can be tweaked and configured as appropriate for
# it's particular use case.
class Furniture < ApplicationRecord
- include RankedModel
location(parent: :room)
+ include RankedModel
ranks :slot, with_same: [:room_id]
belongs_to :room, inverse_of: :gizmos
diff --git a/app/models/room.rb b/app/models/room.rb
index 70e4bc042..453764637 100644
--- a/app/models/room.rb
+++ b/app/models/room.rb
@@ -30,6 +30,8 @@ class Room < ApplicationRecord
has_many :gizmos, dependent: :destroy, inverse_of: :room, class_name: :Furniture
accepts_nested_attributes_for :gizmos
+ has_many :slots, dependent: :destroy, inverse_of: :section
+
DESCRIPTION_MAX_LENGTH = 300
validates :description, length: {maximum: DESCRIPTION_MAX_LENGTH, allow_blank: true}
diff --git a/app/models/section.rb b/app/models/section.rb
new file mode 100644
index 000000000..b5439b760
--- /dev/null
+++ b/app/models/section.rb
@@ -0,0 +1,2 @@
+class Section < Room
+end
diff --git a/app/models/slot.rb b/app/models/slot.rb
new file mode 100644
index 000000000..8e1dfced7
--- /dev/null
+++ b/app/models/slot.rb
@@ -0,0 +1,10 @@
+class Slot < ApplicationRecord
+ belongs_to :section, class_name: "Room", inverse_of: :slots
+ self.location_parent = :section
+ has_one :space, through: :section
+
+ belongs_to :slottable, polymorphic: true, inverse_of: :slot
+
+ include RankedModel
+ ranks :slot_order, with_same: [:section_id]
+end
diff --git a/app/policies/slot_policy.rb b/app/policies/slot_policy.rb
new file mode 100644
index 000000000..80da6fd56
--- /dev/null
+++ b/app/policies/slot_policy.rb
@@ -0,0 +1,10 @@
+class SlotPolicy < ApplicationPolicy
+ alias_method :slot, :object
+
+ def create?
+ current_person.operator? || current_person.member_of?(slot.space)
+ end
+
+ class Scope < ApplicationScope
+ end
+end
diff --git a/app/views/rooms/edit.html.erb b/app/views/rooms/edit.html.erb
index fab30b491..63f9f6ef8 100644
--- a/app/views/rooms/edit.html.erb
+++ b/app/views/rooms/edit.html.erb
@@ -11,6 +11,17 @@
<%= render "rooms/hero_image/form", room: room %>
<% end %>
<%- end %>
+
+<%- if current_person.operator? || Rails.env.test? %>
+ <%= render CardComponent.new do |card| %>
+ <%- card.with_header do %>
+
Gizmos (but lighter)
+ <%- end %>
+
+ <%= link_to "Add Content Block", room.location(:new, child: :content_block), class: "button"%>
+ <%- end %>
+<%- end %>
+
<%= render CardComponent.new do %>
Gizmos
diff --git a/db/migrate/20240314003612_create_slots.rb b/db/migrate/20240314003612_create_slots.rb
new file mode 100644
index 000000000..803dcd37a
--- /dev/null
+++ b/db/migrate/20240314003612_create_slots.rb
@@ -0,0 +1,10 @@
+class CreateSlots < ActiveRecord::Migration[7.1]
+ def change
+ create_table :slots, id: :uuid do |t|
+ t.references :section, foreign_key: {to_table: :rooms}, type: :uuid
+ t.references :slottable, polymorphic: true, type: :uuid
+ t.integer :slot_order, null: true
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20240328004901_create_content_blocks.rb b/db/migrate/20240328004901_create_content_blocks.rb
new file mode 100644
index 000000000..695003982
--- /dev/null
+++ b/db/migrate/20240328004901_create_content_blocks.rb
@@ -0,0 +1,7 @@
+class CreateContentBlocks < ActiveRecord::Migration[7.1]
+ def change
+ create_table :content_blocks, id: :uuid do |t|
+ t.timestamps
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index fa5c62e9c..955c90fa7 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -82,6 +82,11 @@
t.index ["person_id"], name: "index_authentication_methods_on_person_id"
end
+ create_table "content_blocks", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
create_table "friendly_id_slugs", force: :cascade do |t|
t.string "slug", null: false
t.integer "sluggable_id", null: false
@@ -311,6 +316,17 @@
t.index ["space_id"], name: "index_rooms_on_space_id"
end
+ create_table "slots", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.uuid "section_id"
+ t.string "slottable_type"
+ t.uuid "slottable_id"
+ t.integer "slot_order"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["section_id"], name: "index_slots_on_section_id"
+ t.index ["slottable_type", "slottable_id"], name: "index_slots_on_slottable"
+ end
+
create_table "space_agreements", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.uuid "space_id"
t.string "name", null: false
@@ -373,6 +389,7 @@
add_foreign_key "marketplace_vendor_representatives", "people"
add_foreign_key "memberships", "invitations"
add_foreign_key "rooms", "media", column: "hero_image_id"
+ add_foreign_key "slots", "rooms", column: "section_id"
add_foreign_key "space_agreements", "spaces"
add_foreign_key "spaces", "rooms", column: "entrance_id"
end
diff --git a/db/seeds.rb b/db/seeds.rb
index 381b112b3..b4921354f 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -50,3 +50,8 @@
)
journal = FactoryBot.create(:journal, room: journal_section)
FactoryBot.create_list(:journal_entry, 7, :with_keywords, :published, journal:)
+
+_content_block_section = FactoryBot.create(:room, space:, name: "Content Block-o-Clock",
+ description: "Content Blocks show static Words, Photos, or Videos!")
+
+# FactoryBot.create(:content_block, in_section: content_block_section)
diff --git a/spec/models/room_spec.rb b/spec/models/room_spec.rb
index 1db962ace..38a1746fe 100644
--- a/spec/models/room_spec.rb
+++ b/spec/models/room_spec.rb
@@ -4,6 +4,8 @@
let(:space) { Space.new }
it { is_expected.to have_many(:gizmos).inverse_of(:room).dependent(:destroy) }
+ it { is_expected.to have_many(:slots).inverse_of(:section).dependent(:destroy) }
+
it { is_expected.to belong_to(:space).inverse_of(:rooms) }
describe "#description" do
diff --git a/spec/models/slot_spec.rb b/spec/models/slot_spec.rb
new file mode 100644
index 000000000..cd0b5fd8c
--- /dev/null
+++ b/spec/models/slot_spec.rb
@@ -0,0 +1,6 @@
+require "rails_helper"
+
+RSpec.describe Slot do
+ it { is_expected.to belong_to(:section).class_name(:Room).inverse_of(:slots) }
+ it { is_expected.to belong_to(:slottable).inverse_of(:slot) }
+end
diff --git a/spec/system/slots_system_spec.rb b/spec/system/slots_system_spec.rb
new file mode 100644
index 000000000..6fad5a881
--- /dev/null
+++ b/spec/system/slots_system_spec.rb
@@ -0,0 +1,23 @@
+require "rails_helper"
+
+RSpec.describe "Slots" do
+ include ActionText::SystemTestHelper
+
+ let(:space) { create(:space, :with_members) }
+ let(:section) { create(:room, space:) }
+
+ scenario "Adding a Content Block to a Slot" do
+ sign_in(space.members.first, space)
+ visit(polymorphic_path(section.location(:edit)))
+
+ click_link("Add Content Block")
+ expect(page).to have_current_path(polymorphic_path(section.location(:new, child: :text_block)))
+
+ fill_in_rich_text_area("Body", with: "Prepare yourself for AMAZING")
+ click_button("Create")
+
+ expect(section.slots.count).to eq(1)
+ expect(section.slots.first.slottable).to be_a(ContentBlock)
+ expect(sections.slots.first.slottable.body).to eq("Prepare yourself for AMAZING")
+ end
+end