image admin

This commit is contained in:
2026-05-05 22:53:55 +02:00
parent 05b8b151cb
commit 4aaed0607c
22 changed files with 222 additions and 53 deletions
+47
View File
@@ -0,0 +1,47 @@
ActiveAdmin.register Image do
permit_params :file_upload
menu priority: 5
index do
selectable_column
id_column
column :original_filename
column :content_type
column(:preview) do |img|
if File.exist?(img.file_path)
image_tag("/api/image/#{img.id}", style: "max-height:60px;max-width:120px;object-fit:contain;")
end
end
column :created_at
actions
end
filter :original_filename
filter :content_type
show do
attributes_table do
row :id
row :original_filename
row :filename
row :content_type
row(:preview) do |img|
if File.exist?(img.file_path)
image_tag("/api/image/#{img.id}", style: "max-height:300px;max-width:100%;object-fit:contain;")
else
"File not found on disk"
end
end
row :created_at
row :updated_at
end
end
form html: { multipart: true } do |f|
f.inputs do
f.input :file_upload, as: :file, label: "Image File"
end
f.actions
end
end
+30 -12
View File
@@ -1,14 +1,24 @@
ActiveAdmin.register Member do
permit_params :nick, :real_nick, :motto, :avatar_filename
permit_params :nick, :real_nick, :motto, :avatar_filename, :image_id
menu priority: 4
index do
selectable_column
id_column
column :nick
column :nick do |m|
link_to m.nick, edit_admin_member_path(m)
end
column :real_nick
column :motto do |m|
truncate m.motto, length: 60
end
column :avatar_filename
column(:image) do |m|
if m.image && File.exist?(m.image.file_path)
image_tag "/api/image/#{m.image.id}", style: "max-height:40px;max-width:80px;object-fit:contain;"
end
end
column :created_at
actions
end
@@ -16,15 +26,19 @@ ActiveAdmin.register Member do
filter :nick
filter :real_nick
show do
attributes_table do
row :id
row :nick
row :real_nick
row :motto
row :avatar_filename
row :created_at
row :updated_at
show title: proc { |m| m.nick } do
active_admin_form_for [:admin, resource], url: admin_member_path(resource), html: { method: :put } do |f|
f.inputs "Edit Member" do
f.input :nick
f.input :real_nick
f.input :motto
f.input :avatar_filename,
hint: "Fallback image filename (e.g. mr.zero.png) — only used when no image is selected above."
f.input :image, as: :select,
collection: Image.order(:original_filename).map { |img| [img.original_filename, img.id] },
include_blank: "— no image (use avatar_filename) —"
end
f.actions
end
end
@@ -33,7 +47,11 @@ ActiveAdmin.register Member do
f.input :nick
f.input :real_nick
f.input :motto
f.input :avatar_filename
f.input :avatar_filename,
hint: "Fallback image filename (e.g. mr.zero.png) — only used when no image is selected above."
f.input :image, as: :select,
collection: Image.order(:original_filename).map { |img| [img.original_filename, img.id] },
include_blank: "— no image (use avatar_filename) —"
end
f.actions
end
+34 -30
View File
@@ -1,6 +1,6 @@
ActiveAdmin.register Software do
permit_params :name, :title, :author, :desc, :story,
:license, :platform, :status, :highlighted,
:license, :platform, :status, :highlighted, :image_id,
external_links_attributes: [:id, :label, :url, :_destroy],
releases_attributes: [:id, :_destroy]
@@ -9,13 +9,20 @@ ActiveAdmin.register Software do
index do
selectable_column
id_column
column :name
column :name do |sw|
link_to sw.name, admin_software_path(sw)
end
column :title
column :platform
column :status do |sw|
status_tag sw.status
end
column :highlighted
column(:image) do |sw|
if sw.image && File.exist?(sw.image.file_path)
image_tag "/api/image/#{sw.image.id}", style: "max-height:40px;max-width:80px;object-fit:contain;"
end
end
column :created_at
actions
end
@@ -27,37 +34,31 @@ ActiveAdmin.register Software do
filter :status, as: :select, collection: %w[development demo released archived]
filter :highlighted
show do
attributes_table do
row :id
row :name
row :title
row :author
row :platform
row :status
row :highlighted
row :license
row(:desc) { |sw| simple_format sw.desc }
row(:story) { |sw| simple_format sw.story }
row :created_at
row :updated_at
end
show title: proc { |sw| sw.title } do
active_admin_form_for [:admin, resource], url: admin_software_path(resource), html: { method: :put } do |f|
f.inputs "Details" do
f.input :name
f.input :title
f.input :author
f.input :platform, as: :select, collection: %w[tic80 ebitengine love]
f.input :status, as: :select, collection: %w[development demo released archived]
f.input :highlighted
f.input :license
f.input :image, as: :select,
collection: Image.order(:original_filename).map { |img| [img.original_filename, img.id] },
include_blank: "— no image —"
f.input :desc, as: :text, input_html: { rows: 4 }
f.input :story, as: :text, input_html: { rows: 8 }
end
panel "External Links" do
table_for resource.external_links do
column :label
column(:url) { |el| link_to el.url, el.url, target: "_blank" }
column :created_at
column do |el|
link_to "Edit", edit_admin_software_external_link_path(resource, el)
concat " "
link_to "Delete", admin_software_external_link_path(resource, el),
method: :delete, data: { confirm: "Are you sure?" }
f.inputs "External Links" do
f.has_many :external_links, allow_destroy: true, new_record: true do |el|
el.input :label
el.input :url
end
end
br
link_to "Add External Link",
new_admin_software_external_link_path(resource)
f.actions
end
panel "Releases" do
@@ -81,6 +82,9 @@ ActiveAdmin.register Software do
f.input :status, as: :select, collection: %w[development demo released archived]
f.input :highlighted
f.input :license
f.input :image, as: :select,
collection: Image.order(:original_filename).map { |img| [img.original_filename, img.id] },
include_blank: "— no image —"
f.input :desc, as: :text, input_html: { rows: 4 }
f.input :story, as: :text, input_html: { rows: 8 }
end
@@ -0,0 +1,14 @@
class Api::ImagesController < ApplicationController
skip_before_action :verify_authenticity_token
def show
image = Image.find(params[:id])
path = image.file_path
return render plain: "Not Found", status: :not_found unless File.exist?(path)
send_file path, type: image.content_type, disposition: "inline"
rescue ActiveRecord::RecordNotFound
render plain: "Not Found", status: :not_found
end
end
@@ -1,7 +1,13 @@
class Api::MembersController < ApiController
def index
members = Member.order(:id).map do |m|
{ nick: m.nick, real_nick: m.real_nick, motto: m.motto, avatar_filename: m.avatar_filename }
members = Member.includes(:image).order(:id).map do |m|
{
nick: m.nick,
real_nick: m.real_nick,
motto: m.motto,
avatar_filename: m.avatar_filename,
image_url: m.image_id ? "/api/image/#{m.image_id}" : nil
}
end
render json: members
end
+26
View File
@@ -0,0 +1,26 @@
class Image < ApplicationRecord
UPLOAD_PATH = ENV.fetch("IMAGE_CONTAINER_PATH", "/images")
attr_accessor :file_upload
before_save :process_upload, if: -> { file_upload.present? }
def self.ransackable_attributes(auth_object = nil)
%w[content_type created_at filename id original_filename updated_at]
end
def file_path
File.join(UPLOAD_PATH, filename.to_s)
end
private
def process_upload
FileUtils.mkdir_p(UPLOAD_PATH)
self.original_filename = file_upload.original_filename
self.content_type = file_upload.content_type.presence || "application/octet-stream"
ext = File.extname(file_upload.original_filename)
self.filename = "#{SecureRandom.uuid}#{ext}"
IO.copy_stream(file_upload.to_io, file_path)
end
end
+7 -1
View File
@@ -1,5 +1,11 @@
class Member < ApplicationRecord
belongs_to :image, optional: true
def self.ransackable_attributes(auth_object = nil)
%w[avatar_filename created_at id motto nick real_nick updated_at]
%w[avatar_filename created_at id image_id motto nick real_nick updated_at]
end
def self.ransackable_associations(auth_object = nil)
%w[image]
end
end
+4 -2
View File
@@ -1,6 +1,8 @@
class Software < ApplicationRecord
self.table_name = "softwares"
belongs_to :image, optional: true
has_many :releases, foreign_key: :software_id
has_many :external_links, foreign_key: :software_id
@@ -9,10 +11,10 @@ class Software < ApplicationRecord
default_scope { where(deleted_at: nil) }
def self.ransackable_attributes(auth_object = nil)
%w[author created_at desc highlighted id license name platform site status story title updated_at]
%w[author created_at desc highlighted id image_id license name platform site status story title updated_at]
end
def self.ransackable_associations(auth_object = nil)
%w[releases external_links]
%w[releases external_links image]
end
end
@@ -1,6 +1,6 @@
class SoftwareHighlightedService
def call
software = Software.includes(:external_links)
software = Software.includes(:external_links, :image)
.where(highlighted: true)
.order(id: :desc)
.first
@@ -1,6 +1,6 @@
class SoftwareListService
def call
softwares = Software.includes(:releases, :external_links).all
softwares = Software.includes(:releases, :external_links, :image).all
items = softwares.map do |sw|
SoftwareSerializer.show_data(sw, sw.releases.to_a)
end
+2 -1
View File
@@ -32,7 +32,8 @@ class SoftwareSerializer
"platform" => sw.platform,
"status" => sw.status,
"highlighted" => sw.highlighted ? true : false,
"externalLinks" => sw.external_links.map { |el| serialize_external_link(el) }
"externalLinks" => sw.external_links.map { |el| serialize_external_link(el) },
"imageUrl" => sw.image_id ? "/api/image/#{sw.image_id}" : nil
}
end
+1
View File
@@ -7,6 +7,7 @@ Rails.application.routes.draw do
get "software/highlighted", to: "software_highlighted#index"
get "events", to: "events#index"
get "members", to: "members#index"
get "image/:id", to: "images#show"
end
get "update", to: "update#update"
@@ -0,0 +1,11 @@
class CreateImages < ActiveRecord::Migration[7.1]
def change
create_table :images do |t|
t.string :filename, null: false
t.string :original_filename, null: false
t.string :content_type, null: false, default: "application/octet-stream"
t.timestamps
end
end
end
@@ -0,0 +1,5 @@
class AddImageIdToSoftwares < ActiveRecord::Migration[7.1]
def change
add_reference :softwares, :image, null: true, foreign_key: true
end
end
@@ -0,0 +1,5 @@
class AddImageIdToMembers < ActiveRecord::Migration[7.1]
def change
add_reference :members, :image, null: true, foreign_key: true
end
end