From c06723ee715512c2033c7786c48f15674585b56b Mon Sep 17 00:00:00 2001 From: erdgeist Date: Fri, 26 Jun 2026 01:59:57 +0200 Subject: Stage 4: Rails 5.2 -> 6.1 on Ruby 2.7.2 - routing-filter 0.6.3 -> 0.7.0 (Rails 6.1 compatibility) - RSS named routes rss_xml/rss_rdf added - RouteWithParams workarounds: will_paginate_patch, content_path shim, safe_path helper - Paperclip removed, replaced with FileAttachment concern (preserves URL scheme) - Assets resource moved to /admin/assets (Sprockets middleware conflict) - ApplicationRecord base class added, all models migrated - Strong parameters added to Assets, Occurrences, Events, MenuItems controllers - update_attributes -> update throughout - render :nothing -> head :ok/:not_found throughout - language_selector rewritten (removes :overwrite_params) - Environment files updated for Rails 6.1 (eager_load, public_file_server, ActionMailer) - Arel::Visitors::DepthFirst and Integer/Float duration patches removed from test_helper - AssetsController tests added (10 tests covering upload, variants, destroy, auth) - ImageMagick geometry: 460x250! for headline crop (not # which is invalid in IM6) 129 runs, 311 assertions, 5 failures (all pre-existing), 0 errors --- app/models/application_record.rb | 3 + app/models/asset.rb | 17 ++--- app/models/concerns/file_attachment.rb | 124 +++++++++++++++++++++++++++++++++ app/models/event.rb | 2 +- app/models/menu_item.rb | 2 +- app/models/node.rb | 2 +- app/models/occurrence.rb | 2 +- app/models/page.rb | 2 +- app/models/permission.rb | 2 +- app/models/related_asset.rb | 2 +- app/models/user.rb | 2 +- 11 files changed, 139 insertions(+), 21 deletions(-) create mode 100644 app/models/application_record.rb create mode 100644 app/models/concerns/file_attachment.rb (limited to 'app/models') diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 00000000..10a4cba8 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/app/models/asset.rb b/app/models/asset.rb index f6526f2c..aca0ee80 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -1,20 +1,11 @@ -class Asset < ActiveRecord::Base +class Asset < ApplicationRecord + + include FileAttachment has_many :related_assets, :dependent => :destroy has_many :pages, :through => :related_assets - has_attached_file( - :upload, - :path => ":rails_root/public/system/:attachment/:id/:style/:filename", - :url => "/system/:attachment/:id/:style/:filename", - :styles => { - :medium => "300x300", - :thumb => "100x100", - :headline => "460x250#" - } - ) - - scope :images, -> { where(:upload_content_type => ["image/gif", "image/jpeg", "image/png"]) } + scope :images, -> { where(:upload_content_type => ["image/gif", "image/jpeg", "image/png", "image/webp"]) } scope :documents, -> { where(:upload_content_type => ["application/pdf", "text/plain", "text/rtf"]) } scope :audio, -> { where(:upload_content_type => ["audio/mpeg", "audio/x-m4a", "audio/wav", "audio/x-wav"]) } diff --git a/app/models/concerns/file_attachment.rb b/app/models/concerns/file_attachment.rb new file mode 100644 index 00000000..b3ff0f14 --- /dev/null +++ b/app/models/concerns/file_attachment.rb @@ -0,0 +1,124 @@ +# FileAttachment — minimal drop-in replacement for Paperclip's has_attached_file. +# +# Provides the same interface used throughout this codebase: +# asset.upload.url -> "/system/uploads/:id/original/:filename" +# asset.upload.url(:thumb) -> "/system/uploads/:id/thumb/:filename" +# asset.upload.content_type -> string +# asset.upload.size -> integer (bytes) +# +# Files are stored at: +# Rails.root/public/system/uploads/:id/:style/:filename +# +# Image variants are generated via ImageMagick (convert) on upload. +# Non-image files get only an original, no variants. +# +# To replace an asset: assign a new file to asset.upload= and save. +# The filename is fixed on first upload and preserved on replacement, +# keeping all public URLs stable. +# +# Future: if more sophisticated asset management is needed (versioning, +# S3, on-demand resizing), replace this module and keep the interface. + +module FileAttachment + extend ActiveSupport::Concern + + STYLES = { + medium: { geometry: "300x300>", format: nil }, + thumb: { geometry: "100x100>", format: nil }, + headline: { geometry: "460x250!", format: nil } + }.freeze + + IMAGE_CONTENT_TYPES = %w[image/jpeg image/gif image/png image/webp].freeze + + included do + attr_reader :upload + + after_initialize :build_upload_proxy + after_save :process_upload + before_destroy :delete_upload_files + end + + def upload=(uploaded_file) + return if uploaded_file.blank? + @pending_upload = uploaded_file + # Populate the database columns immediately so validations can use them + self.upload_file_name = sanitize_filename(uploaded_file.original_filename) + self.upload_content_type = uploaded_file.content_type.to_s.split(';').first.strip + self.upload_file_size = uploaded_file.size + self.upload_updated_at = Time.current + build_upload_proxy + end + + private + + def build_upload_proxy + @upload = UploadProxy.new(self) + end + + def process_upload + return unless @pending_upload + uploaded_file = @pending_upload + @pending_upload = nil + + original_path = file_path(:original) + FileUtils.mkdir_p(File.dirname(original_path)) + FileUtils.cp(uploaded_file.tempfile.path, original_path) + + if IMAGE_CONTENT_TYPES.include?(upload_content_type) + generate_variants(original_path) + end + end + + def generate_variants(original_path) + STYLES.each do |style, options| + dest_path = file_path(style) + FileUtils.mkdir_p(File.dirname(dest_path)) + system("convert", original_path, "-resize", options[:geometry], dest_path) + end + end + + def delete_upload_files + dir = Rails.root.join("public", "system", "uploads", id.to_s) + FileUtils.rm_rf(dir) if Dir.exist?(dir) + end + + def file_path(style) + Rails.root.join( + "public", "system", "uploads", + id.to_s, style.to_s, upload_file_name + ).to_s + end + + def sanitize_filename(filename) + File.basename(filename).gsub(/[^\w\.\-]/, '_') + end + + # Proxy object returned by asset.upload, providing the Paperclip-compatible + # interface used in views: .url, .url(:style), .content_type, .size + class UploadProxy + def initialize(record) + @record = record + end + + def url(style = :original) + return "" if @record.upload_file_name.blank? + "/system/uploads/#{@record.id}/#{style}/#{@record.upload_file_name}" + end + + def content_type + @record.upload_content_type.to_s + end + + def size + @record.upload_file_size.to_i + end + + def present? + @record.upload_file_name.present? + end + + def blank? + !present? + end + end +end diff --git a/app/models/event.rb b/app/models/event.rb index 23deed69..94a22e36 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,4 +1,4 @@ -class Event < ActiveRecord::Base +class Event < ApplicationRecord # Associations diff --git a/app/models/menu_item.rb b/app/models/menu_item.rb index eb823473..7769b7fa 100644 --- a/app/models/menu_item.rb +++ b/app/models/menu_item.rb @@ -1,4 +1,4 @@ -class MenuItem < ActiveRecord::Base +class MenuItem < ApplicationRecord default_scope -> { where(:type => "MenuItem") } diff --git a/app/models/node.rb b/app/models/node.rb index d760f0a4..f7a70d00 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -1,4 +1,4 @@ -class Node < ActiveRecord::Base +class Node < ApplicationRecord # Mixins and Plugins acts_as_nested_set diff --git a/app/models/occurrence.rb b/app/models/occurrence.rb index 8457ffdc..3baf4474 100644 --- a/app/models/occurrence.rb +++ b/app/models/occurrence.rb @@ -1,7 +1,7 @@ # TODO Make a gem out of the c wrapper require 'chaos_calendar' -class Occurrence < ActiveRecord::Base +class Occurrence < ApplicationRecord # Associations diff --git a/app/models/page.rb b/app/models/page.rb index 93debf82..d1e74395 100644 --- a/app/models/page.rb +++ b/app/models/page.rb @@ -1,6 +1,6 @@ require 'xml' -class Page < ActiveRecord::Base +class Page < ApplicationRecord PUBLIC_TEMPLATE_PATH = File.join(%w(custom page_templates public)) FULL_PUBLIC_TEMPLATE_PATH = Rails.root.join('app', 'views', PUBLIC_TEMPLATE_PATH) diff --git a/app/models/permission.rb b/app/models/permission.rb index f304538c..1383a4b8 100644 --- a/app/models/permission.rb +++ b/app/models/permission.rb @@ -1,4 +1,4 @@ -class Permission < ActiveRecord::Base +class Permission < ApplicationRecord # Validations validates_presence_of :user_id, :node_id, :granted validates_inclusion_of :granted, :in => [true, false] diff --git a/app/models/related_asset.rb b/app/models/related_asset.rb index 2b61c513..8f164600 100644 --- a/app/models/related_asset.rb +++ b/app/models/related_asset.rb @@ -1,4 +1,4 @@ -class RelatedAsset < ActiveRecord::Base +class RelatedAsset < ApplicationRecord belongs_to :page belongs_to :asset diff --git a/app/models/user.rb b/app/models/user.rb index a2540b5c..92ac33aa 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,6 +1,6 @@ require 'digest/sha1' -class User < ActiveRecord::Base +class User < ApplicationRecord # Mixins and Plugins include Authentication include Authentication::ByPassword -- cgit v1.3