Replace Paperclippable ContentConfig

The old Paperclip configuration was very clever and easy to use but it
was also a complicated implementation building on the complicated Spree
preference system.

I simplified this with Active Storage, storing simple references to blob
ids and default URLs as backup.
This commit is contained in:
Maikel Linke
2022-04-06 14:48:06 +10:00
parent ddd9ae6ce2
commit 727eef3c4f
15 changed files with 86 additions and 203 deletions

View File

@@ -10,14 +10,14 @@ module Admin
def update
params.each do |name, value|
if ContentConfig.has_preference?(name) || ContentConfig.has_attachment?(name)
ContentConfig.public_send("#{name}=", value)
if value.is_a?(ActionDispatch::Http::UploadedFile)
blob = store_file(value)
update_preference("#{name}_blob_id", blob.id)
else
update_preference(name, value)
end
end
# Save any uploaded images
ContentConfig.save
flash[:success] =
t(:successfully_updated, resource: I18n.t('admin.contents.edit.your_content'))
@@ -26,6 +26,22 @@ module Admin
private
def store_file(attachable)
ActiveStorage::Blob.create_and_upload!(
io: attachable.open,
filename: attachable.original_filename,
content_type: attachable.content_type,
service_name: :local,
identify: false,
)
end
def update_preference(name, value)
return unless ContentConfig.has_preference?(name)
ContentConfig.public_send("#{name}=", value)
end
def preference_sections
[
PreferenceSections::HeaderSection.new,

View File

@@ -0,0 +1,43 @@
# frozen_string_literal: true
module FilePreferences
extend ActiveSupport::Concern
included do
@default_urls = {}
end
class_methods do
def file_preference(name, default_url: nil)
preference "#{name}_blob_id", :integer
@default_urls[name] = default_url if default_url
end
def default_url(name)
@default_urls[name]
end
end
def preference_type(key)
if has_preference?("#{key}_blob_id")
:file
else
super(key)
end
end
def url_for(name)
blob = blob_for(name)
if blob
Rails.application.routes.url_helpers.url_for(blob)
else
self.class.default_url(name)
end
end
def blob_for(name)
blob_id = get_preference("#{name}_blob_id")
ActiveStorage::Blob.find_by(id: blob_id) if blob_id
end
end

View File

@@ -1,23 +1,17 @@
# frozen_string_literal: true
require 'open_food_network/paperclippable'
class ContentConfiguration < Spree::Preferences::FileConfiguration
include OpenFoodNetwork::Paperclippable
class ContentConfiguration < Spree::Preferences::Configuration
include FilePreferences
# Header
preference :logo, :file
preference :logo_mobile, :file
preference :logo_mobile_svg, :file
has_attached_file :logo, default_url: "/default_images/ofn-logo.png"
has_attached_file :logo_mobile
has_attached_file :logo_mobile_svg, default_url: "/default_images/ofn-logo-mobile.svg"
file_preference :logo, default_url: "/default_images/ofn-logo.png"
file_preference :logo_mobile
file_preference :logo_mobile_svg, default_url: "/default_images/ofn-logo-mobile.svg"
# Home page
preference :home_page_alert_html, :text
preference :home_hero, :file
file_preference :home_hero, default_url: "/default_images/home.jpg"
preference :home_show_stats, :boolean, default: true
has_attached_file :home_hero, default_url: "/default_images/home.jpg"
# Map
preference :open_street_map_enabled, :boolean, default: false
@@ -66,8 +60,7 @@ class ContentConfiguration < Spree::Preferences::FileConfiguration
preference :menu_7_icon_name, :string, default: "ofn-i_013-help"
# Footer
preference :footer_logo, :file
has_attached_file :footer_logo, default_url: "/default_images/ofn-logo-footer.png"
file_preference :footer_logo, default_url: "/default_images/ofn-logo-footer.png"
# Other
preference :footer_facebook_url, :string, default: "https://www.facebook.com/OpenFoodNet"

View File

@@ -1,62 +0,0 @@
# frozen_string_literal: true
module Spree
module Preferences
class FileConfiguration < Configuration
def self.preference(name, type, *args)
if type == :file
# Active Storage blob id:
super "#{name}_blob_id", :integer, *args
# Paperclip attachment attributes:
super "#{name}_file_name", :string, *args
super "#{name}_content_type", :string, *args
super "#{name}_file_size", :integer, *args
super "#{name}_updated_at", :string, *args
else
super name, type, *args
end
end
def get_preference(key)
if !has_preference?(key) && has_attachment?(key)
# Call Paperclip's attachment method:
public_send key
elsif key.ends_with?("_blob")
# Find referenced Active Storage blob:
blob_id = super("#{key}_id")
ActiveStorage::Blob.find_by(id: blob_id)
else
super key
end
end
alias :[] :get_preference
def preference_type(name)
if has_attachment? name
:file
else
super name
end
end
# Spree's Configuration responds to preference methods via method_missing, but doesn't
# override respond_to?, which consequently reports those methods as unavailable. Paperclip
# errors if respond_to? isn't correct, so we override it here.
def respond_to?(method, include_all = false)
name = method.to_s.delete('=')
reference_name = "#{name}_id"
super(self.class.preference_getter_method(name), include_all) ||
super(reference_name, include_all) ||
super(method, include_all)
end
def has_attachment?(name)
self.class.respond_to?(:attachment_definitions) &&
self.class.attachment_definitions.key?(name.to_sym)
end
end
end
end

View File

@@ -8,5 +8,8 @@
- text = t(key)
.field
= label_tag(key, text + ': ') + tag(:br) if type != :boolean
= preference_field_tag(key, ContentConfig[key], :type => type)
- if type == :file
= file_field_tag(key, type: type)
- else
= preference_field_tag(key, ContentConfig[key], type: type)
= label_tag(key, text) + tag(:br) if type == :boolean

View File

@@ -1,5 +1,5 @@
:css
#tagline:before { background-image: url("#{ContentConfig.home_hero.url}") }
#tagline:before { background-image: url("#{ContentConfig.url_for(:home_hero)}") }
- content_for :page_alert do

View File

@@ -4,7 +4,7 @@
%meta{name: 'viewport', content: "width=device-width,initial-scale=1.0"}/
%meta{property: "og:title", content: content_for?(:title) ? yield(:title) : t(:title)}
%meta{property: "og:description", content: content_for?(:description) ? yield(:description) : t(:site_meta_description)}
%meta{property: "og:image", content: content_for?(:image) ? yield(:image) : ContentConfig.logo.url}
%meta{property: "og:image", content: content_for?(:image) ? yield(:image) : ContentConfig.url_for(:logo)}
- if !Rails.env.production? || @noindex_meta_tag
%meta{name: "robots", content: "noindex"}
%title= content_for?(:title) ? "#{yield(:title)} - #{t(:title)}".html_safe : "#{t(:welcome_to)} #{t(:title)}"

View File

@@ -15,7 +15,7 @@
%table{:bgcolor => "#f2f2f2"}
%tr
%td
%img{src: ContentConfig.footer_logo.url, width: "144", height: "50"}/
%img{src: ContentConfig.url_for(:footer_logo), width: "144", height: "50"}/
%td{:align => "right"}
%h6.collapse
= Spree::Config[:site_name]

View File

@@ -96,7 +96,7 @@
.row.legal
.small-12.medium-3.medium-offset-2.columns.text-left
%a{href: main_app.root_path}
%img{src: ContentConfig.footer_logo.url, width: "220"}
%img{src: ContentConfig.url_for(:footer_logo), width: "220"}
.small-12.medium-5.columns.text-left
%p.text-small
= t '.footer_legal_call'

View File

@@ -3,7 +3,7 @@
%ul.nav-logo
%li.ofn-logo
%a{href: main_app.root_path}
%img{src: ContentConfig.logo.url}
%img{src: ContentConfig.url_for(:logo)}
%li.powered-by
%img{src: '/favicon.ico'}
%span

View File

@@ -6,7 +6,7 @@
%section.left
.ofn-logo
%a{href: main_app.root_path}
%img{src: ContentConfig.logo_mobile.url, srcset: ContentConfig.logo_mobile_svg.url, width: "75", height: "26"}
%img{src: ContentConfig.url_for(:logo_mobile), srcset: ContentConfig.url_for(:logo_mobile_svg), width: "75", height: "26"}
%section.right{"ng-cloak" => true}
%span.cart-span{"ng-class" => "{ dirty: Cart.dirty || Cart.empty(), 'pure-dirty': Cart.dirty }"}

View File

@@ -2,7 +2,7 @@
%ul.off-canvas-list
%li.ofn-logo
%a{href: main_app.root_path}
%img{src: ContentConfig.logo_mobile.url, srcset: ContentConfig.logo_mobile_svg.url, width: "75", height: "26"}
%img{src: ContentConfig.url_for(:logo_mobile), srcset: ContentConfig.url_for(:logo_mobile_svg), width: "75", height: "26"}
- [*1..7].each do |menu_number|
- menu_name = "menu_#{menu_number}"
- if ContentConfig[menu_name].present?

View File

@@ -1,51 +0,0 @@
# frozen_string_literal: true
# Allow use of Paperclip's has_attached_file on non-ActiveRecord classes
# https://gist.github.com/basgys/5712426
module OpenFoodNetwork
module Paperclippable
def self.included(base)
base.extend(ActiveModel::Naming)
base.extend(ActiveModel::Callbacks)
base.include(ActiveModel::Validations)
base.include(Paperclip::Glue)
# Paperclip required callbacks
base.define_model_callbacks(:save, only: [:after])
base.define_model_callbacks(:commit, only: [:after])
base.define_model_callbacks(:destroy, only: [:before, :after])
# Initialise an ID
base.__send__(:attr_accessor, :id)
base.instance_variable_set :@id, 1
end
# ActiveModel requirements
def to_model
self
end
def valid?() true end
def new_record?() true end
def destroyed?() true end
def save
run_callbacks :save do
end
true
end
def errors
obj = Object.new
def obj.[](_key) [] end
def obj.full_messages() [] end
def obj.any?() false end
obj
end
end
end

View File

@@ -5,10 +5,10 @@ require 'spec_helper'
describe ContentConfiguration do
describe "default logos and home_hero" do
it "sets a default url with existing image" do
expect(image_exist?(ContentConfig.logo.options[:default_url])).to be true
expect(image_exist?(ContentConfig.logo_mobile_svg.options[:default_url])).to be true
expect(image_exist?(ContentConfig.home_hero.options[:default_url])).to be true
expect(image_exist?(ContentConfig.footer_logo.options[:default_url])).to be true
expect(image_exist?(ContentConfig.url_for(:logo))).to be true
expect(image_exist?(ContentConfig.url_for(:logo_mobile_svg))).to be true
expect(image_exist?(ContentConfig.url_for(:home_hero))).to be true
expect(image_exist?(ContentConfig.url_for(:footer_logo))).to be true
end
def image_exist?(default_url)

View File

@@ -1,59 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
module Spree
module Preferences
class TestConfiguration < FileConfiguration
preference :name, :string
include OpenFoodNetwork::Paperclippable
preference :logo, :file
has_attached_file :logo
end
describe FileConfiguration do
let(:c) { TestConfiguration.new }
describe "getting preferences" do
it "returns regular preferences" do
c.name = 'foo'
expect(c.get_preference(:name)).to eq('foo')
end
it "returns file preferences" do
expect(c.get_preference(:logo)).to be_a Paperclip::Attachment
end
it "returns regular preferences via []" do
c.name = 'foo'
expect(c[:name]).to eq('foo')
end
it "returns file preferences via []" do
expect(c[:logo]).to be_a Paperclip::Attachment
end
end
describe "getting preference types" do
it "returns regular preference types" do
expect(c.preference_type(:name)).to eq(:string)
end
it "returns file preference types" do
expect(c.preference_type(:logo)).to eq(:file)
end
end
describe "respond_to?" do
it "responds to preference getters" do
expect(c.respond_to?(:name)).to be true
end
it "responds to preference setters" do
expect(c.respond_to?(:name=)).to be true
end
end
end
end
end