Allow attaching assets when saving a photo
When saving a photo, users can optionally select one or more assets
(from the capture form) to automatically link the uploaded file as an
attachment. Introduces the assets and asset_attachments tables, wires
up the multi-select UI in the capture view, and handles the join-table
inserts in a single transaction alongside the file write.
diff --git a/db/migrate/003_create_assets.rb b/db/migrate/003_create_assets.rb
new file mode 100644
index 0000000..a153001
--- /dev/null
+++ b/db/migrate/003_create_assets.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+Sequel.migration do
+ change do
+ create_table(:assets) do
+ primary_key :id
+ String :name, null: false
+ DateTime :created_at, null: false
+ end
+ end
+end
diff --git a/db/migrate/004_create_asset_attachments.rb b/db/migrate/004_create_asset_attachments.rb
new file mode 100644
index 0000000..a9914cf
--- /dev/null
+++ b/db/migrate/004_create_asset_attachments.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+Sequel.migration do
+ change do
+ create_table(:asset_attachments) do
+ primary_key :id
+ foreign_key :asset_id, :assets, null: false
+ foreign_key :file_id, :files, null: false
+ DateTime :created_at, null: false
+ end
+ end
+end
diff --git a/lib/views/capture.rb b/lib/views/capture.rb
index 8b1645b..a1506ad 100644
--- a/lib/views/capture.rb
+++ b/lib/views/capture.rb
@@ -7,6 +7,10 @@ module Domus
class Capture < Phlex::HTML
ICONS_DIR = File.expand_path("../../public/icons", __dir__)
+ def initialize(assets: [])
+ @assets = assets
+ end
+
def view_template
doctype
html(lang: "en") do
@@ -119,6 +123,18 @@ module Domus
end
div(class: "save-form") do
+ unless @assets.empty?
+ div(class: "asset-list") do
+ p(class: "asset-list-label") { plain "Add to assets (optional)" }
+ @assets.each do |asset|
+ label(class: "asset-item") do
+ input(type: "checkbox", name: "asset_ids[]", value: asset[:id])
+ plain asset[:name]
+ end
+ end
+ end
+ end
+
div(class: "btn-row") do
button(type: "submit", class: "btn btn-primary") do
icon("check")
diff --git a/lib/web.rb b/lib/web.rb
index 2e5b587..ad1a1a9 100644
--- a/lib/web.rb
+++ b/lib/web.rb
@@ -35,7 +35,8 @@ module Domus
r.root do
r.get do
- Views::Capture.new.call
+ assets = db[:assets].order(:name).all
+ Views::Capture.new(assets:).call
end
end
@@ -65,11 +66,20 @@ module Domus
raise ClientError, "That image format isn't supported." unless IMAGE_EXTENSIONS.include?(ext)
raise ClientError, "That image is too large (25 MB max)." if upload[:tempfile].size > MAX_UPLOAD_BYTES
- id = db[:files].insert(extension: ext, created_at: Time.now)
+ asset_ids = Array(params["asset_ids"]).flatten.map(&:to_i).reject(&:zero?)
- dest = app.file_path(id: id, extension: ext)
- FileUtils.mkdir_p(::File.dirname(dest))
- FileUtils.cp(upload[:tempfile].path, dest)
+ db.transaction do
+ file_id = db[:files].insert(extension: ext, created_at: Time.now)
+
+ dest = app.file_path(id: file_id, extension: ext)
+ FileUtils.mkdir_p(::File.dirname(dest))
+ FileUtils.cp(upload[:tempfile].path, dest)
+
+ now = Time.now
+ asset_ids.each do |asset_id|
+ db[:asset_attachments].insert(asset_id: asset_id, file_id: file_id, created_at: now)
+ end
+ end
end
end
end
diff --git a/test/test_app.rb b/test/test_app.rb
index deabe64..14b9873 100644
--- a/test/test_app.rb
+++ b/test/test_app.rb
@@ -14,6 +14,8 @@ class TestApp < Minitest::Test
def domus = Domus::Web.opts.fetch(:app)
def setup
+ domus.db[:asset_attachments].delete
+ domus.db[:assets].delete
domus.db[:files].delete
end
@@ -63,6 +65,46 @@ class TestApp < Minitest::Test
assert_equal 0, domus.db[:files].count
end
+ def test_root_shows_assets
+ domus.db[:assets].insert(name: "Camera", created_at: Time.now)
+
+ get "/"
+
+ assert_equal 200, last_response.status
+ assert_includes last_response.body, "Camera"
+ end
+
+ def test_upload_with_asset_ids_creates_attachments
+ asset_id = domus.db[:assets].insert(name: "Laptop", created_at: Time.now)
+
+ post "/files", "file" => upload("photo.png", "image/png", "bytes"), "asset_ids[]" => asset_id.to_s
+
+ assert_equal 302, last_response.status
+ file = domus.db[:files].first
+ attachment = domus.db[:asset_attachments].first
+ refute_nil attachment
+ assert_equal asset_id, attachment[:asset_id]
+ assert_equal file[:id], attachment[:file_id]
+ end
+
+ def test_upload_with_multiple_asset_ids_creates_all_attachments
+ id1 = domus.db[:assets].insert(name: "Camera", created_at: Time.now)
+ id2 = domus.db[:assets].insert(name: "Laptop", created_at: Time.now)
+
+ post "/files", "file" => upload("photo.png", "image/png", "bytes"),
+ "asset_ids[]" => [id1.to_s, id2.to_s]
+
+ assert_equal 302, last_response.status
+ assert_equal 2, domus.db[:asset_attachments].count
+ end
+
+ def test_upload_without_asset_ids_creates_no_attachments
+ post "/files", "file" => upload("photo.png", "image/png", "bytes")
+
+ assert_equal 302, last_response.status
+ assert_equal 0, domus.db[:asset_attachments].count
+ end
+
private
def upload(filename, type, contents)