From 8143fe10370edb1eb2edd5ca31ff34576d157097 Mon Sep 17 00:00:00 2001 From: Amaia Castro Date: Thu, 23 Mar 2017 18:03:22 +0100 Subject: [PATCH] Site customization: images uploads --- Gemfile | 2 + Gemfile.lock | 10 ++- .../site_customization/images_controller.rb | 43 ++++++++++++ .../site_customization/pages_controller.rb | 4 +- app/helpers/application_helper.rb | 4 ++ app/models/abilities/administrator.rb | 1 + app/models/site_customization/image.rb | 48 ++++++++++++++ app/views/admin/_menu.html.erb | 6 ++ .../site_customization/images/index.html.erb | 25 +++++++ app/views/layouts/_admin_header.html.erb | 4 +- app/views/layouts/_header.html.erb | 2 +- app/views/layouts/application.html.erb | 2 +- app/views/layouts/devise.html.erb | 2 +- app/views/layouts/management.html.erb | 2 +- .../shared/_social_media_meta_tags.html.erb | 4 +- app/views/welcome/index.html.erb | 2 +- config/locales/activerecord.en.yml | 8 +++ config/locales/activerecord.es.yml | 8 +++ config/locales/admin.en.yml | 16 ++++- config/locales/admin.es.yml | 18 ++++- config/routes.rb | 1 + ...145702_create_site_customization_images.rb | 11 ++++ db/schema.rb | 14 +++- .../admin/site_customization/images_spec.rb | 62 ++++++++++++++++++ .../admin/site_customization/pages_spec.rb | 4 +- spec/fixtures/files/logo_header.png | Bin 0 -> 463 bytes spec/fixtures/files/social-media-icon.png | Bin 0 -> 3353 bytes 27 files changed, 283 insertions(+), 20 deletions(-) create mode 100644 app/controllers/admin/site_customization/images_controller.rb create mode 100644 app/models/site_customization/image.rb create mode 100644 app/views/admin/site_customization/images/index.html.erb create mode 100644 db/migrate/20170322145702_create_site_customization_images.rb create mode 100644 spec/features/admin/site_customization/images_spec.rb create mode 100644 spec/fixtures/files/logo_header.png create mode 100644 spec/fixtures/files/social-media-icon.png diff --git a/Gemfile b/Gemfile index f9c2f54e5..39dee65f8 100644 --- a/Gemfile +++ b/Gemfile @@ -65,6 +65,8 @@ gem 'browser' gem 'turnout', '~> 2.4.0' gem 'redcarpet', '~> 3.4.0' +gem 'paperclip' + group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug' diff --git a/Gemfile.lock b/Gemfile.lock index 6c117ea77..67ab5a78b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -238,6 +238,7 @@ GEM mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) + mimemagic (0.3.2) mini_portile2 (2.1.0) minitest (5.10.1) multi_json (1.12.1) @@ -277,6 +278,12 @@ GEM omniauth-oauth (~> 1.1) rack orm_adapter (0.5.0) + paperclip (5.1.0) + activemodel (>= 4.2.0) + activesupport (>= 4.2.0) + cocaine (~> 0.5.5) + mime-types + mimemagic (~> 0.3.0) paranoia (2.2.1) activerecord (>= 4.0, < 5.1) parser (2.4.0.0) @@ -504,6 +511,7 @@ DEPENDENCIES omniauth-facebook (~> 4.0.0) omniauth-google-oauth2 (~> 0.4.0) omniauth-twitter + paperclip paranoia (~> 2.2.1) pg (~> 0.20.0) pg_search @@ -533,4 +541,4 @@ DEPENDENCIES whenever BUNDLED WITH - 1.13.7 + 1.14.6 diff --git a/app/controllers/admin/site_customization/images_controller.rb b/app/controllers/admin/site_customization/images_controller.rb new file mode 100644 index 000000000..c9f318f41 --- /dev/null +++ b/app/controllers/admin/site_customization/images_controller.rb @@ -0,0 +1,43 @@ +class Admin::SiteCustomization::ImagesController < Admin::SiteCustomization::BaseController + load_and_authorize_resource :image, class: "SiteCustomization::Image" + + def index + @images = SiteCustomization::Image.all_images + end + + def update + if params[:site_customization_image].nil? + redirect_to admin_site_customization_images_path + return + end + + if @image.update(image_params) + redirect_to admin_site_customization_images_path, notice: t('admin.site_customization.images.update.notice') + else + flash.now[:error] = t('admin.site_customization.images.update.error') + + @images = SiteCustomization::Image.all_images + idx = @images.index {|e| e.name == @image.name } + @images[idx] = @image + + render :index + end + end + + def destroy + @image.image = nil + if @image.save + redirect_to admin_site_customization_images_path, notice: t('admin.site_customization.images.destroy.notice') + else + redirect_to admin_site_customization_images_path, notice: t('admin.site_customization.images.destroy.error') + end + end + + private + + def image_params + params.require(:site_customization_image).permit( + :image + ) + end +end diff --git a/app/controllers/admin/site_customization/pages_controller.rb b/app/controllers/admin/site_customization/pages_controller.rb index 0b838054e..4d92a6a1e 100644 --- a/app/controllers/admin/site_customization/pages_controller.rb +++ b/app/controllers/admin/site_customization/pages_controller.rb @@ -7,7 +7,7 @@ class Admin::SiteCustomization::PagesController < Admin::SiteCustomization::Base def create if @page.save - redirect_to admin_site_customization_pages_path, notice: t('admin.site_customization.pages.create.notice', link: @page.slug.html_safe) + redirect_to admin_site_customization_pages_path, notice: t('admin.site_customization.pages.create.notice') else flash.now[:error] = t('admin.site_customization.pages.create.error') render :new @@ -16,7 +16,7 @@ class Admin::SiteCustomization::PagesController < Admin::SiteCustomization::Base def update if @page.update(page_params) - redirect_to admin_site_customization_pages_path, notice: t('admin.site_customization.pages.update.notice', link: @page.slug.html_safe) + redirect_to admin_site_customization_pages_path, notice: t('admin.site_customization.pages.update.notice') else flash.now[:error] = t('admin.site_customization.pages.update.error') render :edit diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 208ed3d3d..c449fa9f4 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -47,4 +47,8 @@ module ApplicationHelper "".html_safe + t("shared.back") end end + + def image_path_for(filename) + SiteCustomization::Image.image_path_for(filename) || filename + end end diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index 8df9270a3..c61530772 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -53,6 +53,7 @@ module Abilities can [:index, :create, :edit, :update, :destroy], Geozone can :manage, SiteCustomization::Page + can :manage, SiteCustomization::Image end end end diff --git a/app/models/site_customization/image.rb b/app/models/site_customization/image.rb new file mode 100644 index 000000000..2230a96ce --- /dev/null +++ b/app/models/site_customization/image.rb @@ -0,0 +1,48 @@ +class SiteCustomization::Image < ActiveRecord::Base + VALID_IMAGES = { + "icon_home" => [330, 240], + "logo_header" => [80, 80], + "social-media-icon" => [200, 200], + "apple-touch-icon-200" => [200, 200] + } + + has_attached_file :image + + validates :name, presence: true, uniqueness: true, inclusion: { in: VALID_IMAGES.keys } + validates_attachment_content_type :image, :content_type => ["image/png"] + validate :check_image + + def self.all_images + VALID_IMAGES.keys.map do |image_name| + find_by(name: image_name) || create!(name: image_name.to_s) + end + end + + def self.image_path_for(filename) + image_name = filename.split(".").first + + if i = find_by(name: image_name) + i.image.exists? ? i.image.url : nil + end + end + + def required_width + VALID_IMAGES[name].try(:first) + end + + def required_height + VALID_IMAGES[name].try(:second) + end + + private + + def check_image + return unless image? + + dimensions = Paperclip::Geometry.from_file(image.queued_for_write[:original].path) + + errors.add(:image, :image_width, required_width: required_width) unless dimensions.width == required_width + errors.add(:image, :image_height, required_height: required_height) unless dimensions.height == required_height + end + +end diff --git a/app/views/admin/_menu.html.erb b/app/views/admin/_menu.html.erb index 448da1662..6a48b493b 100644 --- a/app/views/admin/_menu.html.erb +++ b/app/views/admin/_menu.html.erb @@ -128,5 +128,11 @@ <%= t("admin.menu.site_customization.pages") %> <% end %> + +
  • > + <%= link_to admin_site_customization_images_path do %> + <%= t("admin.menu.site_customization.images") %> + <% end %> +
  • diff --git a/app/views/admin/site_customization/images/index.html.erb b/app/views/admin/site_customization/images/index.html.erb new file mode 100644 index 000000000..b9de152d1 --- /dev/null +++ b/app/views/admin/site_customization/images/index.html.erb @@ -0,0 +1,25 @@ +

    <%= t("admin.site_customization.images.index.title") %>

    + + + + <% @images.each do |image| %> + + + + + <% end %> + +
    + <%= image.name %> (<%= image.required_width %>x<%= image.required_height %>) + + <%= form_for([:admin, image], html: { id: "edit_#{dom_id(image)}"}) do |f| %> +
    + <%= image_tag image.image.url if image.image.exists? %> + <%= f.file_field :image, label: false %> +
    +
    + <%= f.submit(t('admin.site_customization.images.index.update'), class: "button hollow") %> + <%= link_to t('admin.site_customization.images.index.delete'), admin_site_customization_image_path(image), method: :delete, class: "button hollow alert" if image.image.exists? %> +
    + <% end %> +
    diff --git a/app/views/layouts/_admin_header.html.erb b/app/views/layouts/_admin_header.html.erb index 095e351d6..72617386c 100644 --- a/app/views/layouts/_admin_header.html.erb +++ b/app/views/layouts/_admin_header.html.erb @@ -18,7 +18,7 @@
    <%= link_to admin_root_path, class: "hide-for-small-only" do %> - <%= image_tag('logo_header.png', class: 'hide-for-small-only float-left', size: '80x80', alt: t("layouts.header.logo")) %> + <%= image_tag(image_path_for('logo_header.png'), class: 'hide-for-small-only float-left', size: '80x80', alt: t("layouts.header.logo")) %> <%= setting['org_name'] %>  | <%= t("admin.dashboard.index.title") %> <% end %> @@ -34,4 +34,4 @@
    - \ No newline at end of file + diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb index 9a33662c6..a3162bb8c 100644 --- a/app/views/layouts/_header.html.erb +++ b/app/views/layouts/_header.html.erb @@ -22,7 +22,7 @@
    <%= link_to root_path, class: "hide-for-small-only", accesskey: "0" do %> - <%= image_tag('logo_header.png', class: 'hide-for-small-only float-left', size: '80x80', alt: t("layouts.header.logo")) %> + <%= image_tag(image_path_for('logo_header.png'), class: 'hide-for-small-only float-left', size: '80x80', alt: t("layouts.header.logo")) %> <%= setting['org_name'] %> <% end %>
    diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index ef4455320..7ae505aad 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -14,7 +14,7 @@ <%= javascript_include_tag "application", 'data-turbolinks-track' => true %> <%= csrf_meta_tags %> <%= favicon_link_tag "favicon.ico" %> - <%= favicon_link_tag "apple-touch-icon-200.png", + <%= favicon_link_tag image_path_for("apple-touch-icon-200.png"), rel: "icon apple-touch-icon", sizes: "200x200", type: "image/png" %> diff --git a/app/views/layouts/devise.html.erb b/app/views/layouts/devise.html.erb index a0e9eaaff..50b84287d 100644 --- a/app/views/layouts/devise.html.erb +++ b/app/views/layouts/devise.html.erb @@ -17,7 +17,7 @@

    <%= link_to root_path do %> - <%= image_tag('logo_header.png', class: 'float-left', alt: t("layouts.header.logo")) %> + <%= image_tag(image_path_for('logo_header.png'), class: 'float-left', alt: t("layouts.header.logo")) %> <%= setting['org_name'] %> <% end %>

    diff --git a/app/views/layouts/management.html.erb b/app/views/layouts/management.html.erb index 8e497bf24..21367d8ab 100644 --- a/app/views/layouts/management.html.erb +++ b/app/views/layouts/management.html.erb @@ -35,7 +35,7 @@
    <%= link_to management_root_path, class: "hide-for-small-only" do %> - <%= image_tag('logo_header.png', class: 'hide-for-small-only float-left', size: '80x80', alt: t("layouts.header.logo")) %> + <%= image_tag(image_path_for('logo_header.png'), class: 'hide-for-small-only float-left', size: '80x80', alt: t("layouts.header.logo")) %> <%= setting['org_name'] %>  | <%= t("management.dashboard.index.title") %> <% end %> diff --git a/app/views/shared/_social_media_meta_tags.html.erb b/app/views/shared/_social_media_meta_tags.html.erb index 50a2efec9..c0e6a5e8a 100644 --- a/app/views/shared/_social_media_meta_tags.html.erb +++ b/app/views/shared/_social_media_meta_tags.html.erb @@ -3,7 +3,7 @@ - + <% if setting['url'] %> @@ -14,7 +14,7 @@ <% end %> - + diff --git a/app/views/welcome/index.html.erb b/app/views/welcome/index.html.erb index 32124a950..1eb6cf38c 100644 --- a/app/views/welcome/index.html.erb +++ b/app/views/welcome/index.html.erb @@ -12,7 +12,7 @@
    - <%= image_tag("icon_home.png", size: "330x240", alt:"") %> + <%= image_tag(image_path_for("icon_home.png"), size: "330x240", alt:"") %>
    diff --git a/config/locales/activerecord.en.yml b/config/locales/activerecord.en.yml index e8354b88e..bd18422ae 100644 --- a/config/locales/activerecord.en.yml +++ b/config/locales/activerecord.en.yml @@ -43,6 +43,9 @@ en: site_customization/page: one: Custom page other: Custom pages + site_customization/image: + one: Custom image + other: Custom images attributes: budget: name: "Name" @@ -151,3 +154,8 @@ en: attributes: slug: slug_format: "must be letters, numbers, _ and -" + site_customization/image: + attributes: + image: + image_width: "Width must be %{required_width}px" + image_height: "Height must be %{required_height}px" diff --git a/config/locales/activerecord.es.yml b/config/locales/activerecord.es.yml index 32b26223b..f3397ef19 100644 --- a/config/locales/activerecord.es.yml +++ b/config/locales/activerecord.es.yml @@ -43,6 +43,9 @@ es: site_customization/page: one: Página other: Páginas + site_customization/image: + one: Imagen + other: Imágenes attributes: budget: name: "Nombre" @@ -146,3 +149,8 @@ es: attributes: slug: slug_format: "deber ser letras, números, _ y -" + site_customization/image: + attributes: + image: + image_width: "Debe tener %{required_width}px de ancho" + image_height: "Debe tener %{required_height}px de alto" diff --git a/config/locales/admin.en.yml b/config/locales/admin.en.yml index be2f9280e..5f84f69d3 100755 --- a/config/locales/admin.en.yml +++ b/config/locales/admin.en.yml @@ -209,6 +209,7 @@ en: signature_sheets: Signature Sheets site_customization: pages: Custom Pages + images: Custom Images moderators: index: title: Moderators @@ -478,12 +479,23 @@ en: sms_code_not_confirmed: Has not confirmed the sms code title: Incomplete verifications site_customization: + images: + index: + title: Custom images + update: Update + delete: Delete + update: + notice: Image updated successfully + error: Image couldn't be updated + destroy: + notice: Image deleted successfully + error: Image couldn't be deleted pages: create: - notice: 'Page created successfully.' + notice: Page created successfully error: Process couldn't be created update: - notice: 'Page updated successfully.' + notice: Page updated successfully error: Page couldn't be updated destroy: notice: Page deleted successfully diff --git a/config/locales/admin.es.yml b/config/locales/admin.es.yml index 7b14bf90a..f4c834e34 100644 --- a/config/locales/admin.es.yml +++ b/config/locales/admin.es.yml @@ -208,7 +208,8 @@ es: stats: Estadísticas signature_sheets: Hojas de firmas site_customization: - pages: Páginas + pages: Personalizar páginas + images: Personalizar imágenes moderators: index: title: Moderadores @@ -478,12 +479,23 @@ es: sms_code_not_confirmed: No ha introducido su código de seguridad title: Verificaciones incompletas site_customization: + images: + index: + title: Personalizar imágenes + update: Actualizar + delete: Borrar + update: + notice: Imagen actualizada correctamente + error: No se ha podido actualizar la imagen + destroy: + notice: Imagen borrada correctamente + error: No se ha podido borrar la imagen pages: create: - notice: 'Página creada correctamente.' + notice: Página creada correctamente error: No se ha podido crear la página update: - notice: 'Página actualizada correctamente.s' + notice: Página actualizada correctamente error: No se ha podido actualizar la página destroy: notice: Página eliminada correctamente diff --git a/config/routes.rb b/config/routes.rb index af9ac8a08..51d2a47da 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -225,6 +225,7 @@ Rails.application.routes.draw do namespace :site_customization do resources :pages, except: [:show] + resources :images, only: [:index, :update, :destroy] end end diff --git a/db/migrate/20170322145702_create_site_customization_images.rb b/db/migrate/20170322145702_create_site_customization_images.rb new file mode 100644 index 000000000..4f980b566 --- /dev/null +++ b/db/migrate/20170322145702_create_site_customization_images.rb @@ -0,0 +1,11 @@ +class CreateSiteCustomizationImages < ActiveRecord::Migration + def change + create_table :site_customization_images do |t| + t.string :name, null: false + t.attachment :image + t.timestamps null: false + end + + add_index :site_customization_images, :name, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 0b38a4196..938fbb9ec 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170316174351) do +ActiveRecord::Schema.define(version: 20170322145702) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -439,6 +439,18 @@ ActiveRecord::Schema.define(version: 20170316174351) do t.datetime "updated_at" end + create_table "site_customization_images", force: :cascade do |t| + t.string "name", null: false + t.string "image_file_name" + t.string "image_content_type" + t.integer "image_file_size" + t.datetime "image_updated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "site_customization_images", ["name"], name: "index_site_customization_images_on_name", unique: true, using: :btree + create_table "site_customization_pages", force: :cascade do |t| t.string "slug", null: false t.string "title", null: false diff --git a/spec/features/admin/site_customization/images_spec.rb b/spec/features/admin/site_customization/images_spec.rb new file mode 100644 index 000000000..094ac19bf --- /dev/null +++ b/spec/features/admin/site_customization/images_spec.rb @@ -0,0 +1,62 @@ +require 'rails_helper' + +feature "Admin custom images" do + + background do + admin = create(:administrator) + login_as(admin.user) + end + + scenario "Upload valid image" do + visit admin_root_path + + within("#side_menu") do + click_link "Custom Images" + end + + within("tr.logo_header") do + attach_file "site_customization_image_image", "spec/fixtures/files/logo_header.png" + click_button "Update" + end + + expect(page).to have_css("tr.logo_header img[src*='logo_header.png']") + expect(page).to have_css("img[src*='logo_header.png']", count: 2) # one in the admin form an one in the page header + end + + scenario "Upload invalid image" do + visit admin_root_path + + within("#side_menu") do + click_link "Custom Images" + end + + within("tr.icon_home") do + attach_file "site_customization_image_image", "spec/fixtures/files/logo_header.png" + click_button "Update" + end + + expect(page).to have_content("Width must be 330px") + expect(page).to have_content("Height must be 240px") + end + + scenario "Delete image" do + visit admin_root_path + + within("#side_menu") do + click_link "Custom Images" + end + + within("tr.social-media-icon") do + attach_file "site_customization_image_image", "spec/fixtures/files/social-media-icon.png" + click_button "Update" + end + + expect(page).to have_css("img[src*='social-media-icon.png']") + + within("tr.social-media-icon") do + click_link "Delete" + end + + expect(page).to_not have_css("img[src*='social-media-icon.png']") + end +end diff --git a/spec/features/admin/site_customization/pages_spec.rb b/spec/features/admin/site_customization/pages_spec.rb index 20b817f50..5d03638d8 100644 --- a/spec/features/admin/site_customization/pages_spec.rb +++ b/spec/features/admin/site_customization/pages_spec.rb @@ -39,7 +39,7 @@ feature "Admin custom pages" do context "Update" do scenario "Valid custom page" do - custom_page = create(:site_customization_page, title: "An example custom page") + create(:site_customization_page, title: "An example custom page") visit admin_root_path within("#side_menu") do @@ -58,7 +58,7 @@ feature "Admin custom pages" do end end - scenario "Index" do + scenario "Delete" do custom_page = create(:site_customization_page, title: "An example custom page") visit edit_admin_site_customization_page_path(custom_page) diff --git a/spec/fixtures/files/logo_header.png b/spec/fixtures/files/logo_header.png new file mode 100644 index 0000000000000000000000000000000000000000..5f53555744121150dfe598df06fe86f37079d3d7 GIT binary patch literal 463 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdl8)Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmPtvzP#z^keqBd<+bXF`h1tAr-gY-nlKyo5Xf?>f2fHB@6Y;-h8rto}ik%bkW2_PtCh0zDlq#FmWg}usARzr=6AEkbnG& z@X^18@hcp56+F<32|jnY$9Qr@bbm^<^id?ru2F59`e}y~bP0l+XkKoT#qZ literal 0 HcmV?d00001 diff --git a/spec/fixtures/files/social-media-icon.png b/spec/fixtures/files/social-media-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..fcd00e212f4902b2cf737405a3b55024703ecfcf GIT binary patch literal 3353 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 bQzobvL?qkdav$vz128qVgtlMx`15}Nuhbo& literal 0 HcmV?d00001