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.
change
commit 5ed8df76de65ee45740f51b7087effa2d238e0c3
author Claude <noreply@anthropic.com>
date
parent tkkmyxxp
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)