From 28792fc8952fa7f44f7d10362c38aabd89896a00 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Thu, 26 Jul 2018 13:21:49 +0800 Subject: [PATCH 1/8] Add tests for adding/updating enterprise images --- .../admin/enterprises/form/_images.html.haml | 9 +- .../features/admin/enterprises/images_spec.rb | 82 +++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 spec/features/admin/enterprises/images_spec.rb diff --git a/app/views/admin/enterprises/form/_images.html.haml b/app/views/admin/enterprises/form/_images.html.haml index 97d31dabcc..ea718607b5 100644 --- a/app/views/admin/enterprises/form/_images.html.haml +++ b/app/views/admin/enterprises/form/_images.html.haml @@ -1,12 +1,13 @@ -.row +.row.page-admin-enterprises-form__logo-field-group.image-field-group .alpha.three.columns = f.label :logo %br 100 x 100 pixels .omega.eight.columns - = image_tag @object.logo(:medium) if @object.logo.present? + = image_tag @object.logo(:medium), class: 'image-field-group__preview-image' if @object.logo.present? = f.file_field :logo -.row + +.row.page-admin-enterprises-form__promo-image-field-group.image-field-group .alpha.three.columns = f.label :promo_image, 'ofn-with-tip' => t('.promo_image_placeholder') %br/ @@ -17,5 +18,5 @@ = t('.promo_image_note3') .omega.eight.columns - = image_tag @object.promo_image(:large) if @object.promo_image.present? + = image_tag @object.promo_image(:large), class: 'image-field-group__preview-image' if @object.promo_image.present? = f.file_field :promo_image diff --git a/spec/features/admin/enterprises/images_spec.rb b/spec/features/admin/enterprises/images_spec.rb new file mode 100644 index 0000000000..01f8754e2d --- /dev/null +++ b/spec/features/admin/enterprises/images_spec.rb @@ -0,0 +1,82 @@ +require "spec_helper" + +feature "Managing enterprise images" do + include AuthenticationWorkflow + include WebHelper + + context "as an Enterprise user", js: true do + let(:enterprise_user) { create_enterprise_user(enterprise_limit: 1) } + let(:distributor) { create(:distributor_enterprise, name: "First Distributor") } + + before do + enterprise_user.enterprise_roles.build(enterprise: distributor).save! + + quick_login_as enterprise_user + visit edit_admin_enterprise_path(distributor) + end + + describe "images for an enterprise", js: true do + def go_to_images + within(".side_menu") do + click_link "Images" + end + end + + before do + go_to_images + end + + scenario "editing logo" do + # Adding image + attach_file "enterprise[logo]", File.join(Rails.root, "app/assets/images/logo-white.png") + click_button "Update" + + expect(page).to have_content("Enterprise \"#{distributor.name}\" has been successfully updated!") + + go_to_images + within ".page-admin-enterprises-form__logo-field-group" do + expect(page).to have_selector(".image-field-group__preview-image") + expect(html).to include("logo-white.png") + end + + # Replacing image + attach_file "enterprise[logo]", File.join(Rails.root, "app/assets/images/logo-black.png") + click_button "Update" + + expect(page).to have_content("Enterprise \"#{distributor.name}\" has been successfully updated!") + + go_to_images + within ".page-admin-enterprises-form__logo-field-group" do + expect(page).to have_selector(".image-field-group__preview-image") + expect(html).to include("logo-black.png") + end + end + + scenario "editing promo image" do + # Adding image + attach_file "enterprise[promo_image]", File.join(Rails.root, "app/assets/images/logo-white.png") + click_button "Update" + + expect(page).to have_content("Enterprise \"#{distributor.name}\" has been successfully updated!") + + go_to_images + within ".page-admin-enterprises-form__promo-image-field-group" do + expect(page).to have_selector(".image-field-group__preview-image") + expect(html).to include("logo-white.jpg") + end + + # Replacing image + attach_file "enterprise[promo_image]", File.join(Rails.root, "app/assets/images/logo-black.png") + click_button "Update" + + expect(page).to have_content("Enterprise \"#{distributor.name}\" has been successfully updated!") + + go_to_images + within ".page-admin-enterprises-form__promo-image-field-group" do + expect(page).to have_selector(".image-field-group__preview-image") + expect(html).to include("logo-black.jpg") + end + end + end + end +end From f30032eee7f8d64e7444df15a33760b6da98596f Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Sun, 29 Jul 2018 15:51:24 +0800 Subject: [PATCH 2/8] Include image URLs in serialized enterprise for admin --- .../api/admin/enterprise_serializer.rb | 29 ++++++++++ .../admin/enterprise_serializer_spec.rb | 54 ++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/app/serializers/api/admin/enterprise_serializer.rb b/app/serializers/api/admin/enterprise_serializer.rb index e29954992f..348bb75461 100644 --- a/app/serializers/api/admin/enterprise_serializer.rb +++ b/app/serializers/api/admin/enterprise_serializer.rb @@ -5,11 +5,20 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer attributes :preferred_product_selection_from_inventory_only attributes :owner, :contact, :users, :tag_groups, :default_tag_group attributes :require_login, :allow_guest_orders, :allow_order_changes + attributes :logo, :promo_image has_one :owner, serializer: Api::Admin::UserSerializer has_many :users, serializer: Api::Admin::UserSerializer has_one :address, serializer: Api::AddressSerializer + def logo + attachment_urls(object.logo, [:thumb, :small, :medium]) + end + + def promo_image + attachment_urls(object.promo_image, [:thumb, :medium, :large]) + end + def tag_groups object.tag_rules.prioritised.reject(&:is_default).each_with_object([]) do |tag_rule, tag_groups| tag_group = find_match(tag_groups, tag_rule.preferred_customer_tags.split(",").map{ |t| { text: t } }) @@ -33,4 +42,24 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer end return { tags: tags, rules: [] } end + + private + + # Returns a hash of URLs for specified versions of an attachment. + # + # Example: + # + # attachment_urls(object.logo, [:thumb, :small, :medium]) + # # { + # # thumb: LOGO_THUMB_URL, + # # small: LOGO_SMALL_URL, + # # medium: LOGO_MEDIUM_URL + # # } + def attachment_urls(attachment, versions) + return unless attachment.exists? + + versions.each_with_object({}) do |version, urls| + urls[version] = attachment.url(version) + end + end end diff --git a/spec/serializers/admin/enterprise_serializer_spec.rb b/spec/serializers/admin/enterprise_serializer_spec.rb index 0898b7d4a0..5bf40559d0 100644 --- a/spec/serializers/admin/enterprise_serializer_spec.rb +++ b/spec/serializers/admin/enterprise_serializer_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe Api::Admin::EnterpriseSerializer do let(:enterprise) { create(:distributor_enterprise) } @@ -6,4 +6,56 @@ describe Api::Admin::EnterpriseSerializer do serializer = Api::Admin::EnterpriseSerializer.new enterprise serializer.to_json.should match enterprise.name end + + context "for logo" do + let(:enterprise) { create(:distributor_enterprise, logo: image) } + + context "when there is a logo" do + let(:image) do + image_path = File.open(Rails.root.join("app", "assets", "images", "logo-black.png")) + Rack::Test::UploadedFile.new(image_path, "image/png") + end + + it "includes URLs of image versions" do + serializer = Api::Admin::EnterpriseSerializer.new(enterprise) + expect(serializer.as_json[:logo]).to_not be_blank + expect(serializer.as_json[:logo][:medium]).to match(/logo-black.png/) + end + end + + context "when there is no logo" do + let(:image) { nil } + + it "includes URLs of image versions" do + serializer = Api::Admin::EnterpriseSerializer.new(enterprise) + expect(serializer.as_json[:logo]).to be_blank + end + end + end + + context "for promo image" do + let(:enterprise) { create(:distributor_enterprise, promo_image: image) } + + context "when there is a promo image" do + let(:image) do + image_path = File.open(Rails.root.join("app", "assets", "images", "logo-black.png")) + Rack::Test::UploadedFile.new(image_path, "image/png") + end + + it "includes URLs of image versions" do + serializer = Api::Admin::EnterpriseSerializer.new(enterprise) + expect(serializer.as_json[:promo_image]).to_not be_blank + expect(serializer.as_json[:promo_image][:medium]).to match(/logo-black.jpg/) + end + end + + context "when there is no promo image" do + let(:image) { nil } + + it "includes URLs of image versions" do + serializer = Api::Admin::EnterpriseSerializer.new(enterprise) + expect(serializer.as_json[:promo_image]).to be_nil + end + end + end end From 01d4b8fb1037facf0cc0f0c3d245a3cd34241422 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Sun, 26 Aug 2018 03:41:54 +0800 Subject: [PATCH 3/8] Allow custom resource permission for admin controllers --- app/controllers/spree/admin/base_controller_decorator.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/spree/admin/base_controller_decorator.rb b/app/controllers/spree/admin/base_controller_decorator.rb index fb0e54baa8..60887f2ce5 100644 --- a/app/controllers/spree/admin/base_controller_decorator.rb +++ b/app/controllers/spree/admin/base_controller_decorator.rb @@ -28,7 +28,11 @@ Spree::Admin::BaseController.class_eval do record = self.class.to_s.sub("Controller", "").underscore.split('/').last.singularize.to_sym end authorize! :admin, record - authorize! action, record + authorize! resource_authorize_action, record + end + + def resource_authorize_action + action end # This is in Spree::Core::ControllerHelpers::Auth From 9c3bb863da166cdc31c5e56b930762f586a46fb9 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Thu, 12 Jul 2018 11:43:10 +0800 Subject: [PATCH 4/8] Add endpoints for removing enterprise images --- app/controllers/api/base_controller.rb | 4 + .../api/enterprise_attachment_controller.rb | 37 ++++++++ app/controllers/api/logos_controller.rb | 16 ++++ .../api/promo_images_controller.rb | 16 ++++ app/models/spree/ability_decorator.rb | 2 +- config/locales/en.yml | 8 ++ config/routes.rb | 4 + spec/controllers/api/logos_controller_spec.rb | 90 +++++++++++++++++++ .../api/promo_images_controller_spec.rb | 90 +++++++++++++++++++ spec/models/spree/ability_spec.rb | 4 +- 10 files changed, 268 insertions(+), 3 deletions(-) create mode 100644 app/controllers/api/enterprise_attachment_controller.rb create mode 100644 app/controllers/api/logos_controller.rb create mode 100644 app/controllers/api/promo_images_controller.rb create mode 100644 spec/controllers/api/logos_controller_spec.rb create mode 100644 spec/controllers/api/promo_images_controller_spec.rb diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index f25c47417d..a7f96d01cf 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -9,5 +9,9 @@ module Api include ActionController::UrlFor include Rails.application.routes.url_helpers use_renderers :json + + def respond_with_conflict(json_hash) + render json: json_hash, status: :conflict + end end end diff --git a/app/controllers/api/enterprise_attachment_controller.rb b/app/controllers/api/enterprise_attachment_controller.rb new file mode 100644 index 0000000000..083d6946be --- /dev/null +++ b/app/controllers/api/enterprise_attachment_controller.rb @@ -0,0 +1,37 @@ +module Api + class EnterpriseAttachmentController < BaseController + class MissingImplementationError < StandardError; end + class UnknownEnterpriseAuthorizationActionError < StandardError; end + + before_filter :load_enterprise + + respond_to :json + + def destroy + return respond_with_conflict(error: destroy_attachment_does_not_exist_error_message) unless @enterprise.public_send("#{attachment_name}?") + + @enterprise.update_attributes!(attachment_name => nil) + render json: @enterprise, serializer: Admin::EnterpriseSerializer, spree_current_user: spree_current_user + end + + protected + + def attachment_name + raise MissingImplementationError, "Method attachment_name should be defined" + end + + def enterprise_authorize_action + raise MissingImplementationError, "Method enterprise_authorize_action should be defined" + end + + def load_enterprise + @enterprise = Enterprise.find_by_permalink(params[:enterprise_id].to_s) + raise UnknownEnterpriseAuthorizationActionError if enterprise_authorize_action.blank? + authorize!(enterprise_authorize_action, @enterprise) + end + + def destroy_attachment_does_not_exist_error_message + I18n.t("api.enterprise_#{attachment_name}.destroy_attachment_does_not_exist") + end + end +end diff --git a/app/controllers/api/logos_controller.rb b/app/controllers/api/logos_controller.rb new file mode 100644 index 0000000000..f3e7934724 --- /dev/null +++ b/app/controllers/api/logos_controller.rb @@ -0,0 +1,16 @@ +module Api + class LogosController < EnterpriseAttachmentController + private + + def attachment_name + :logo + end + + def enterprise_authorize_action + case action_name.to_sym + when :destroy + :remove_logo + end + end + end +end diff --git a/app/controllers/api/promo_images_controller.rb b/app/controllers/api/promo_images_controller.rb new file mode 100644 index 0000000000..0e79ebdb93 --- /dev/null +++ b/app/controllers/api/promo_images_controller.rb @@ -0,0 +1,16 @@ +module Api + class PromoImagesController < EnterpriseAttachmentController + private + + def attachment_name + :promo_image + end + + def enterprise_authorize_action + case action_name.to_sym + when :destroy + :remove_promo_image + end + end + end +end diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 53198251b9..9d0941a220 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -99,7 +99,7 @@ class AbilityDecorator end can [:admin, :index, :create], Enterprise - can [:read, :edit, :update, :bulk_update, :resend_confirmation], Enterprise do |enterprise| + can [:read, :edit, :update, :remove_logo, :remove_promo_image, :bulk_update, :resend_confirmation], Enterprise do |enterprise| OpenFoodNetwork::Permissions.new(user).editable_enterprises.include? enterprise end can [:welcome, :register], Enterprise do |enterprise| diff --git a/config/locales/en.yml b/config/locales/en.yml index 540d306a95..61f74ad2a8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1077,6 +1077,14 @@ en: stripe_connect_settings: resource: Stripe Connect configuration +# API +# + api: + enterprise_logo: + destroy_attachment_does_not_exist: "Logo does not exist" + enterprise_promo_image: + destroy_attachment_does_not_exist: "Promo image does not exist" + # Frontend views # # These keys are referenced relatively like `t('.message')` in diff --git a/config/routes.rb b/config/routes.rb index 574140bd5b..4f6125c8ac 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -95,7 +95,11 @@ Openfoodnetwork::Application.routes.draw do post :update_image, on: :member get :managed, on: :collection get :accessible, on: :collection + + resource :logo, only: [:destroy] + resource :promo_image, only: [:destroy] end + resources :order_cycles do get :managed, on: :collection get :accessible, on: :collection diff --git a/spec/controllers/api/logos_controller_spec.rb b/spec/controllers/api/logos_controller_spec.rb new file mode 100644 index 0000000000..213ac8bc6e --- /dev/null +++ b/spec/controllers/api/logos_controller_spec.rb @@ -0,0 +1,90 @@ +require "spec_helper" + +module Api + describe LogosController, type: :controller do + include AuthenticationWorkflow + + let(:admin_user) { create(:admin_user) } + let(:enterprise_owner) { create(:user) } + let(:enterprise) { create(:enterprise, owner: enterprise_owner ) } + let(:enterprise_manager) { create(:user, enterprise_limit: 10, enterprises: [enterprise]) } + let(:other_enterprise_owner) { create(:user) } + let(:other_enterprise) { create(:enterprise, owner: other_enterprise_owner ) } + let(:other_enterprise_manager) { create(:user, enterprise_limit: 10, enterprises: [other_enterprise]) } + + describe "removing logo" do + image_path = File.open(Rails.root.join("app", "assets", "images", "logo-black.png")) + let(:image) { Rack::Test::UploadedFile.new(image_path, "image/png") } + + let(:enterprise) { create(:enterprise, owner: enterprise_owner, logo: image) } + + before do + allow(controller).to receive(:spree_current_user) { current_user } + end + + context "as manager" do + let(:current_user) { enterprise_manager } + + it "removes logo" do + spree_delete :destroy, enterprise_id: enterprise + + expect(response).to be_success + expect(json_response["id"]).to eq enterprise.id + enterprise.reload + expect(enterprise.logo?).to be false + end + + context "when logo does not exist" do + let(:enterprise) { create(:enterprise, owner: enterprise_owner, logo: nil) } + + it "responds with error" do + spree_delete :destroy, enterprise_id: enterprise + + expect(response.status).to eq(409) + expect(json_response["error"]).to eq I18n.t("api.enterprise_logo.destroy_attachment_does_not_exist") + end + end + end + + context "as owner" do + let(:current_user) { enterprise_owner } + + it "allows removal of logo" do + spree_delete :destroy, enterprise_id: enterprise + expect(response).to be_success + end + end + + context "as super admin" do + let(:current_user) { admin_user } + + it "allows removal of logo" do + spree_delete :destroy, enterprise_id: enterprise + expect(response).to be_success + end + end + + context "as manager of other enterprise" do + let(:current_user) { other_enterprise_manager } + + it "does not allow removal of logo" do + spree_delete :destroy, enterprise_id: enterprise + expect(response.status).to eq(401) + enterprise.reload + expect(enterprise.logo?).to be true + end + end + + context "as owner of other enterprise" do + let(:current_user) { other_enterprise_owner } + + it "does not allow removal of logo" do + spree_delete :destroy, enterprise_id: enterprise + expect(response.status).to eq(401) + enterprise.reload + expect(enterprise.logo?).to be true + end + end + end + end +end diff --git a/spec/controllers/api/promo_images_controller_spec.rb b/spec/controllers/api/promo_images_controller_spec.rb new file mode 100644 index 0000000000..cce6d08f5f --- /dev/null +++ b/spec/controllers/api/promo_images_controller_spec.rb @@ -0,0 +1,90 @@ +require "spec_helper" + +module Api + describe PromoImagesController, type: :controller do + include AuthenticationWorkflow + + let(:admin_user) { create(:admin_user) } + let(:enterprise_owner) { create(:user) } + let(:enterprise) { create(:enterprise, owner: enterprise_owner ) } + let(:enterprise_manager) { create(:user, enterprise_limit: 10, enterprises: [enterprise]) } + let(:other_enterprise_owner) { create(:user) } + let(:other_enterprise) { create(:enterprise, owner: other_enterprise_owner ) } + let(:other_enterprise_manager) { create(:user, enterprise_limit: 10, enterprises: [other_enterprise]) } + + describe "removing promo image" do + image_path = File.open(Rails.root.join("app", "assets", "images", "logo-black.png")) + let(:image) { Rack::Test::UploadedFile.new(image_path, "image/png") } + + let(:enterprise) { create(:enterprise, owner: enterprise_owner, promo_image: image) } + + before do + allow(controller).to receive(:spree_current_user) { current_user } + end + + context "as manager" do + let(:current_user) { enterprise_manager } + + it "removes promo image" do + spree_delete :destroy, enterprise_id: enterprise + + expect(response).to be_success + expect(json_response["id"]).to eq enterprise.id + enterprise.reload + expect(enterprise.promo_image?).to be false + end + + context "when promo image does not exist" do + let(:enterprise) { create(:enterprise, owner: enterprise_owner, promo_image: nil) } + + it "responds with error" do + spree_delete :destroy, enterprise_id: enterprise + + expect(response.status).to eq(409) + expect(json_response["error"]).to eq I18n.t("api.enterprise_promo_image.destroy_attachment_does_not_exist") + end + end + end + + context "as owner" do + let(:current_user) { enterprise_owner } + + it "allows removal of promo image" do + spree_delete :destroy, enterprise_id: enterprise + expect(response).to be_success + end + end + + context "as super admin" do + let(:current_user) { admin_user } + + it "allows removal of promo image" do + spree_delete :destroy, enterprise_id: enterprise + expect(response).to be_success + end + end + + context "as manager of other enterprise" do + let(:current_user) { other_enterprise_manager } + + it "does not allow removal of promo image" do + spree_delete :destroy, enterprise_id: enterprise + expect(response.status).to eq(401) + enterprise.reload + expect(enterprise.promo_image?).to be true + end + end + + context "as owner of other enterprise" do + let(:current_user) { other_enterprise_owner } + + it "does not allow removal of promo image" do + spree_delete :destroy, enterprise_id: enterprise + expect(response.status).to eq(401) + enterprise.reload + expect(enterprise.promo_image?).to be true + end + end + end + end +end diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 7882665618..dcd290e195 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -297,11 +297,11 @@ module Spree let!(:er_pd) { create(:enterprise_relationship, parent: d_related, child: d1, permissions_list: [:edit_profile]) } it "should be able to edit enterprises it manages" do - should have_ability([:read, :edit, :update, :bulk_update, :resend_confirmation], for: d1) + should have_ability([:read, :edit, :update, :remove_logo, :remove_promo_image, :bulk_update, :resend_confirmation], for: d1) end it "should be able to edit enterprises it has permission to" do - should have_ability([:read, :edit, :update, :bulk_update, :resend_confirmation], for: d_related) + should have_ability([:read, :edit, :update, :remove_logo, :remove_promo_image, :bulk_update, :resend_confirmation], for: d_related) end it "should be able to manage shipping methods, payment methods and enterprise fees for enterprises it manages" do From c9370672c6a38d8be7cf0ec7f3e9aeafb49d402b Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Sat, 28 Jul 2018 23:44:28 +0800 Subject: [PATCH 5/8] Add JS support for removal of enterprise images --- .../resources/enterprise_resource.js.coffee | 10 +++ .../resources/services/enterprises.js.coffee | 14 ++++ .../services/enterprises_spec.js.coffee | 70 +++++++++++++++++++ 3 files changed, 94 insertions(+) diff --git a/app/assets/javascripts/admin/resources/resources/enterprise_resource.js.coffee b/app/assets/javascripts/admin/resources/resources/enterprise_resource.js.coffee index 7cdd5b7bce..25b72e1922 100644 --- a/app/assets/javascripts/admin/resources/resources/enterprise_resource.js.coffee +++ b/app/assets/javascripts/admin/resources/resources/enterprise_resource.js.coffee @@ -8,4 +8,14 @@ angular.module("admin.resources").factory 'EnterpriseResource', ($resource) -> isArray: true 'update': method: 'PUT' + 'removeLogo': + url: '/admin/enterprises/:id/images/:action.json' + method: 'DELETE' + params: + action: 'remove_logo' + 'removePromoImage': + url: '/admin/enterprises/:id/images/:action.json' + method: 'DELETE' + params: + action: 'remove_promo_image' }) diff --git a/app/assets/javascripts/admin/resources/services/enterprises.js.coffee b/app/assets/javascripts/admin/resources/services/enterprises.js.coffee index 0b7fa6e870..435cc88500 100644 --- a/app/assets/javascripts/admin/resources/services/enterprises.js.coffee +++ b/app/assets/javascripts/admin/resources/services/enterprises.js.coffee @@ -38,3 +38,17 @@ angular.module("admin.resources").factory 'Enterprises', ($q, EnterpriseResource resetAttribute: (enterprise, attribute) -> enterprise[attribute] = @pristineByID[enterprise.id][attribute] + + performActionOnEnterpriseResource = (resourceAction) -> + (enterprise) -> + deferred = $q.defer() + resourceAction({id: enterprise.permalink}, ((data) => + @pristineByID[enterprise.id] = angular.copy(data) + deferred.resolve(data) + ), ((response) -> + deferred.reject(response) + )) + deferred.promise + + removeLogo: performActionOnEnterpriseResource(EnterpriseResource.removeLogo) + removePromoImage: performActionOnEnterpriseResource(EnterpriseResource.removePromoImage) diff --git a/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee index 8c088b1715..85cf19f067 100644 --- a/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee @@ -118,3 +118,73 @@ describe "Enterprises service", -> it "resets the specified value according to the pristine record", -> Enterprises.resetAttribute(enterprise, "name") expect(enterprise.name).toEqual "enterprise1" + + describe "#removeLogo", -> + enterprise = null + + describe "success", -> + resolved = false + + beforeEach -> + enterprise = new EnterpriseResource({ id: 15, permalink: "enterprise1", name: "Enterprise 1", logo: {} }) + $httpBackend.expectDELETE("/admin/enterprises/enterprise1/images/remove_logo.json").respond 200, { id: 15, name: "Enterprise 1"} + Enterprises.removeLogo(enterprise).then( -> resolved = true) + $httpBackend.flush() + + it "updates the pristine copy of the enterprise", -> + expect(Enterprises.pristineByID[15]).not.toBeUndefined() + expect(Enterprises.pristineByID[15]["id"]).toEqual(15) + expect(Enterprises.pristineByID[15]["logo"]).toBeUndefined() + + it "resolves the promise", -> + expect(resolved).toBe(true) + + describe "failure", -> + rejected = false + + beforeEach -> + enterprise = new EnterpriseResource( { id: 15, permalink: "enterprise1", name: "Enterprise 1" } ) + $httpBackend.expectDELETE("/admin/enterprises/enterprise1/images/remove_logo.json").respond 409, { error: "obj" } + Enterprises.removeLogo(enterprise).catch( -> rejected = true) + $httpBackend.flush() + + it "does not update the pristine copy of the enterprise", -> + expect(Enterprises.pristineByID[15]).toBeUndefined() + + it "rejects the promise", -> + expect(rejected).toBe(true) + + describe "#removePromoImage", -> + enterprise = null + + describe "success", -> + resolved = false + + beforeEach -> + enterprise = new EnterpriseResource({ id: 15, permalink: "enterprise1", name: "Enterprise 1", promo_image: {} }) + $httpBackend.expectDELETE("/admin/enterprises/enterprise1/images/remove_promo_image.json").respond 200, { id: 15, name: "Enterprise 1"} + Enterprises.removePromoImage(enterprise).then( -> resolved = true) + $httpBackend.flush() + + it "updates the pristine copy of the enterprise", -> + expect(Enterprises.pristineByID[15]).not.toBeUndefined() + expect(Enterprises.pristineByID[15]["id"]).toEqual(15) + expect(Enterprises.pristineByID[15]["promo_image"]).toBeUndefined() + + it "resolves the promise", -> + expect(resolved).toBe(true) + + describe "failure", -> + rejected = false + + beforeEach -> + enterprise = new EnterpriseResource( { id: 15, permalink: "enterprise1", name: "Enterprise 1" } ) + $httpBackend.expectDELETE("/admin/enterprises/enterprise1/images/remove_promo_image.json").respond 409, { error: "obj" } + Enterprises.removePromoImage(enterprise).catch( -> rejected = true) + $httpBackend.flush() + + it "does not update the pristine copy of the enterprise", -> + expect(Enterprises.pristineByID[15]).toBeUndefined() + + it "rejects the promise", -> + expect(rejected).toBe(true); From 368612cad6098cf177168e880d4dccab3f6090d8 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Mon, 30 Jul 2018 07:42:51 +0800 Subject: [PATCH 6/8] Allow removal of enterprise logo and promo image --- .../enterprise_controller.js.coffee | 22 +++++- .../resources/enterprise_resource.js.coffee | 8 +-- .../admin/enterprises/form/_images.html.haml | 8 ++- config/locales/en.yml | 6 ++ .../features/admin/enterprises/images_spec.rb | 30 ++++++-- spec/features/admin/enterprises_spec.rb | 12 ---- .../enterprise_controller_spec.js.coffee | 72 ++++++++++++++++++- .../services/enterprises_spec.js.coffee | 8 +-- 8 files changed, 135 insertions(+), 31 deletions(-) diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index bb1ae6b31a..a6b0a40958 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -1,5 +1,5 @@ angular.module("admin.enterprises") - .controller "enterpriseCtrl", ($scope, $http, $window, NavigationCheck, enterprise, EnterprisePaymentMethods, EnterpriseShippingMethods, SideMenu, StatusMessage) -> + .controller "enterpriseCtrl", ($scope, $http, $window, NavigationCheck, enterprise, Enterprises, EnterprisePaymentMethods, EnterpriseShippingMethods, SideMenu, StatusMessage) -> $scope.Enterprise = enterprise $scope.PaymentMethods = EnterprisePaymentMethods.paymentMethods $scope.ShippingMethods = EnterpriseShippingMethods.shippingMethods @@ -67,3 +67,23 @@ angular.module("admin.enterprises") $scope.resetModal = -> $scope.newUser = $scope.invite_errors = $scope.invite_success = null + + $scope.removeLogo = -> + Enterprises.removeLogo($scope.Enterprise).then (data) -> + $scope.Enterprise = angular.copy(data) + $scope.$emit("enterprise:updated", $scope.Enterprise) + + StatusMessage.display("success", t("admin.enterprises.remove_logo.removed_successfully")) + , (response) -> + if response.data.error? + StatusMessage.display("failure", response.data.error) + + $scope.removePromoImage = -> + Enterprises.removePromoImage($scope.Enterprise).then (data) -> + $scope.Enterprise = angular.copy(data) + $scope.$emit("enterprise:updated", $scope.Enterprise) + + StatusMessage.display("success", t("admin.enterprises.remove_promo_image.removed_successfully")) + , (response) -> + if response.data.error? + StatusMessage.display("failure", response.data.error) diff --git a/app/assets/javascripts/admin/resources/resources/enterprise_resource.js.coffee b/app/assets/javascripts/admin/resources/resources/enterprise_resource.js.coffee index 25b72e1922..ec89bbda36 100644 --- a/app/assets/javascripts/admin/resources/resources/enterprise_resource.js.coffee +++ b/app/assets/javascripts/admin/resources/resources/enterprise_resource.js.coffee @@ -9,13 +9,9 @@ angular.module("admin.resources").factory 'EnterpriseResource', ($resource) -> 'update': method: 'PUT' 'removeLogo': - url: '/admin/enterprises/:id/images/:action.json' + url: '/api/enterprises/:id/logo.json' method: 'DELETE' - params: - action: 'remove_logo' 'removePromoImage': - url: '/admin/enterprises/:id/images/:action.json' + url: '/api/enterprises/:id/promo_image.json' method: 'DELETE' - params: - action: 'remove_promo_image' }) diff --git a/app/views/admin/enterprises/form/_images.html.haml b/app/views/admin/enterprises/form/_images.html.haml index ea718607b5..d44e619cf6 100644 --- a/app/views/admin/enterprises/form/_images.html.haml +++ b/app/views/admin/enterprises/form/_images.html.haml @@ -4,8 +4,10 @@ %br 100 x 100 pixels .omega.eight.columns - = image_tag @object.logo(:medium), class: 'image-field-group__preview-image' if @object.logo.present? + = image_tag '', class: 'image-field-group__preview-image', 'ng-src' => '{{ Enterprise.logo.medium }}', 'ng-if' => 'Enterprise.logo' = f.file_field :logo + %a.button.red{ href: '', ng: {click: 'removeLogo()', if: 'Enterprise.logo'} } + = t('admin.enterprises.remove_logo.remove') .row.page-admin-enterprises-form__promo-image-field-group.image-field-group .alpha.three.columns @@ -18,5 +20,7 @@ = t('.promo_image_note3') .omega.eight.columns - = image_tag @object.promo_image(:large), class: 'image-field-group__preview-image' if @object.promo_image.present? + = image_tag '', class: 'image-field-group__preview-image', 'ng-src' => '{{ Enterprise.promo_image.large }}', 'ng-if' => 'Enterprise.promo_image' = f.file_field :promo_image + %a.button.red{ href: '', ng: {click: 'removePromoImage()', if: 'Enterprise.promo_image'} } + = t('admin.enterprises.remove_promo_image.remove') diff --git a/config/locales/en.yml b/config/locales/en.yml index 61f74ad2a8..677b3b9d2f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -831,6 +831,12 @@ en: new: title: New Enterprise back_link: Back to enterprises list + remove_logo: + remove: "Remove Image" + removed_successfully: "Logo removed successfully" + remove_promo_image: + remove: "Remove Image" + removed_successfully: "Promo image removed successfully" welcome: welcome_title: Welcome to the Open Food Network! welcome_text: You have successfully created a diff --git a/spec/features/admin/enterprises/images_spec.rb b/spec/features/admin/enterprises/images_spec.rb index 01f8754e2d..92c4bbafaf 100644 --- a/spec/features/admin/enterprises/images_spec.rb +++ b/spec/features/admin/enterprises/images_spec.rb @@ -28,7 +28,7 @@ feature "Managing enterprise images" do scenario "editing logo" do # Adding image - attach_file "enterprise[logo]", File.join(Rails.root, "app/assets/images/logo-white.png") + attach_file "enterprise[logo]", Rails.root.join("app", "assets", "images", "logo-white.png") click_button "Update" expect(page).to have_content("Enterprise \"#{distributor.name}\" has been successfully updated!") @@ -40,7 +40,7 @@ feature "Managing enterprise images" do end # Replacing image - attach_file "enterprise[logo]", File.join(Rails.root, "app/assets/images/logo-black.png") + attach_file "enterprise[logo]", Rails.root.join("app", "assets", "images", "logo-black.png") click_button "Update" expect(page).to have_content("Enterprise \"#{distributor.name}\" has been successfully updated!") @@ -50,11 +50,22 @@ feature "Managing enterprise images" do expect(page).to have_selector(".image-field-group__preview-image") expect(html).to include("logo-black.png") end + + # Removing image + within ".page-admin-enterprises-form__logo-field-group" do + click_on "Remove Image" + end + + expect(page).to have_content("Logo removed successfully") + + within ".page-admin-enterprises-form__logo-field-group" do + expect(page).to have_no_selector(".image-field-group__preview-image") + end end scenario "editing promo image" do # Adding image - attach_file "enterprise[promo_image]", File.join(Rails.root, "app/assets/images/logo-white.png") + attach_file "enterprise[promo_image]", Rails.root.join("app", "assets", "images", "logo-white.png") click_button "Update" expect(page).to have_content("Enterprise \"#{distributor.name}\" has been successfully updated!") @@ -66,7 +77,7 @@ feature "Managing enterprise images" do end # Replacing image - attach_file "enterprise[promo_image]", File.join(Rails.root, "app/assets/images/logo-black.png") + attach_file "enterprise[promo_image]", Rails.root.join("app", "assets", "images", "logo-black.png") click_button "Update" expect(page).to have_content("Enterprise \"#{distributor.name}\" has been successfully updated!") @@ -76,6 +87,17 @@ feature "Managing enterprise images" do expect(page).to have_selector(".image-field-group__preview-image") expect(html).to include("logo-black.jpg") end + + # Removing image + within ".page-admin-enterprises-form__promo-image-field-group" do + click_on "Remove Image" + end + + expect(page).to have_content("Promo image removed successfully") + + within ".page-admin-enterprises-form__promo-image-field-group" do + expect(page).to have_no_selector(".image-field-group__preview-image") + end end end end diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index bdbb7cde16..5b1f951e95 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -417,18 +417,6 @@ feature %q{ end end - scenario "editing images for an enterprise" do - visit admin_enterprises_path - within("tbody#e_#{distributor1.id}") { click_link 'Settings' } - - within(".side_menu") do - click_link "Images" - end - - page.should have_content "LOGO" - page.should have_content "PROMO" - end - scenario "managing producer properties" do create(:property, name: "Certified Organic") visit admin_enterprises_path diff --git a/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee index 17ef445846..8ea5d60837 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee @@ -4,6 +4,8 @@ describe "enterpriseCtrl", -> enterprise = null PaymentMethods = null ShippingMethods = null + Enterprises = null + StatusMessage = null beforeEach -> module('admin.enterprises') @@ -18,9 +20,11 @@ describe "enterpriseCtrl", -> shippingMethods: "shipping methods" receivesNotifications = 99 - inject ($rootScope, $controller) -> + inject ($rootScope, $controller, _Enterprises_, _StatusMessage_) -> scope = $rootScope - ctrl = $controller 'enterpriseCtrl', {$scope: scope, enterprise: enterprise, EnterprisePaymentMethods: PaymentMethods, EnterpriseShippingMethods: ShippingMethods, receivesNotifications: receivesNotifications} + Enterprises = _Enterprises_ + StatusMessage = _StatusMessage_ + ctrl = $controller "enterpriseCtrl", {$scope: scope, enterprise: enterprise, EnterprisePaymentMethods: PaymentMethods, EnterpriseShippingMethods: ShippingMethods, Enterprises: Enterprises, StatusMessage: StatusMessage, receivesNotifications: receivesNotifications} describe "initialisation", -> it "stores enterprise", -> @@ -32,6 +36,70 @@ describe "enterpriseCtrl", -> it "stores shipping methods", -> expect(scope.ShippingMethods).toBe ShippingMethods.shippingMethods + describe "removing logo", -> + deferred = null + + beforeEach inject ($q) -> + spyOn(scope, "$emit") + deferred = $q.defer() + spyOn(Enterprises, "removeLogo").and.returnValue(deferred.promise) + spyOn(StatusMessage, "display").and.callThrough() + scope.removeLogo() + + describe "when successful", -> + beforeEach inject ($rootScope) -> + deferred.resolve() + $rootScope.$digest() + + it "emits an 'enterprise:updated' event", -> + expect(scope.$emit).toHaveBeenCalledWith("enterprise:updated", scope.Enterprise) + + it "notifies user of success", -> + expect(StatusMessage.display).toHaveBeenCalledWith("success", "Logo removed successfully") + + describe "when unsuccessful", -> + beforeEach inject ($rootScope) -> + deferred.reject({ data: { error: "Logo does not exist" } }) + $rootScope.$digest() + + it "does not emit an 'enterprise:updated' event", -> + expect(scope.$emit).not.toHaveBeenCalled() + + it "notifies user of failure", -> + expect(StatusMessage.display).toHaveBeenCalledWith("failure", "Logo does not exist") + + describe "removing promo image", -> + deferred = null + + beforeEach inject ($q) -> + spyOn(scope, "$emit") + deferred = $q.defer() + spyOn(Enterprises, "removePromoImage").and.returnValue(deferred.promise) + spyOn(StatusMessage, "display").and.callThrough() + scope.removePromoImage() + + describe "when successful", -> + beforeEach inject ($rootScope) -> + deferred.resolve() + $rootScope.$digest() + + it "emits an 'enterprise:updated' event", -> + expect(scope.$emit).toHaveBeenCalledWith("enterprise:updated", scope.Enterprise) + + it "notifies user of success", -> + expect(StatusMessage.display).toHaveBeenCalledWith("success", "Promo image removed successfully") + + describe "when unsuccessful", -> + beforeEach inject ($rootScope) -> + deferred.reject({ data: { error: "Promo image does not exist" } }) + $rootScope.$digest() + + it "does not emit an 'enterprise:updated' event", -> + expect(scope.$emit).not.toHaveBeenCalled() + + it "notifies user of failure", -> + expect(StatusMessage.display).toHaveBeenCalledWith("failure", "Promo image does not exist") + describe "adding managers", -> u1 = u2 = u3 = null beforeEach -> diff --git a/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee index 85cf19f067..47c669e105 100644 --- a/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/services/enterprises_spec.js.coffee @@ -127,7 +127,7 @@ describe "Enterprises service", -> beforeEach -> enterprise = new EnterpriseResource({ id: 15, permalink: "enterprise1", name: "Enterprise 1", logo: {} }) - $httpBackend.expectDELETE("/admin/enterprises/enterprise1/images/remove_logo.json").respond 200, { id: 15, name: "Enterprise 1"} + $httpBackend.expectDELETE("/api/enterprises/enterprise1/logo.json").respond 200, { id: 15, name: "Enterprise 1"} Enterprises.removeLogo(enterprise).then( -> resolved = true) $httpBackend.flush() @@ -144,7 +144,7 @@ describe "Enterprises service", -> beforeEach -> enterprise = new EnterpriseResource( { id: 15, permalink: "enterprise1", name: "Enterprise 1" } ) - $httpBackend.expectDELETE("/admin/enterprises/enterprise1/images/remove_logo.json").respond 409, { error: "obj" } + $httpBackend.expectDELETE("/api/enterprises/enterprise1/logo.json").respond 409, { error: "obj" } Enterprises.removeLogo(enterprise).catch( -> rejected = true) $httpBackend.flush() @@ -162,7 +162,7 @@ describe "Enterprises service", -> beforeEach -> enterprise = new EnterpriseResource({ id: 15, permalink: "enterprise1", name: "Enterprise 1", promo_image: {} }) - $httpBackend.expectDELETE("/admin/enterprises/enterprise1/images/remove_promo_image.json").respond 200, { id: 15, name: "Enterprise 1"} + $httpBackend.expectDELETE("/api/enterprises/enterprise1/promo_image.json").respond 200, { id: 15, name: "Enterprise 1"} Enterprises.removePromoImage(enterprise).then( -> resolved = true) $httpBackend.flush() @@ -179,7 +179,7 @@ describe "Enterprises service", -> beforeEach -> enterprise = new EnterpriseResource( { id: 15, permalink: "enterprise1", name: "Enterprise 1" } ) - $httpBackend.expectDELETE("/admin/enterprises/enterprise1/images/remove_promo_image.json").respond 409, { error: "obj" } + $httpBackend.expectDELETE("/api/enterprises/enterprise1/promo_image.json").respond 409, { error: "obj" } Enterprises.removePromoImage(enterprise).catch( -> rejected = true) $httpBackend.flush() From ce0758d4209a58d8880c8a38905111e41e6a70a9 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Mon, 30 Jul 2018 08:39:37 +0800 Subject: [PATCH 7/8] Add confirm dialog for enterprise image removal --- .../enterprises/controllers/enterprise_controller.js.coffee | 4 ++++ config/locales/en.yml | 2 ++ .../controllers/enterprise_controller_spec.js.coffee | 2 ++ 3 files changed, 8 insertions(+) diff --git a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee index a6b0a40958..4acd61e84b 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -69,6 +69,8 @@ angular.module("admin.enterprises") $scope.newUser = $scope.invite_errors = $scope.invite_success = null $scope.removeLogo = -> + return unless confirm(t("admin.enterprises.remove_logo.immediate_removal_warning")) + Enterprises.removeLogo($scope.Enterprise).then (data) -> $scope.Enterprise = angular.copy(data) $scope.$emit("enterprise:updated", $scope.Enterprise) @@ -79,6 +81,8 @@ angular.module("admin.enterprises") StatusMessage.display("failure", response.data.error) $scope.removePromoImage = -> + return unless confirm(t("admin.enterprises.remove_promo_image.immediate_removal_warning")) + Enterprises.removePromoImage($scope.Enterprise).then (data) -> $scope.Enterprise = angular.copy(data) $scope.$emit("enterprise:updated", $scope.Enterprise) diff --git a/config/locales/en.yml b/config/locales/en.yml index 677b3b9d2f..bfb9b8486d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -834,9 +834,11 @@ en: remove_logo: remove: "Remove Image" removed_successfully: "Logo removed successfully" + immediate_removal_warning: "The logo will be removed immediately after you confirm." remove_promo_image: remove: "Remove Image" removed_successfully: "Promo image removed successfully" + immediate_removal_warning: "The promo image will be removed immediately after you confirm." welcome: welcome_title: Welcome to the Open Food Network! welcome_text: You have successfully created a diff --git a/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee index 8ea5d60837..475de27e29 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/enterprise_controller_spec.js.coffee @@ -42,6 +42,7 @@ describe "enterpriseCtrl", -> beforeEach inject ($q) -> spyOn(scope, "$emit") deferred = $q.defer() + spyOn(window, "confirm").and.returnValue(true) spyOn(Enterprises, "removeLogo").and.returnValue(deferred.promise) spyOn(StatusMessage, "display").and.callThrough() scope.removeLogo() @@ -74,6 +75,7 @@ describe "enterpriseCtrl", -> beforeEach inject ($q) -> spyOn(scope, "$emit") deferred = $q.defer() + spyOn(window, "confirm").and.returnValue(true) spyOn(Enterprises, "removePromoImage").and.returnValue(deferred.promise) spyOn(StatusMessage, "display").and.callThrough() scope.removePromoImage() From 02a909c1b1f500277fa9c3c5366fdc8c34ddb8f2 Mon Sep 17 00:00:00 2001 From: Kristina Lim Date: Sat, 8 Sep 2018 23:10:06 +0800 Subject: [PATCH 8/8] Do not use image_tag for enterprise image previews If Rails config.assets.compile is false and config.assets.digest is true, which is the case for staging and production, image_tag fails when the image file is not found. We do not need sprockets to ensure presence of the image file for these tags, because the correct SRC values are assigned through the JS. Inserting HTML should be sufficient. --- app/views/admin/enterprises/form/_images.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/admin/enterprises/form/_images.html.haml b/app/views/admin/enterprises/form/_images.html.haml index d44e619cf6..0f48fce322 100644 --- a/app/views/admin/enterprises/form/_images.html.haml +++ b/app/views/admin/enterprises/form/_images.html.haml @@ -4,7 +4,7 @@ %br 100 x 100 pixels .omega.eight.columns - = image_tag '', class: 'image-field-group__preview-image', 'ng-src' => '{{ Enterprise.logo.medium }}', 'ng-if' => 'Enterprise.logo' + %img{ class: 'image-field-group__preview-image', ng: { src: '{{ Enterprise.logo.medium }}', if: 'Enterprise.logo' } } = f.file_field :logo %a.button.red{ href: '', ng: {click: 'removeLogo()', if: 'Enterprise.logo'} } = t('admin.enterprises.remove_logo.remove') @@ -20,7 +20,7 @@ = t('.promo_image_note3') .omega.eight.columns - = image_tag '', class: 'image-field-group__preview-image', 'ng-src' => '{{ Enterprise.promo_image.large }}', 'ng-if' => 'Enterprise.promo_image' + %img{ class: 'image-field-group__preview-image', ng: { src: '{{ Enterprise.promo_image.large }}', if: 'Enterprise.promo_image' } } = f.file_field :promo_image %a.button.red{ href: '', ng: {click: 'removePromoImage()', if: 'Enterprise.promo_image'} } = t('admin.enterprises.remove_promo_image.remove')