diff --git a/Gemfile b/Gemfile index 456234f554..2e03d65a35 100644 --- a/Gemfile +++ b/Gemfile @@ -42,6 +42,7 @@ gem 'gmaps4rails' gem 'spinjs-rails' gem 'rack-ssl', :require => 'rack/ssl' gem 'custom_error_message', :github => 'jeremydurham/custom-err-msg' +gem 'angularjs-file-upload-rails', '~> 1.1.0' gem 'foreigner' gem 'immigrant' diff --git a/Gemfile.lock b/Gemfile.lock index 9bb6e501db..c1a2c66522 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -153,6 +153,7 @@ GEM railties (>= 3.1) sprockets tilt + angularjs-file-upload-rails (1.1.0) angularjs-rails (1.2.13) ansi (1.4.2) arel (3.0.3) @@ -505,6 +506,7 @@ DEPENDENCIES active_model_serializers andand angular-rails-templates + angularjs-file-upload-rails (~> 1.1.0) angularjs-rails awesome_print aws-sdk diff --git a/app/assets/javascripts/darkswarm/all.js.coffee b/app/assets/javascripts/darkswarm/all.js.coffee index f529ac3255..45acfd6523 100644 --- a/app/assets/javascripts/darkswarm/all.js.coffee +++ b/app/assets/javascripts/darkswarm/all.js.coffee @@ -15,6 +15,8 @@ #= require ../shared/bindonce.min.js #= require ../shared/ng-infinite-scroll.min.js #= require ../shared/angular-local-storage.js +#= require angularjs-file-upload + #= require angular-rails-templates #= require_tree ../templates diff --git a/app/assets/javascripts/darkswarm/controllers/enterprise_image_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/enterprise_image_controller.js.coffee new file mode 100644 index 0000000000..e12d55b50c --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/enterprise_image_controller.js.coffee @@ -0,0 +1,13 @@ +angular.module('Darkswarm').controller "EnterpriseImageCtrl", ($scope, EnterpriseImageService) -> + $scope.imageStep = 'logo' + + $scope.imageSteps = ['logo', 'promo'] + + $scope.imageUploader = EnterpriseImageService.imageUploader + + $scope.imageSelect = (image_step) -> + EnterpriseImageService.imageSrc = null + $scope.imageStep = image_step + + $scope.imageSrc = -> + EnterpriseImageService.imageSrc diff --git a/app/assets/javascripts/darkswarm/controllers/registration_form_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/registration_form_controller.js.coffee index 84f133da54..fabc2c382a 100644 --- a/app/assets/javascripts/darkswarm/controllers/registration_form_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/registration_form_controller.js.coffee @@ -12,4 +12,4 @@ Darkswarm.controller "RegistrationFormCtrl", ($scope, RegistrationService, Enter EnterpriseRegistrationService.update(nextStep) if $scope.valid(form) $scope.selectIfValid = (nextStep, form) -> - RegistrationService.select(nextStep) if $scope.valid(form) \ No newline at end of file + RegistrationService.select(nextStep) if $scope.valid(form) diff --git a/app/assets/javascripts/darkswarm/darkswarm.js.coffee b/app/assets/javascripts/darkswarm/darkswarm.js.coffee index 1e58fe7294..e8ea9dce3c 100644 --- a/app/assets/javascripts/darkswarm/darkswarm.js.coffee +++ b/app/assets/javascripts/darkswarm/darkswarm.js.coffee @@ -1,18 +1,19 @@ -window.Darkswarm = angular.module("Darkswarm", ["ngResource", - 'mm.foundation', - 'angularLocalStorage', - 'pasvaz.bindonce', - 'infinite-scroll', - 'angular-flash.service', +window.Darkswarm = angular.module("Darkswarm", ["ngResource", + 'mm.foundation', + 'angularLocalStorage', + 'pasvaz.bindonce', + 'infinite-scroll', + 'angular-flash.service', 'templates', 'ngSanitize', 'ngAnimate', 'google-maps', 'duScroll', + 'angularFileUpload', ]).config ($httpProvider, $tooltipProvider, $locationProvider, $anchorScrollProvider) -> - $httpProvider.defaults.headers.post['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content') - $httpProvider.defaults.headers.put['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content') - $httpProvider.defaults.headers['common']['X-Requested-With'] = 'XMLHttpRequest' + $httpProvider.defaults.headers.post['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content') + $httpProvider.defaults.headers.put['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content') + $httpProvider.defaults.headers['common']['X-Requested-With'] = 'XMLHttpRequest' $httpProvider.defaults.headers.common.Accept = "application/json, text/javascript, */*" # This allows us to trigger these two events on tooltips @@ -20,4 +21,3 @@ window.Darkswarm = angular.module("Darkswarm", ["ngResource", # We manually handle our scrolling $anchorScrollProvider.disableAutoScrolling() - diff --git a/app/assets/javascripts/darkswarm/services/enterprise_image_service.js.coffee b/app/assets/javascripts/darkswarm/services/enterprise_image_service.js.coffee new file mode 100644 index 0000000000..fbefabcfa8 --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/enterprise_image_service.js.coffee @@ -0,0 +1,13 @@ +Darkswarm.factory "EnterpriseImageService", (EnterpriseRegistrationService, FileUploader, spreeApiKey) -> + new class EnterpriseImageService + imageSrc: null + + imageUploader: new FileUploader + headers: + 'X-Spree-Token': spreeApiKey + url: "/api/enterprises/#{EnterpriseRegistrationService.enterprise.id}/update_image" + autoUpload: true + + constructor: -> + @imageUploader.onSuccessItem = (image, response) => + @imageSrc = response diff --git a/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee b/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee index 68915193ee..5aa2f3e8bf 100644 --- a/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee +++ b/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee @@ -54,4 +54,4 @@ Darkswarm.factory "EnterpriseRegistrationService", ($http, RegistrationService, enterprise[key] = value enterprise.address_attributes = @enterprise.address if @enterprise.address? enterprise.address_attributes.country_id = @enterprise.country.id if @enterprise.country? - enterprise \ No newline at end of file + enterprise diff --git a/app/assets/javascripts/darkswarm/services/registration_service.js.coffee b/app/assets/javascripts/darkswarm/services/registration_service.js.coffee index a2a1fe2dc4..530d118025 100644 --- a/app/assets/javascripts/darkswarm/services/registration_service.js.coffee +++ b/app/assets/javascripts/darkswarm/services/registration_service.js.coffee @@ -1,4 +1,4 @@ -Darkswarm.factory "RegistrationService", (Navigation, $modal, Loading)-> +angular.module('Darkswarm').factory "RegistrationService", (Navigation, $modal, Loading)-> new class RegistrationService constructor: -> @@ -20,4 +20,4 @@ Darkswarm.factory "RegistrationService", (Navigation, $modal, Loading)-> close: -> Loading.message = "Taking you back to the home page" - Navigation.go "/" \ No newline at end of file + Navigation.go "/" diff --git a/app/assets/javascripts/templates/registration/about.html.haml b/app/assets/javascripts/templates/registration/about.html.haml index 07e631345f..57f7c482e6 100644 --- a/app/assets/javascripts/templates/registration/about.html.haml +++ b/app/assets/javascripts/templates/registration/about.html.haml @@ -9,7 +9,7 @@ {{ enterprise.name }} %ng-include{ src: "'registration/steps.html'" } - %form{ name: 'about', novalidate: true, ng: { controller: "RegistrationFormCtrl", submit: "update('social',about)" } } + %form{ name: 'about', novalidate: true, ng: { controller: "RegistrationFormCtrl", submit: "update('images',about)" } } .row .small-12.columns .alert-box.alert{"data-alert" => ""} diff --git a/app/assets/javascripts/templates/registration/images.html.haml b/app/assets/javascripts/templates/registration/images.html.haml index efd3688076..889bc4596f 100644 --- a/app/assets/javascripts/templates/registration/images.html.haml +++ b/app/assets/javascripts/templates/registration/images.html.haml @@ -1,14 +1,20 @@ -.container#registration-images +.container#registration-images{ 'nv-file-drop' => true, uploader: "imageUploader", options:"{ alias: imageStep }", ng: { controller: "EnterpriseImageCtrl" } } .header %h2 Thanks! %h5 Let's upload some pretty pictures so your profile looks great! :) %ng-include{ src: "'registration/steps.html'" } - .row.content + %form{ name: 'images', novalidate: true, ng: { controller: "RegistrationFormCtrl", submit: "select('social')" } } + .row{ ng: { repeat: 'image_step in imageSteps', show: "imageStep == image_step" } } + %ng-include{ src: "'registration/images/'+ image_step + '.html'" } - .row.buttons - .small-12.columns - %input.button.primary.left{ type: "button", value: "Back", ng: { click: "select('about')" } } -   - %input.button.primary.right{ type: "button", value: "Continue", ng: { click: "select('social')" } } - - \ No newline at end of file + .row.buttons.pad-top{ ng: { if: "imageStep == 'logo'" } } + .small-12.columns + %input.button.primary{ type: "button", value: "Back", ng: { click: "select('about')" } } +   + %input.button.primary{ type: "button", value: "Continue", ng: { click: "imageSelect('promo')" } } + + .row.buttons.pad-top{ ng: { if: "imageStep == 'promo'" } } + .small-12.columns + %input.button.primary{ type: "button", value: "Back", ng: { click: "imageSelect('logo')" } } +   + %input.button.primary{ type: "submit", value: "Continue" } diff --git a/app/assets/javascripts/templates/registration/images/logo.html.haml b/app/assets/javascripts/templates/registration/images/logo.html.haml new file mode 100644 index 0000000000..803b103a58 --- /dev/null +++ b/app/assets/javascripts/templates/registration/images/logo.html.haml @@ -0,0 +1,41 @@ +.small-12.medium-12.large-6.columns + .row + .small-12.columns.center + .row + .small-12.columns.center + %h4 + Step 1. Select Logo Image + .row + .small-12.columns.center + %span.small + Tip: Square images will work best, preferably at least 300×300px + .row.pad-top + .small-12.columns + .image-select.small-12.columns + %label.small-12.columns.button{ for: 'image-select' } Choose a logo image + %input#image-select{ type: 'file', hidden: true, 'nv-file-select' => true, uploader: "imageUploader", options: '{ alias: imageStep }' } + .row.show-for-large-up + .large-12.columns + %span#or.large-12.columns + OR + .row.show-for-large-up + .large-12.columns + #image-over{ 'nv-file-over' => true, uploader: "imageUploader" } + Drag and drop your logo here +.small-12.medium-12.large-6.columns + .row + .small-12.columns.center + .row + .small-12.columns.center + %h4 + Step 2. Review Your Logo + .row + .small-12.columns.center + %span.small + Tip: for best results, your logo should fill the available space + .row.pad-top + .small-12.columns.center + #image-placeholder.logo + %img{ ng: { show: "imageSrc()", src: '{{ imageSrc() }}' } } + .message{ ng: { hide: "imageSrc()" } } + Your logo will appear here for review once uploaded diff --git a/app/assets/javascripts/templates/registration/images/promo.html.haml b/app/assets/javascripts/templates/registration/images/promo.html.haml new file mode 100644 index 0000000000..f134834d5c --- /dev/null +++ b/app/assets/javascripts/templates/registration/images/promo.html.haml @@ -0,0 +1,39 @@ +.small-12.medium-12.large-12.columns + .row + .small-12.columns.center + %h4 + Step 3. Select Promo Image + .row + .small-12.medium-12.large-5.columns.center + .row + .small-12.columns.center + %span.small + Tip: Shown as a banner, preferred size is 1200×260px + .row.pad-top + .small-12.columns + .image-select.small-12.columns + %label.small-12.columns.button{ for: 'image-select' } Choose a promo image + %input#image-select{ type: 'file', hidden: true, 'nv-file-select' => true, uploader: "imageUploader", options: '{ alias: imageStep }' } + .large-2.columns + %span#or.horizontal.large-12.columns + OR + .large-5.columns + #image-over{ 'nv-file-over' => true, uploader: "imageUploader" } + Drag and drop your promo here +.small-12.medium-12.large-12.columns.pad-top + .row + .small-12.columns.center + %h4 + Step 4. Review Your Promo Banner + .row + .small-12.columns.center + .row + .small-12.columns.center + %span.small + Tip: for best results, your promo image should fill the available space + .row.pad-top + .small-12.columns.center + #image-placeholder.promo + %img{ ng: { show: "imageSrc()", src: '{{ imageSrc() }}' } } + .message{ ng: { hide: "imageSrc()" } } + Your logo will appear here for review once uploaded diff --git a/app/assets/javascripts/templates/registration/introduction.html.haml b/app/assets/javascripts/templates/registration/introduction.html.haml index 8a4f4f7e02..ccefdefbe0 100644 --- a/app/assets/javascripts/templates/registration/introduction.html.haml +++ b/app/assets/javascripts/templates/registration/introduction.html.haml @@ -6,35 +6,34 @@ .small-12.medium-3.large-2.columns.text-right.hide-for-small-only %img{:src => "/assets/potatoes.png"} .small-12.medium-9.large-10.columns - %p - Your profile gives you an online presence on the - %strong Open Food Network, + %p + Your profile gives you an online presence on the + %strong Open Food Network, allowing you to easily connect with potential customers or partners. You can always choose to update your info later, as well as choose to upgrade your Profile to and Online Store, where you can sell products, track orders and receive payments. Creating a profile takes about 5-10 minutes. .row{ 'data-equalizer' => true } .small-12.medium-6.large-6.columns.pad-top{ 'data-equalizer-watch' => true } %h5 You'll need the following: %ul.check-list - %li + %li Your enterprise address and contact details - %li + %li Your logo image - %li + %li A pretty picture for your profile header - %li + %li Some 'About Us' text .small-12.medium-6.large-6.columns{ 'data-equalizer-watch' => true} .highlight-box %h5 Your profile entitles you to: %ul.small-block-grid-1 - %li + %li %i.ofn-i_020-search A searchable listing - %li + %li %i.ofn-i_040-hub A pin on the OFN map .row .small-12.columns %hr %input.button.primary{ type: "button", value: "Let's get started!", ng: { click: "select('details')" } } - \ No newline at end of file diff --git a/app/assets/javascripts/templates/registration/social.html.haml b/app/assets/javascripts/templates/registration/social.html.haml index 2aa12bd08b..1b3490ffa0 100644 --- a/app/assets/javascripts/templates/registration/social.html.haml +++ b/app/assets/javascripts/templates/registration/social.html.haml @@ -30,6 +30,6 @@ .row.buttons .small-12.columns - %input.button.secondary{ type: "button", value: "Back", ng: { click: "select('about')" } } + %input.button.secondary{ type: "button", value: "Back", ng: { click: "select('images')" } }   - %input.button.primary{ type: "submit", value: "Continue" } \ No newline at end of file + %input.button.primary{ type: "submit", value: "Continue" } diff --git a/app/assets/stylesheets/darkswarm/registration.css.sass b/app/assets/stylesheets/darkswarm/registration.css.sass index 3e851709bc..3261fc0fd4 100644 --- a/app/assets/stylesheets/darkswarm/registration.css.sass +++ b/app/assets/stylesheets/darkswarm/registration.css.sass @@ -80,6 +80,53 @@ color: #333 @include box-shadow(inset 0 0 1px 0 #fff) + + .image-select + label + font-size: 18px + padding: 21px 0px + #logo-select + display: none + + #image-over + font-size: 18px + padding: 41px 0px + border: 3px dashed #494949 + text-align: center + font-weight: bold + color: #494949 + &.nv-file-over + background-color: #78cd91 + + #or + text-align: center + font-weight: bold + font-size: 18px + padding: 21px 0px + &.horizontal + padding: 41px 0px + + #image-placeholder + font-size: 18px + font-weight: bold + color: #373737 + background-color: #e1e1e1 + text-align: center + border: 3px dashed #494949 + margin-left: auto + margin-right: auto + &.logo + .message + padding-top: 6em + width: 306px + height: 306px + &.promo + .message + padding-top: 4em + width: 726px + height: 166px + + #registration-details #enterprise-types a.panel @@ -112,4 +159,3 @@ p clear: both font-size: 0.875rem - diff --git a/app/controllers/api/enterprises_controller.rb b/app/controllers/api/enterprises_controller.rb index 9a3b715093..76320b0750 100644 --- a/app/controllers/api/enterprises_controller.rb +++ b/app/controllers/api/enterprises_controller.rb @@ -27,9 +27,9 @@ module Api end def update - authorize! :update, Enterprise - @enterprise = Enterprise.find(params[:id]) + authorize! :update, @enterprise + if @enterprise.update_attributes(params[:enterprise]) render text: @enterprise.id, :status => 200 else @@ -37,6 +37,19 @@ module Api end end + def update_image + @enterprise = Enterprise.find(params[:id]) + authorize! :update, @enterprise + + if params[:logo] && @enterprise.update_attributes( { logo: params[:logo] } ) + render text: @enterprise.logo.url(:medium), :status => 200 + elsif params[:promo] && @enterprise.update_attributes( { promo_image: params[:promo] } ) + render text: @enterprise.promo_image.url(:medium), :status => 200 + else + invalid_resource!(@enterprise) + end + end + private def override_owner diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 537ac78295..e75d18dae9 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -34,7 +34,7 @@ class Enterprise < ActiveRecord::Base path: 'public/images/enterprises/logos/:id/:style/:basename.:extension' has_attached_file :promo_image, - styles: { large: "1200x260#", thumb: "100x100>" }, + styles: { large: "1200x260#", medium: "720x156#", thumb: "100x100>" }, url: '/images/enterprises/promo_images/:id/:style/:basename.:extension', path: 'public/images/enterprises/promo_images/:id/:style/:basename.:extension' diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 79019f3e8a..5ded33f463 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -71,7 +71,7 @@ class AbilityDecorator can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Shipment can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::Adjustment can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::ReturnAuthorization - + can [:create], OrderCycle can [:admin, :index, :read, :edit, :update, :bulk_update, :clone], OrderCycle do |order_cycle| user.enterprises.include? order_cycle.coordinator diff --git a/config/routes.rb b/config/routes.rb index f95b0c07df..3e063641dd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -67,6 +67,7 @@ Openfoodnetwork::Application.routes.draw do namespace :api do resources :enterprises do + post :update_image, on: :member get :managed, on: :collection get :accessible, on: :collection end diff --git a/spec/controllers/api/enterprises_controller_spec.rb b/spec/controllers/api/enterprises_controller_spec.rb new file mode 100644 index 0000000000..e21a879d4f --- /dev/null +++ b/spec/controllers/api/enterprises_controller_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +module Api + describe EnterprisesController do + include AuthenticationWorkflow + render_views + + let(:enterprise) { create(:distributor_enterprise) } + + before do + stub_authentication! + Enterprise.stub(:find).and_return(enterprise) + end + + describe "as an enterprise manager" do + let(:enterprise_manager) { create_enterprise_user } + + before do + enterprise_manager.enterprise_roles.build(enterprise: enterprise).save + Spree.user_class.stub :find_by_spree_api_key => enterprise_manager + end + + describe "submitting a valid image" do + before do + enterprise.stub(:update_attributes).and_return(true) + end + + it "I can update enterprise image" do + spree_post :update_image, logo: 'a logo' + response.should be_success + end + end + end + + describe "as an non-managing user" do + let(:non_managing_user) { create_enterprise_user } + + before do + Spree.user_class.stub :find_by_spree_api_key => non_managing_user + end + + describe "submitting a valid image" do + before do + enterprise.stub(:update_attributes).and_return(true) + end + + it "I can't update enterprise image" do + spree_post :update_image, logo: 'a logo' + assert_unauthorized! + end + end + end + end +end diff --git a/spec/features/consumer/registration_spec.rb b/spec/features/consumer/registration_spec.rb index 3500736100..7ac46a8970 100644 --- a/spec/features/consumer/registration_spec.rb +++ b/spec/features/consumer/registration_spec.rb @@ -60,15 +60,22 @@ feature "Registration", js: true do fill_in 'enterprise_acn', with: '54321' click_button 'Continue' - # Enterprise should be updated - expect(page).to have_content 'Last step!' + # Enterprise should be update + expect(page).to have_content "Let's upload some pretty pictures so your profile looks great!" e.reload expect(e.description).to eq "Short description" expect(e.long_description).to eq "Long description" expect(e.abn).to eq '12345' expect(e.acn).to eq '54321' + # Images + # Move from logo page + click_button 'Continue' + # Move from promo page + click_button 'Continue' + # Filling in social + expect(page).to have_content 'Last step!' fill_in 'enterprise_website', with: 'www.shop.com' fill_in 'enterprise_facebook', with: 'FaCeBoOk' fill_in 'enterprise_linkedin', with: 'LiNkEdIn' 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 b1b019276b..8f6bd64a5f 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 @@ -7,7 +7,7 @@ describe "enterpriseCtrl", -> beforeEach -> module('admin.enterprises') - Enterprise = + Enterprise = enterprise: payment_method_ids: [ 1, 3 ] shipping_method_ids: [ 2, 4 ] @@ -15,7 +15,7 @@ describe "enterpriseCtrl", -> paymentMethods: [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 } ] ShippingMethods = shippingMethods: [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 } ] - + inject ($controller) -> scope = {} ctrl = $controller 'enterpriseCtrl', {$scope: scope, Enterprise: Enterprise, PaymentMethods: PaymentMethods, ShippingMethods: ShippingMethods} @@ -82,4 +82,4 @@ describe "enterpriseCtrl", -> describe "counting selected shipping methods", -> it "counts only shipping methods with selected: true", -> scope.ShippingMethods = [ { selected: true }, { selected: true }, { selected: false }, { selected: true } ] - expect(scope.selectedShippingMethodsCount()).toBe 3 \ No newline at end of file + expect(scope.selectedShippingMethodsCount()).toBe 3