diff --git a/Gemfile.lock b/Gemfile.lock index 1ac2fa34fb..0fef1c1d4b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -640,7 +640,7 @@ GEM shellany (0.0.1) shoulda-matchers (2.8.0) activesupport (>= 3.0.0) - skylight (1.5.0) + skylight (1.5.1) activesupport (>= 3.0.0) spinjs-rails (1.3) rails (>= 3.1) diff --git a/app/assets/javascripts/admin/admin_ofn.js.coffee b/app/assets/javascripts/admin/admin_ofn.js.coffee index d617722175..1a5e1c79bf 100644 --- a/app/assets/javascripts/admin/admin_ofn.js.coffee +++ b/app/assets/javascripts/admin/admin_ofn.js.coffee @@ -1,3 +1,14 @@ -angular.module("ofn.admin", ["ngResource", "ngAnimate", "admin.utils", "admin.indexUtils", "admin.dropdown", "admin.products", "admin.taxons", "infinite-scroll"]).config ($httpProvider) -> +angular.module("ofn.admin", [ + "ngResource", + "mm.foundation", + "angularFileUpload", + "ngAnimate", + "admin.utils", + "admin.indexUtils", + "admin.dropdown", + "admin.products", + "admin.taxons", + "infinite-scroll" +]).config ($httpProvider) -> $httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content") $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*" diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 366f7344e7..688494f870 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -51,7 +51,6 @@ //= require textAngular.min.js //= require i18n/translations //= require darkswarm/i18n.translate.js -// //= require moment //= require moment/en-gb.js //= require moment/es.js @@ -60,5 +59,7 @@ //= require moment/nb.js //= require moment/pt-br.js //= require moment/sv.js +//= require ../shared/mm-foundation-tpls-0.8.0.min.js +//= require angularjs-file-upload //= require_tree . diff --git a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee index 2506e994ac..d4ba6383d8 100644 --- a/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee +++ b/app/assets/javascripts/admin/customers/controllers/customers_controller.js.coffee @@ -12,7 +12,7 @@ angular.module("admin.customers").controller "customersCtrl", ($scope, $q, $filt $scope.$watch "shop_id", -> if $scope.shop_id? - CurrentShop.shop = $filter('filter')($scope.shops, {id: $scope.shop_id})[0] + CurrentShop.shop = $filter('filter')($scope.shops, {id: parseInt($scope.shop_id)}, true)[0] Customers.index({enterprise_id: $scope.shop_id}).then (data) -> pendingChanges.removeAll() $scope.customers_form.$setPristine() 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 cd1da7b447..2d1dc494f4 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/enterprise_controller.js.coffee @@ -5,7 +5,7 @@ angular.module("admin.enterprises") $scope.ShippingMethods = EnterpriseShippingMethods.shippingMethods $scope.navClear = NavigationCheck.clear $scope.menu = SideMenu - $scope.newManager = { id: '', email: (t('add_manager')) } + $scope.newManager = { id: null, email: (t('add_manager')) } $scope.StatusMessage = StatusMessage $scope.$watch 'enterprise_form.$dirty', (newValue) -> @@ -38,17 +38,17 @@ angular.module("admin.enterprises") return for i, user of $scope.Enterprise.users when user.id == manager.id $scope.Enterprise.users.splice i, 1 - if $scope.enterprise_form? - $scope.enterprise_form.$setDirty() + $scope.enterprise_form?.$setDirty() $scope.addManager = (manager) -> - if manager.id? and manager.email? + if manager.id? and angular.isNumber(manager.id) and manager.email? manager = id: manager.id email: manager.email confirmed: manager.confirmed if (user for user in $scope.Enterprise.users when user.id == manager.id).length == 0 $scope.Enterprise.users.push manager + $scope.enterprise_form?.$setDirty() else alert ("#{manager.email}" + " " + t("is_already_manager")) diff --git a/app/assets/javascripts/admin/products/controllers/product_image_controller.js.coffee b/app/assets/javascripts/admin/products/controllers/product_image_controller.js.coffee new file mode 100644 index 0000000000..1fb2ceea06 --- /dev/null +++ b/app/assets/javascripts/admin/products/controllers/product_image_controller.js.coffee @@ -0,0 +1,6 @@ +angular.module("ofn.admin").controller "ProductImageCtrl", ($scope, ProductImageService) -> + $scope.imageUploader = ProductImageService.imageUploader + $scope.imagePreview = ProductImageService.imagePreview + + $scope.$watch 'product.image_url', (newValue) -> + $scope.imagePreview = newValue if newValue diff --git a/app/assets/javascripts/admin/products/directives/image_modal.js.coffee b/app/assets/javascripts/admin/products/directives/image_modal.js.coffee new file mode 100644 index 0000000000..526c14bf2a --- /dev/null +++ b/app/assets/javascripts/admin/products/directives/image_modal.js.coffee @@ -0,0 +1,6 @@ +angular.module("ofn.admin").directive "imageModal", ($modal, ProductImageService) -> + restrict: 'C' + link: (scope, elem, attrs, ctrl) -> + elem.on "click", (ev) => + scope.uploadModal = $modal.open(templateUrl: 'admin/modals/image_upload.html', controller: ctrl, scope: scope, windowClass: 'product-image-upload') + ProductImageService.configure(scope.product) diff --git a/app/assets/javascripts/admin/products/services/product_image_service.js.coffee b/app/assets/javascripts/admin/products/services/product_image_service.js.coffee new file mode 100644 index 0000000000..8b0f273e8b --- /dev/null +++ b/app/assets/javascripts/admin/products/services/product_image_service.js.coffee @@ -0,0 +1,15 @@ +angular.module("ofn.admin").factory "ProductImageService", (FileUploader, SpreeApiKey) -> + new class ProductImageService + imagePreview: null + + imageUploader: new FileUploader + headers: + 'X-Spree-Token': SpreeApiKey + autoUpload: true + + configure: (product) => + @imageUploader.url = "/api/product_images/#{product.id}" + @imagePreview = product.image_url + @imageUploader.onSuccessItem = (image, response) => + product.thumb_url = response.thumb_url + product.image_url = response.image_url diff --git a/app/assets/javascripts/darkswarm/darkswarm.js.coffee b/app/assets/javascripts/darkswarm/darkswarm.js.coffee index 6d75916894..01a93a3dd1 100644 --- a/app/assets/javascripts/darkswarm/darkswarm.js.coffee +++ b/app/assets/javascripts/darkswarm/darkswarm.js.coffee @@ -1,4 +1,5 @@ -window.Darkswarm = angular.module("Darkswarm", ["ngResource", +window.Darkswarm = angular.module("Darkswarm", [ + 'ngResource', 'mm.foundation', 'LocalStorageModule', 'infinite-scroll', @@ -10,7 +11,7 @@ window.Darkswarm = angular.module("Darkswarm", ["ngResource", 'duScroll', 'angularFileUpload', 'angularSlideables' - ]).config ($httpProvider, $tooltipProvider, $locationProvider, $anchorScrollProvider) -> +]).config ($httpProvider, $tooltipProvider, $locationProvider, $anchorScrollProvider) -> $httpProvider.defaults.headers['common']['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, */*" diff --git a/app/assets/javascripts/templates/admin/modals/image_upload.html.haml b/app/assets/javascripts/templates/admin/modals/image_upload.html.haml new file mode 100644 index 0000000000..76a96805ef --- /dev/null +++ b/app/assets/javascripts/templates/admin/modals/image_upload.html.haml @@ -0,0 +1,10 @@ +%a.close-reveal-modal{"ng-click" => "$close()"} + %i.fa.fa-times-circle{'aria-hidden' => "true"} + +%form#image_upload{ name: 'form', novalidate: true, enctype: 'multipart/form-data', multipart: true, ng: { controller: "ProductImageCtrl" } } + %div.image-preview + %img.spinner{ src: "/assets/spinning-circles.svg", ng: { hide: "!imageUploader.isUploading" }} + %img.preview{ng: {src: "{{imagePreview}}", class: "{'faded': imageUploader.isUploading}"}} + + %label{for: 'image-upload', class: 'button'} #{t('admin.products.bulk_edit.upload_an_image')} + %input#image-upload{hidden: true, type: 'file', 'nv-file-select' => true, uploader: "imageUploader"} diff --git a/app/assets/stylesheets/admin/modals.css.scss b/app/assets/stylesheets/admin/modals.css.scss new file mode 100644 index 0000000000..a543f15ee3 --- /dev/null +++ b/app/assets/stylesheets/admin/modals.css.scss @@ -0,0 +1,198 @@ +.reveal-modal-bg { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: black; + z-index: 1004; + display: none; + left: 0; +} + +.reveal-modal, dialog { + visibility: hidden; + display: none; + position: absolute; + z-index: 1005; + width: 100vw; + top: 0; + border-radius: 0.4em; + border: 0px none; + left: 0; + background-color: white; + padding: 1.25rem; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.4); + padding: 1.875rem; +} + +.reveal-modal .column, dialog .column, .reveal-modal .columns, dialog .columns { + min-width: 0; +} + +.reveal-modal > :first-child, dialog > :first-child { + margin-top: 0; +} + +.reveal-modal > :last-child, dialog > :last-child { + margin-bottom: 0; +} + +@media only screen and (min-width: 40.063em) { + .reveal-modal, dialog { + width: 80%; + max-width: 62.5rem; + left: 0; + right: 0; + margin: 0 auto; + } +} + +.reveal-modal.radius, dialog.radius { + border-radius: 3px; +} + +.reveal-modal.round, dialog.round { + border-radius: 1000px; +} + +.reveal-modal.collapse, dialog.collapse { + padding: 0; +} + +.reveal-modal.full, dialog.full { + top: 0; + left: 0; + height: 100%; + height: 100vh; + min-height: 100vh; + max-width: none !important; + margin-left: 0 !important; +} + +.reveal-modal .close-reveal-modal, dialog .close-reveal-modal { + font-size: 2.5rem; + line-height: 1; + position: absolute; + top: 0.625rem; + right: 1.375rem; + color: #aaaaaa; + font-weight: bold; + cursor: pointer; +} + +dialog::backdrop, dialog + .backdrop { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: black; + background: rgba(0, 0, 0, 0.45); + z-index: auto; + display: none; + left: 0; +} + +dialog[open] { + display: block; +} + +@media print { + dialog, .reveal-modal, dialog { + display: none; + background: white !important; + } +} + +// ANIMATION CLASSES + +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} + +.fade.in { + opacity: 1; +} + +.reveal-modal.fade { + -webkit-transition: -webkit-transform 0.2s ease-out; + -moz-transition: -moz-transform 0.2s ease-out; + -o-transition: -o-transform 0.2s ease-out; + transition: transform 0.2s ease-out; + -webkit-transform: translate(0, -25%); + -ms-transform: translate(0, -25%); + transform: translate(0, -25%); +} + +.reveal-modal.in { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); +} + +.reveal-modal-bg.fade { + filter: alpha(opacity = 0); + opacity: 0; +} + +.reveal-modal-bg.in { + filter: alpha(opacity = 50); + opacity: 0.5; +} + +@media only screen and (max-width: 40em) { + .reveal-modal, dialog { + min-height: 100vh; + } +} + +@media only screen and (min-width: 40.063em) { + .reveal-modal, dialog { + top: 6.25rem; + } + .reveal-modal.tiny, dialog.tiny { + width: 30%; + max-width: 62.5rem; + left: 0; + right: 0; + margin: 0 auto; + } + .reveal-modal.small, dialog.small { + width: 40%; + max-width: 62.5rem; + left: 0; + right: 0; + margin: 0 auto; + } + .reveal-modal.medium, dialog.medium { + width: 60%; + max-width: 62.5rem; + left: 0; + right: 0; + margin: 0 auto; + } + .reveal-modal.large, dialog.large { + width: 70%; + max-width: 62.5rem; + left: 0; + right: 0; + margin: 0 auto; + } + .reveal-modal.xlarge, dialog.xlarge { + width: 95%; + max-width: 62.5rem; + left: 0; + right: 0; + margin: 0 auto; + } + .reveal-modal.full, dialog.full { + width: 100vw; + max-width: 62.5rem; + left: 0; + right: 0; + margin: 0 auto; + } +} diff --git a/app/assets/stylesheets/admin/products.css.scss b/app/assets/stylesheets/admin/products.css.scss index d6fc8f74e6..b8ae95a779 100644 --- a/app/assets/stylesheets/admin/products.css.scss +++ b/app/assets/stylesheets/admin/products.css.scss @@ -33,6 +33,9 @@ table#listing_products.bulk { margin-bottom: 60px; colgroup col { + &.image { + width: 1%; + } &.producer { width: 18%; } @@ -62,6 +65,21 @@ table#listing_products.bulk { } } + td.image { + padding: 10px; + text-align: center; + img { + border: 1px solid transparent; + border-radius: 0.4em; + } + img:hover { + opacity: 0.8; + cursor: pointer; + background-color: #fbfbfb; + border: 1px solid #e4e4e4; + } + } + td.unit { input, select { width: 100%; @@ -77,3 +95,46 @@ table#listing_products.bulk { } } } + +.reveal-modal.product-image-upload { + width: 300px; + a.close-reveal-modal { + font-size: 23px; + color: #de6060; + right: 0.45rem; + top: 0.35rem; + :hover { + color: #bf4545; + } + } + div.image-preview { + //float: left; + } + label { + margin-top: 20px; + } +} + +form#image_upload { + text-align: center; + .spinner { + width: 160px; + height: 65%; + position: absolute; + //background-color: rgba(255, 255, 255, 0.75); + padding: 32px; + margin: 0px -80px; + left: 50%; + z-index: 100; + } + .preview { + width: 240px; + } + .faded { + opacity: 0.25; + } + .button:hover { + cursor: pointer; + } +} + diff --git a/app/assets/stylesheets/darkswarm/modal-enterprises.css.scss b/app/assets/stylesheets/darkswarm/modal-enterprises.css.scss index 25169ec990..4f66a180c3 100644 --- a/app/assets/stylesheets/darkswarm/modal-enterprises.css.scss +++ b/app/assets/stylesheets/darkswarm/modal-enterprises.css.scss @@ -133,8 +133,6 @@ font-size: 0.875rem; margin-bottom: 0.75rem; - 5rem {} - color: $dark-grey; } diff --git a/app/assets/stylesheets/darkswarm/style.css.scss b/app/assets/stylesheets/darkswarm/style.css.scss index f3eca6b24b..e9f018445b 100644 --- a/app/assets/stylesheets/darkswarm/style.css.scss +++ b/app/assets/stylesheets/darkswarm/style.css.scss @@ -1,10 +1,10 @@ @font-face { font-family: 'OFN'; - src:url('/OFN.eot?eslsji'); - src:url('/OFN.eot?#iefixeslsji') format('embedded-opentype'), - url('/OFN.woff?eslsji') format('woff'), - url('/OFN.ttf?eslsji') format('truetype'), - url('/OFN.svg?eslsji#OFN') format('svg'); + src:url('/OFN-v2.eot?eslsji'); + src:url('/OFN-v2.eot?#iefixeslsji') format('embedded-opentype'), + url('/OFN-v2.woff?eslsji') format('woff'), + url('/OFN-v2.ttf?eslsji') format('truetype'), + url('/OFN-v2.svg?eslsji#OFN') format('svg'); font-weight: normal; font-style: normal; } diff --git a/app/controllers/api/product_images_controller.rb b/app/controllers/api/product_images_controller.rb new file mode 100644 index 0000000000..adcf978c51 --- /dev/null +++ b/app/controllers/api/product_images_controller.rb @@ -0,0 +1,19 @@ +module Api + class ProductImagesController < Spree::Api::BaseController + respond_to :json + + def update_product_image + @product = Spree::Product.find(params[:product_id]) + authorize! :update, @product + + if @product.images.first.nil? + @image = Spree::Image.create(attachment: params[:file], viewable_id: @product.master.id, viewable_type: 'Spree::Variant') + respond_with(@image, status: 201) + else + @image = @product.images.first + @image.update_attributes(attachment: params[:file]) + respond_with(@image, status: 200) + end + end + end +end diff --git a/app/mailers/spree/base_mailer_decorator.rb b/app/mailers/spree/base_mailer_decorator.rb index f6cd835f84..863f97033a 100644 --- a/app/mailers/spree/base_mailer_decorator.rb +++ b/app/mailers/spree/base_mailer_decorator.rb @@ -7,8 +7,11 @@ Spree::BaseMailer.class_eval do protected + # This method copies the one defined in Spree's mailers. It should be removed + # once in Spree v2.0 and Spree's BaseMailer class lands in our codebase. + # Then, we'll be able to rely on its #from_address. def from_address - Spree::Config[:mails_from] || 'test@example.com' + Spree::MailMethod.current.preferred_mails_from end def roadie_options diff --git a/app/models/spree/app_configuration_decorator.rb b/app/models/spree/app_configuration_decorator.rb index c00c9bea27..53cf81b3b7 100644 --- a/app/models/spree/app_configuration_decorator.rb +++ b/app/models/spree/app_configuration_decorator.rb @@ -4,8 +4,6 @@ Spree::AppConfiguration.class_eval do # we can allow to be modified in the UI by adding appropriate form # elements to existing or new configuration pages. - preference :mails_from, :string, default: 'no-reply@example.com' - # Embedded Shopfronts preference :enable_embedded_shopfronts, :boolean, default: false preference :embedded_shopfronts_whitelist, :text, default: nil diff --git a/app/serializers/api/admin/product_serializer.rb b/app/serializers/api/admin/product_serializer.rb index fb340a61e7..dc88a03448 100644 --- a/app/serializers/api/admin/product_serializer.rb +++ b/app/serializers/api/admin/product_serializer.rb @@ -1,13 +1,21 @@ class Api::Admin::ProductSerializer < ActiveModel::Serializer attributes :id, :name, :sku, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand, :inherits_properties - attributes :on_hand, :price, :available_on, :permalink_live, :tax_category_id + attributes :on_hand, :price, :available_on, :permalink_live, :tax_category_id, :image_url, :thumb_url has_one :supplier, key: :producer_id, embed: :id has_one :primary_taxon, key: :category_id, embed: :id has_many :variants, key: :variants, serializer: Api::Admin::VariantSerializer # embed: ids has_one :master, serializer: Api::Admin::VariantSerializer + def image_url + object.images.present? ? object.images.first.attachment.url(:product) : "/assets/noimage/product.png" + end + + def thumb_url + object.images.present? ? object.images.first.attachment.url(:mini) : "/assets/noimage/mini.png" + end + def on_hand object.on_hand.nil? ? 0 : object.on_hand.to_f.finite? ? object.on_hand : I18n.t(:on_demand) end diff --git a/app/services/create_mail_method.rb b/app/services/create_mail_method.rb new file mode 100644 index 0000000000..7eec387603 --- /dev/null +++ b/app/services/create_mail_method.rb @@ -0,0 +1,34 @@ +# Configures Rails to use the specified mail settings. It does so creating +# a Spree::MailMethod and applying its configuration. +class CreateMailMethod + # Constructor + # + # @param attributes [Hash] MailMethod attributes + def initialize(attributes) + @attributes = attributes + end + + def call + persist_attributes + initialize_mail_settings + end + + private + + attr_reader :attributes + + # Updates the created mail method's attributes with the ones specified + def persist_attributes + mail_method.update_attributes(attributes) + end + + # Creates a new Spree::MailMethod for the current environment + def mail_method + Spree::MailMethod.create(environment: attributes[:environment]) + end + + # Makes Spree apply the specified mail settings + def initialize_mail_settings + Spree::Core::MailSettings.init + end +end diff --git a/app/views/admin/enterprises/_ng_form.html.haml b/app/views/admin/enterprises/_ng_form.html.haml index f635ad95c4..fa97605104 100644 --- a/app/views/admin/enterprises/_ng_form.html.haml +++ b/app/views/admin/enterprises/_ng_form.html.haml @@ -3,7 +3,6 @@ -# So we use onchange and have to get the scope to access the ng controller = form_for [main_app, :admin, @enterprise], html: { name: "enterprise_form", "ng-controller" => 'enterpriseCtrl', - 'onchange' => 'angular.element(enterprise_form).scope().setFormDirty()', "ng-cloak" => true } do |f| %save-bar{ dirty: "enterprise_form.$dirty", persist: "true" } diff --git a/app/views/admin/enterprises/form/_users.html.haml b/app/views/admin/enterprises/form/_users.html.haml index 6934f58931..adc6d4ba67 100644 --- a/app/views/admin/enterprises/form/_users.html.haml +++ b/app/views/admin/enterprises/form/_users.html.haml @@ -42,7 +42,7 @@ %tr %td - # Ignore this input in the submit - = hidden_field_tag :ignored, :new_manager, class: "select2 fullwidth", 'user-select' => 'newManager', 'ng-model' => 'newManager' + = hidden_field_tag :ignored, nil, class: "select2 fullwidth", 'user-select' => 'newManager', 'ng-model' => 'newManager' %td.actions %a{ 'ng-click' => 'addManager(newManager)', :class => "icon-plus no-text" } %tr.animate-repeat{ id: "manager-{{manager.id}}", ng: { repeat: 'manager in Enterprise.users' }} diff --git a/app/views/api/product_images/update_product_image.v1.rabl b/app/views/api/product_images/update_product_image.v1.rabl new file mode 100644 index 0000000000..c9e62475dc --- /dev/null +++ b/app/views/api/product_images/update_product_image.v1.rabl @@ -0,0 +1,5 @@ +object @image +attributes(*image_attributes) +attributes :viewable_type, :viewable_id +node( :thumb_url ) { @product.images.first.attachment.url(:mini) } +node( :image_url ) { @product.images.first.attachment.url(:product) } diff --git a/app/views/spree/admin/products/bulk_edit/_products_head.html.haml b/app/views/spree/admin/products/bulk_edit/_products_head.html.haml index 6ebe7b1f24..1ed74174f1 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_head.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_products_head.html.haml @@ -1,5 +1,6 @@ %colgroup %col.actions + %col.image{ ng: { show: 'columns.image.visible' } } %col.producer{ ng: { show: 'columns.producer.visible' } } %col.sku{ ng: { show: 'columns.sku.visible' } } %col.name{ ng: { show: 'columns.name.visible' } } @@ -21,6 +22,7 @@ %th.left-actions %a{ 'ng-click' => 'toggleShowAllVariants()', :style => 'color: red; cursor: pointer' } = t(:expand_all) + %th.image{ 'ng-show' => 'columns.image.visible' } %th.producer{ 'ng-show' => 'columns.producer.visible' }=t('admin.producer') %th.sku{ 'ng-show' => 'columns.sku.visible' }=t('admin.sku') %th.name{ 'ng-show' => 'columns.name.visible' }=t('.name') diff --git a/app/views/spree/admin/products/bulk_edit/_products_product.html.haml b/app/views/spree/admin/products/bulk_edit/_products_product.html.haml index 1e929878b0..e947f2eba2 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_product.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_products_product.html.haml @@ -2,6 +2,9 @@ %td.left-actions %a{ 'ofn-toggle-variants' => 'true', :class => "view-variants", 'ng-show' => 'hasVariants(product)' } %a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "!hasVariants(product) && hasUnit(product)" } + %td.image{ 'ng-show' => 'columns.image.visible' } + %a{class: 'image-modal'} + %img{'ng-src' => '{{ product.thumb_url }}'} %td.producer{ 'ng-show' => 'columns.producer.visible' } %select.select2.fullwidth{ 'ng-model' => 'product.producer_id', :name => 'producer_id', 'ofn-track-product' => 'producer_id', 'ng-options' => 'producer.id as producer.name for producer in producers' } %td.sku{ 'ng-show' => 'columns.sku.visible' } diff --git a/app/views/spree/admin/products/bulk_edit/_products_variant.html.haml b/app/views/spree/admin/products/bulk_edit/_products_variant.html.haml index 4862f210dd..eb87039ff0 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_variant.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_products_variant.html.haml @@ -2,6 +2,7 @@ %td.left-actions %a{ :class => "variant-item icon-caret-right", 'ng-hide' => "$last" } %a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "$last", 'ofn-with-tip' => t('.new_variant') } + %td{ 'ng-show' => 'columns.image.visible' } %td{ 'ng-show' => 'columns.producer.visible' } %td{ 'ng-show' => 'columns.sku.visible' } %input{ 'ng-model' => "variant.sku", :name => 'variant_sku', 'ofn-track-variant' => 'sku', :type => 'text' } diff --git a/config/application.yml.example b/config/application.yml.example index 27dafdd5e9..a554404a45 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -22,6 +22,13 @@ CURRENCY: AUD # See: config/schedule.rb #SCHEDULE_NOTIFICATIONS: admin@example.com +# Mail settings +MAIL_HOST: 'example.com' +MAIL_DOMAIN: 'example.com' +MAIL_PORT: 25 +SMTP_USERNAME: 'ofn' +SMTP_PASSWORD: 'f00d' + # SingleSignOn login for Discourse # # DISCOURSE_SSO_SECRET should be a random string. It must be the same as provided to your Discourse instance. diff --git a/config/locales/en.yml b/config/locales/en.yml index d020513384..1f817071ee 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -267,6 +267,7 @@ en: phone: Phone price: Price producer: Producer + image: Image product: Product quantity: Quantity schedule: Schedule @@ -438,6 +439,15 @@ en: products: unit_name_placeholder: 'eg. bunches' + bulk_edit: + unit: Unit + display_as: Display As + category: Category + tax_category: Tax Category + inherits_properties?: Inherits Properties? + available_on: Available On + av_on: "Av. On" + upload_an_image: Upload an image properties: property_name: Property Name inherited_property: Inherited Property diff --git a/config/locales/es.yml b/config/locales/es.yml index 6803f210d8..ea11860c3e 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1,26 +1,58 @@ es: + language_name: "Castellano" activerecord: attributes: spree/order: payment_state: Estado del pago shipment_state: Estado del envío + completed_at: Completado en + number: Número + email: E-mail del consumidor errors: models: spree/user: attributes: email: taken: "Ya existe una cuenta con este email. Inicie sesión o restablezca tu contraseña." + spree/order: + no_card: No hay tarjetas de crédito válidas disponibles + activemodel: + errors: + models: + subscription_validator: + attributes: + subscription_line_items: + at_least_one_product: "^Por favor agrega al menos un producto" + not_available: "^%{name} no está disponible en el programa seleccionado" + ends_at: + after_begins_at: "debe ser después comienza en" + customer: + does_not_belong_to_shop: "no pertenece a %{shop}" + schedule: + not_coordinated_by_shop: "no está coordinado por %{shop}" + payment_method: + not_available_to_shop: "no está disponible para %{shop}" + invalid_type: "El método debe ser Cash o Stripe" + shipping_method: + not_available_to_shop: "no está disponible para %{shop}" + credit_card: + not_available: "no está disponible" + blank: "Requerido" devise: + confirmations: + send_instructions: "Recibirás un correo electrónico con instrucciones sobre cómo confirmar su cuenta en unos minutos." + failed_to_send: "Se produjo un error al enviar su correo electrónico de confirmación." + resend_confirmation_email: "Reenviar el correo electrónico de confirmación." + confirmed: "¡Gracias por confirmar tu correo electrónico! Ahora puedes iniciar sesión." + not_confirmed: "Su dirección de correo electrónico no pudo ser confirmada. Tal vez ya has completado este paso?" + user_registrations: + spree_user: + signed_up_but_unconfirmed: "Se ha enviado un mensaje con un enlace de confirmación a tu dirección de correo electrónico. Abre el enlace para activar tu cuenta." failure: invalid: | - Correo o contraseña inválidos. - ¿Has sido invitada? Tal vez necesites crear una cuenta o recuperar tu contraseña. - enterprise_confirmations: - enterprise: - confirmed: Gracias, tu dirección de email ha sido confirmada. - not_confirmed: La dirección de correo no pudo ser confirmada. ¿Tal vez ya completó este paso? - confirmation_sent: "¡Correo de confirmación enviado!" - confirmation_not_sent: "No se pudo enviar un correo de confirmación." + Correo o contraseña inválidos. + ¿Has sido invitada? Tal vez necesites crear una cuenta o recuperar tu contraseña. + unconfirmed: "Debes confirmar tu cuenta antes de continuar." enterprise_mailer: confirmation_instructions: subject: "Confirma la dirección de correo electrónico de %{enterprise}" @@ -29,13 +61,48 @@ es: producer_mailer: order_cycle: subject: "Informe Ciclo de Pedido para %{producer}" + subscription_mailer: + placement_summary_email: + subject: Un resumen los pedidos de suscripción recientes + greeting: "Hola %{name}," + intro: "A continuación se muestra un resumen de los pedidos de suscripción que acaban de realizarse en %{shop}." + confirmation_summary_email: + subject: Un resumen de los pedidos de suscripción confirmadas recientemente + greeting: "Hola %{name}," + intro: "A continuación se muestra un resumen de los pedidos de suscripción que acaban de finalizar para %{shop}." + summary_overview: + total: Se marcaron un total de %{count} suscripciones para ser procesadas automáticamente. + success_zero: De estos, ninguno fue procesado. + success_some: De estos, %{count} se procesaron con éxito. + success_all: Todos fueron procesados ​​con éxito. + issues: Los detalles de los problemas encontrados se proporcionan a continuación. + summary_detail: + no_message_provided: No hay mensajes de error + changes: + title: Insufficient Stock (%{count} pedidos) + explainer: Estos pedidos se procesaron pero no se dispuso de existencias suficientes para algunos artículos solicitados + empty: + title: Sin stock (%{count} pedidos) + explainer: Estas órdenes no se pudieron procesar porque no había existencias disponibles para los artículos solicitados + complete: + title: Ya procesado (%{count} pedidos) + explainer: Estas órdenes ya estaban marcadas como completas y, por lo tanto, no se modificaron + processing: + title: Error encontrado (%{count} pedidos) + explainer: El procesamiento automático de estas órdenes falló debido a un error. El error será mostrado en los casos en que sea posible. + failed_payment: + title: Error en el pago (%{count} pedidos) + explainer: El procesamiento automático del pago de estos pedidos falló debido a un error. El error ha sido listado donde a sido posible. + other: + title: Otros fallos (%{count} pedidos) + explainer: El procesamiento automático de estas órdenes falló por un motivo desconocido. Esto no debería ocurrir, contáctanos si estás viendo esto. home: "OFN" title: Open Food Network welcome_to: 'Bienvenido a ' site_meta_description: "Nosotros empezamos desde abajo. Con granjeros y productoras listas para contar sus historias con orgullo y autenticidad. Con distribuidoras listas para conectar gente con productos de forma justa y honesta. Con compradores que creen que mejores decisiones de compras semanales pueden..." search_by_name: Buscar por nombre o barrio... producers_join: Las productoras australianas ahora son bienvenidas a unirse a Open Food Network. - charges_sales_tax: ¿Cargos de GST? + charges_sales_tax: ¿Cargos de IVA? print_invoice: "Imprimir factura" print_ticket: "Imprimir Ticket" select_ticket_printer: "Seleccionar impresora para los tickets" @@ -47,7 +114,7 @@ es: cancel_order: "Cancelar pedido" confirm_send_invoice: "Una factura para esta orde se envió al cliente. ¿Está seguro que quiere continuar?" confirm_resend_order_confirmation: "¿Estás seguro que quieres reenviar el correo de confirmación del pedido?" - must_have_valid_business_number: "%{enterprise_name} debe tener un ABN válido antes de que las facturas se puedan enviar." + must_have_valid_business_number: "%{enterprise_name} debe tener un NIF válido antes de que las facturas se puedan enviar." invoice: "Factura" percentage_of_sales: "%{percentage} de ventas" capped_at_cap: "Limitado en %{cap}" @@ -59,6 +126,9 @@ es: say_no: "No" say_yes: "Si" then: Entonces + ongoing: En marcha + bill_address: Dirección de facturación + ship_address: Dirección de envío sort_order_cycles_on_shopfront_by: "Ciclos de Pedidos en Tienda por" required_fields: Los campos obligatorios se indican con un asterisco select_continue: Selecciona y Continua @@ -72,6 +142,7 @@ es: show_all_with_more: "Mostrar todo (%{num} Más)" cancel: Cancelar edit: Editar + clone: Duplicar distributors: Distribuidores distribution: Distribución bulk_order_management: Gestión de pedidos en bloque @@ -90,6 +161,7 @@ es: supplier_only: Sólo Proveedor weight: Peso volume: Volumen + items: Elementos summary: Resumen detailed: Detallado updated: Actualizado @@ -103,23 +175,39 @@ es: notes: Notas error: Error processing_payment: Procesando el pago... + show_only_unfulfilled_orders: Mostrar solo órdenes no finalizadas + filter_results: Filtrar resultados + quantity: Cantidad + pick_up: Recogida + copy: Copia actions: create_and_add_another: "Crear y agregar otro" admin: + begins_at: Empieza en + begins_on: Comienza en + customer: Consumidora date: Fecha email: Email + ends_at: Termina en + ends_on: Finaliza name: Nombre on_hand: Disponibles on_demand: Bajo demanda on_demand?: Bajo demanda? order_cycle: Ciclo de Pedido + payment: Pago + payment_method: Método de pago phone: Teléfono price: Precio producer: Productora product: Producto quantity: Cantidad + schedule: Programación + shipping: Envío + shipping_method: Método de envío shop: Tienda sku: número de referencia + status_state: Provincia tags: Etiquetas variant: Variedad weight: Peso @@ -134,6 +222,10 @@ es: form_invalid: "El formulario contiene campos vacíos o inválidos" clear_filters: Limpiar filtros clear: Limpiar + show_more: Mostrar más + show_n_more: Mostrar %{num} más + choose: "Escoger..." + please_select: Por favor selecciona ... columns: Columnas actions: Acciones viewing: "Viendo: %{current_view_name}" @@ -169,6 +261,9 @@ es: embedded_shopfront_settings: "Configuración de la tienda integradaa" enable_embedded_shopfronts: "Habilitar tiendas integradas" embedded_shopfronts_whitelist: "Lista de dominios externos permitidos" + number_localization: + number_localization_settings: "Number Localization Settings" + enable_localized_number: "Usar la logica internacional separador miles/decimales" business_model_configuration: edit: business_model_configuration: "Modelo de Sostenibilidad" @@ -181,7 +276,7 @@ es: fixed_monthly_charge_tip: "Un cargo mensual fijo para todas las organizaciones que se establezcan como una tienda y hayan superado el volumen de negocios facturable mínimo (si se establece)" percentage_of_turnover: "Porcentaje de volumen de ventas" percentage_of_turnover_tip: "Cuando sea mayor que cero (0,0 - 1,0) se aplicará al volumen de ventas total de cada tienda y se agregará a cualquier cargo fijo (a la izquierda) para calcular la factura mensual." - monthly_cap_excl_tax: "Límite mensual (excl. GST)" + monthly_cap_excl_tax: "Límite mensual (excl. IVA)" monthly_cap_excl_tax_tip: "Cuando sea mayor que cero, este valor se usará como un límite en la cantidad que se cobrará cada mes." tax_rate: "Porcentaje del Impuesto" tax_rate_tip: "Porcentaje del impuesto que se aplica a la factura mensual que las organizaciones cobran por usar el sistema." @@ -258,19 +353,15 @@ es: manages: Gestionan products: unit_name_placeholder: 'ej. manojos' - bulk_edit: - unit: Unidad - display_as: Mostrar como - category: Categoría - tax_category: Categoría del impuesto - inherits_properties?: ¿Propiedades heredadas? - available_on: Disponible en - av_on: "Disp. en" properties: property_name: Nombre de la Propiedad inherited_property: Propiedad Heredada variants: to_order_tip: "Los artículos hechos según demanda no tienen un nivel de stock, como por ejemplo panes hechos según demanda." + product_distributions: "Distribuciones de productos" + group_buy_options: "Opciones de compra grupales" + seo: "SEO" + back_to_products_list: "Volver a la lista de productos" variant_overrides: loading_flash: loading_inventory: CARGANDO INVENTARIO @@ -338,7 +429,7 @@ es: desc_long: Sobre nosotras desc_long_placeholder: Explica a las consumidoras quién eres. Esta información aparecerá en tu perfil público. business_details: - abn: ABN + abn: NIF abn_placeholder: ej. 99 123 456 789 acn: ACN acn_placeholder: ej. 123 456 789 @@ -347,8 +438,9 @@ es: contact: name: Nombre name_placeholder: ej. Gustav Plum - email_address: Dirección de Email - email_address_placeholder: ej. gustav@truffles.com + email_address: Dirección de correo electrónico público + email_address_placeholder: p.ej. consultas@lacesta.com + email_address_tip: "Esta dirección de correo electrónico se mostrará en su perfil público" phone: Teléfono phone_placeholder: ej. 98 7654 3210 website: Website @@ -413,9 +505,9 @@ es: create_one_button: Crear una ahora no_method_yet: Todavía no tienes ningún método de envío. shop_preferences: - shopfront_requires_login: "¿Visibilidad pública de la tienda?" + shopfront_requires_login: "¿Visibilidad de la tienda?" shopfront_requires_login_tip: "Elije si los clientes deben iniciar sesión para ver la tienda o si es visible para todos." - shopfront_requires_login_false: "Público" + shopfront_requires_login_false: "Pública" shopfront_requires_login_true: "Visible solo para consumidores registrados" recommend_require_login: "Recomendamos requerir a los usuarios que accedan cuando sus pedidos puedan cambiar." allow_guest_orders: "Pedidos de invitados" @@ -426,6 +518,10 @@ es: allow_order_changes_tip: "Permitir a los clientes cambiar sus pedidos mientras que el ciclo de pedido siga abierto." allow_order_changes_false: "Los pedidos efectuados no pueden ser cambiados / cancelados" allow_order_changes_true: "Los clientes pueden cambiar / cancelar pedidos mientras el ciclo de pedido está abierto" + enable_subscriptions: "Suscripciones" + enable_subscriptions_tip: "¿Habilitar la funcionalidad de suscripciones?" + enable_subscriptions_false: "Deshabilitado" + enable_subscriptions_true: "Habilitado" shopfront_message: Mensaje de la Tienda shopfront_message_placeholder: > Una explicación opcional para los consumidores que detallan cómo funciona @@ -468,6 +564,8 @@ es: email_confirmation_notice_html: "Falta la confirmación del email. Te hemos enviado un correo de confirmación a %{email}." resend: Reenviar owner: 'Propietaria' + contact: "Contacto" + contact_tip: "El administrador que recibirá los correos electrónicos de la empresa para pedidos y notificaciones. Debe tener una dirección de correo electrónico confirmada." owner_tip: La principal usaría responsable para esta organización. notifications: Notificaciones notifications_tip: Las Notificaciones sobre los pedidos se enviarán a esta dirección de correo. @@ -475,6 +573,8 @@ es: notifications_note: 'Nota: Una nueva dirección de correo debe ser confirmada. ' managers: Gestoras managers_tip: Otras usuarias con permiso para gestionar esta organización. + email_confirmed: "Correo electrónico confirmado" + email_not_confirmed: "Correo electrónico no confirmado" actions: edit_profile: Editar Perfil properties: Propiedades @@ -565,8 +665,25 @@ es: distributor: Distribuidor products: Productos tags: Etiquetas + add_a_tag: Add a tag delivery_details: Detalles de Recogida / Entrega debug_info: Información de Debug + index: + involving: Involucrando + schedule: Horario + schedules: Horarios + adding_a_new_schedule: Agregar un nuevo horario + updating_a_schedule: Actualización de un horario + new_schedule: Nuevo horario + create_schedule: Crear horario + update_schedule: Actualizar horario + delete_schedule: Eliminar horario + created_schedule: Horario creado + updated_schedule: Horario actualizado + deleted_schedule: Horario eliminado + schedule_name_placeholder: Nombre del horario + name_required_error: Por favor ingrese un nombre para este horario + no_order_cycles_error: Seleccione al menos un ciclo de pedido (arrastrar y soltar) name_and_timing_form: name: Nombre orders_open: Pedidos abiertos a @@ -583,9 +700,17 @@ es: customer_instructions_placeholder: Notas de la recogida o entrega products: Productos fees: Comisiones + destroy_errors: + orders_present: Ese ciclo de pedido ha sido seleccionado por un cliente y no puede ser eliminado. Para evitar que los clientes accedan a él, ciérrelo. + schedule_present: Ese ciclo de pedido está vinculado a un horario y no puede ser eliminado. Desvincula o elimina el calendario primero. producer_properties: index: title: Propiedades de la Productora + proxy_orders: + cancel: + could_not_cancel_the_order: No se pudo cancelar la orden + resume: + could_not_resume_the_order: No se pudo reanudar el pedido shared: user_guide_link: user_guide: Manual de Usuario @@ -658,6 +783,67 @@ es: description: Facturas para la importación en Xero packing: name: Informes de empaquetado + subscriptions: + subscriptions: Suscripciones + new: Nueva suscripción + create: Crear suscripción + index: + please_select_a_shop: Por favor seleccione una tienda + edit_subscription: Editar suscripción + pause_subscription: Pausa Suscripción + unpause_subscription: Reanudar suscripción + cancel_subscription: Cancelar suscripción + setup_explanation: + just_a_few_more_steps: 'Solo unos pocos pasos más antes de que pueda comenzar:' + enable_subscriptions: "Habilita suscripciones para al menos una de tus tiendas" + enable_subscriptions_step_1_html: 1. Vaya a la página %{enterprises_link}, encuentre su tienda y haga clic en "Administrar" + enable_subscriptions_step_2: 2. En "Comprar preferencias", habilite la opción Suscripciones + set_up_shipping_and_payment_methods_html: Configurar los métodos %{shipping_link} y %{payment_link} + set_up_shipping_and_payment_methods_note_html: Tenga en cuenta que solo se pueden usar
métodos de pago en efectivo y Stripe con las suscripciones + ensure_at_least_one_customer_html: Asegúrese de que exista al menos un %{customer_link} + create_at_least_one_schedule: Crear al menos un horario + create_at_least_one_schedule_step_1_html: 1. Vaya a la página %{order_cycles_link} + create_at_least_one_schedule_step_2: 2. Crea un ciclo de pedido si aún no lo has hecho + create_at_least_one_schedule_step_3: 3. Haga clic en '+ Nuevo horario' y complete el formulario + once_you_are_done_you_can_html: Una vez que haya terminado, puede %{reload_this_page_link} + reload_this_page: recarga esta página + steps: + details: 1. Detalles básicos + address: 2. Dirección + products: 3. Agregue productos + review: 4. Revisar y guardar + details: + details: Detalles + invalid_error: Ups! Por favor complete todos los campos requeridos ... + allowed_payment_method_types_tip: Solo se pueden usar métodos de pago en efectivo y Stripe en este momento + credit_card: Tarjeta de crédito + no_cards_available: No hay tarjetas disponibles + loading_flash: + loading: CARGANDO SUSCRIPCIONES + review: + details: Detalles + address: Dirección + products: Productos + product_already_in_order: Este producto ya ha sido agregado a la orden. Por favor edite la cantidad directamente. + orders: + number: Número + confirm_edit: ¿Seguro que quieres editar esta orden? Si lo hace, puede ser más difícil sincronizar automáticamente los cambios a la suscripción en el futuro. + confirm_cancel_msg: ¿Seguro que quieres cancelar esta suscripción? Esta acción no se puede deshacer. + cancel_failure_msg: 'Lo sentimos, ¡la cancelación falló!' + confirm_pause_msg: ¿Seguro que quieres pausar esta suscripción? + pause_failure_msg: 'Lo sentimos, pausar falló!' + confirm_unpause_msg: ¿Seguro que quieres reanudar esta suscripción? + unpause_failure_msg: 'Lo sentimos, ¡no se pudo reanudar!' + confirm_cancel_open_orders_msg: "Algunas órdenes para esta suscripción están actualmente abiertas. El cliente ya ha sido notificado de que se realizará el pedido. ¿Desea cancelar estos pedidos o conservarlos?" + resume_canceled_orders_msg: "Algunos pedidos de esta suscripción se pueden reanudar en este momento. Puede reanudarlos desde el menú desplegable de pedidos." + yes_cancel_them: Cancelarlos + no_keep_them: Guárdalos + yes_i_am_sure: Sí estoy seguro + order_update_issues_msg: Algunas órdenes no se pudieron actualizar automáticamente, esto es más probable porque se han editado manualmente. Revise los problemas que se detallan a continuación y realice los ajustes a los pedidos individuales si es necesario. + no_results: + no_subscriptions: Aún no hay suscripciones ... + why_dont_you_add_one: ¿Por qué no agregas una? :) + no_matching_subscriptions: No se encontraron suscripciones coincidentes stripe_connect_settings: edit: title: "Stripe Connect" @@ -718,12 +904,12 @@ es: invoice_column_price_with_taxes: "Precio total (Impuestos incl.)" invoice_column_price_without_taxes: "Precio total (Sin Impuestos)" invoice_column_tax_rate: "% Impuestos" - invoice_tax_total: "GST Total:" + invoice_tax_total: "IVA Total:" tax_invoice: "FACTURA DE IMPUESTOS" tax_total: "Total impuestos (%{rate}):" total_excl_tax: "Total (Sin Impuestos):" total_incl_tax: "Total (Impuestos incl.)" - abn: "ABN:" + abn: "NIF:" acn: "ACN:" invoice_issued_on: "Factura emitida el:" order_number: "Número de Factura:" @@ -772,13 +958,18 @@ es: no_payment: Sin métodos de pago no_shipping_or_payment: Sin envío ni métodos de pago unconfirmed: sin confirmar + days: dias + label_shop: "Tienda" label_shops: "Tiendas" label_map: "Mapa" + label_producer: "Productora" label_producers: "Productoras" label_groups: "Redes" label_about: "Acerca de" label_connect: "Conectar" label_learn: "Aprender" + label_blog: "Blog" + label_support: "Soporte" label_shopping: "Tienda" label_login: "Iniciar sesión" label_logout: "Cerrar sesión" @@ -789,7 +980,7 @@ es: label_more: "Mostrar más" label_less: "Mostrar menos" label_notices: "Noticias" - items: "artículos" + cart_items: "elementos" cart_headline: "Su carrito de compras" total: "Total" cart_updating: "Actualizando el carrito..." @@ -921,12 +1112,12 @@ es: email_community_html: "También tenemos un foro en líea para la discusión comunal relacionada con el programa OFN y los retos únicos del funcionamiento de una organización de alimentación. Lo invitamos a unirse. Estamos evolucionando de forma constante y su aporte en este formo le dará forma a lo que pase luego. %{link}" join_community: "Unirse a la comunidad" email_help: "Si tiene dificultades, revise nuestras preguntas frecuentes, navegue el foro o haga una entrada de con tema de 'Soporte' y ¡alguien le ayudará!" + email_confirmation_activate_account: "Antes de que podamos activar su nueva cuenta, necesitamos confirmar su dirección de correo electrónico." email_confirmation_greeting: "Hola, %{contact}!" email_confirmation_profile_created: "¡Se creó un un perfil para %{name} con éxito! Para activar su Perfil necesitamos que confirme esta dirección de correos." email_confirmation_click_link: "Por favor haga clic en el enlace de abajo para confirmar el correo electrónico y continuar configurando su perfil." email_confirmation_link_label: "Confirmar este correo electrónico »" email_confirmation_help_html: "Después de confirmar el correo electrónico puedes acceder a tu cuenta de administración para esta organización. Visita %{link} para encontrar más información sobre las características de %{sitename} y empieza a usar tu perfil o tienda online." - email_confirmation_userguide: "Guía de Usuario" email_social: "Conecte con nosotros:" email_contact: "Envíenos un correo electrónico:" email_signoff: "Saludos," @@ -944,10 +1135,28 @@ es: email_order_summary_price: "Precio" email_order_summary_subtotal: "Subtotal:" email_order_summary_total: "Total:" + email_order_summary_includes_tax: "(incluye impuestos):" email_payment_paid: PAGADO email_payment_not_paid: NO PAGADO email_payment_summary: Resumen de pago email_payment_method: "Pagando con:" + email_so_placement_intro_html: "Tiene un nuevo pedido con %{distributor} " + email_so_placement_details_html: "Aquí están los detalles de su pedido para %{distributor} :" + email_so_placement_changes: "Desafortunadamente, no todos los productos que solicitó estaban disponibles. Las cantidades originales que solicitó aparecen tachadas a continuación." + email_so_payment_success_intro_html: "Se ha procesado un pago automático para su pedido desde %{distributor} ." + email_so_placement_explainer_html: "Esta orden se creó automáticamente para usted." + email_so_edit_true_html: "Puede realizar cambios hasta que los pedidos se cierren en %{orders_close_at}." + email_so_edit_false_html: "Puede ver detalles de este pedido en cualquier momento." + email_so_contact_distributor_html: "Si tiene alguna pregunta, puede contactar %{distributor} a través de %{email}." + email_so_confirmation_intro_html: "Tu pedido con %{distributor} ahora está confirmado" + email_so_confirmation_explainer_html: "Este pedido fue colocado automáticamente para usted, y ahora ha sido finalizado." + email_so_confirmation_details_html: "Aquí encontrará todo lo que necesita saber sobre su pedido en %{distributor} :" + email_so_empty_intro_html: "Intentamos realizar un nuevo pedido con %{distributor} , pero tuvimos algunos problemas ..." + email_so_empty_explainer_html: "Lamentablemente, ninguno de los productos que ordenó estaba disponible, por lo que no se realizó ningún pedido. Las cantidades originales que solicitó aparecen tachadas a continuación." + email_so_empty_details_html: "Aquí están los detalles del pedido no realizado para %{distributor} :" + email_so_failed_payment_intro_html: "Intentamos procesar un pago, pero tuvimos algunos problemas ..." + email_so_failed_payment_explainer_html: "El pago de su suscripción con %{distributor} ha fallado debido a un problema con su tarjeta de crédito. %{distributor} ha recibido notificación de este pago fallido." + email_so_failed_payment_details_html: "Aquí están los detalles del fallo proporcionados por la pasarela de pago:" email_shipping_delivery_details: Detalles de entrega email_shipping_delivery_time: "Entregar en:" email_shipping_delivery_address: "Dirección de entrega:" @@ -957,9 +1166,8 @@ es: email_special_instructions: "Sus notas:" email_signup_greeting: ¡Hola! email_signup_welcome: "Bienvenido a %{sitename}!" - email_signup_login: Su nombre de usuario - email_signup_email: Tu correo electrónico para el inicio de sesión es - email_signup_shop_html: "Puede empezar a comprar en línea ahora en %{link}." + email_signup_confirmed_email: "Gracias por confirmar su email." + email_signup_shop_html: "Ahora puedes iniciar sesión en %{link}." email_signup_text: "Gracias por unirte a la red. Si eres un comprador, ¡esperamos presentarte a muchos agricultores, grupos de consumo y deliciosa comida! Si eres una productora o formas parte de una organización de alimentos, estamos emocionados de que formes parte de la red." email_signup_help_html: "Agradecemos todas tus preguntas y feedback; puedes usar el botón de Enviar Feedback en el sitio o escribir un email a %{email}" producer_mail_greeting: "Estimada" @@ -969,7 +1177,7 @@ es: producer_mail_signoff: "Gracias y hasta pronto" shopping_oc_closed: Los pedidos están cerrados shopping_oc_closed_description: "Por favor espere hasta que el próximo ciclo abra (o contactanos de forma directa para ver si podemos aceptar algunos pedidos tardíos)" - shopping_oc_last_closed: "El último cilco cerró hace %{distance_of_time}" + shopping_oc_last_closed: "El último ciclo cerró hace %{distance_of_time}" shopping_oc_next_open: "El próximo ciclo abrirá en %{distance_of_time}" shopping_tabs_about: "Acerca de %{distributor}" shopping_tabs_contact: "Contacto" @@ -981,6 +1189,8 @@ es: enterprises_next_closing: "Los pedidos se cerrarán" enterprises_ready_for: "Listo para" enterprises_choose: "Escoger cuando quieres tu pedido:" + maps_open: "Abierta" + maps_closed: "Cerrado" hubs_buy: "Comprar:" hubs_shopping_here: "Comprando aquí" hubs_orders_closed: "Pedidos cerrados" @@ -1053,7 +1263,7 @@ es: groups_signup_contact_text: "Póngase en conta para descubrir qué puede hacer OFN por usted:" groups_signup_detail: "Aquí está el detalle." login_invalid: "Correo electrónico o contraseña inválidos" - modal_hubs: "Grupos de Consumo" + modal_hubs: "Hubs" modal_hubs_abstract: ¡Nuestros Grupos de Consumo son el punto de contacto entre usted y la gente que hace su comida! modal_hubs_content1: Puede buscar un grupo de consumo conveniente por ubicación o nombre. Algunos grupos de consumo tienen múltiples puntos en loa que puede recoger las compras, y algunos también brindan opciones de entrega a domicilio. Cada Grupo de Consumo es un punto de venta con operaciones de negocio y logística independientes, entonces puede esperar diferencias entre Grupos de Consumo. modal_hubs_content2: Sólo puedes comprar en un grupo de consumo a la vez. @@ -1101,7 +1311,7 @@ es: sell_headline: "¡Consíguelo en Open Food Network!" sell_motivation: "Muestra tus preciosos alimentos." sell_producers: "Productoras" - sell_hubs: "Grupos de Consumo" + sell_hubs: "Hubs" sell_groups: "Redes" sell_producers_detail: "Crea tu perfil en OFN en cuestión de minutos. En cualquier momento puedes actualizar tu perfil a una tienda en línea y vender tus productos directamente a los consumidores." sell_hubs_detail: "Crea un perfil para tu organización en OFN. En cualquier momento puedes actualizar tu perfil a una tienda de varias productoras." @@ -1122,6 +1332,7 @@ es: shops_signup_help: Estamos listos para ayudar. shops_signup_help_text: Usted necesita un mejor retorno. Usted necesita nuevos compradores y socios de logística. Usted necesita que su historia sea contada a través de ventas al por mayor, al detalle y en la mesa de la cocina. shops_signup_detail: Aquí está el detalle. + orders: Pedidos orders_fees: Tarifas... orders_edit_title: Carrito de compras orders_edit_headline: Su carrito de compras @@ -1147,8 +1358,8 @@ es: orders_could_not_cancel: "Lo sentimos, no se pudo cancelar el pedido" orders_cannot_remove_the_final_item: "No se puede quitar el último artículo de un pedido, en su lugar, por favor cancele el pedido." orders_bought_items_notice: - one: Un elemento adicional ya está confirmado para este ciclo de pedido - other: '%{count} artículos adicionales ya confirmados para este ciclo de pedido' + one: "Un elemento adicional ya está confirmado para este ciclo de pedido" + other: "%{count} artículos adicionales ya confirmados para este ciclo de pedido" orders_bought_edit_button: Editar artículos confirmados orders_bought_already_confirmed: "* ya confirmados" orders_confirm_cancel: Seguro que desea cancelar este pedido? @@ -1304,14 +1515,13 @@ es: enterprise_about_headline: "¡Seguimos!" enterprise_about_message: "Ahora vamos a profundizar en los detalles acerca de" enterprise_success: "¡Felicidades! %{enterprise} se agregó a Open Food Network " - enterprise_registration_exit_message: "Si sale de este asistente en cualquier etapa, debe hacer clic en el enlace de confirmación el el correo electrónico que recibió. Esto lo llevará a la interfaz de administración en la que puede continuar configurando su perfil." enterprise_description: "Descripción corta" enterprise_description_placeholder: "Una frase corta que describa tu organización" enterprise_long_desc: "Descripción larga" enterprise_long_desc_placeholder: "Esta es tu oportunidad de contar la historia de tu organización - ¿qué la hace diferente? Sugerimos mantener la descripción en menos de 600 caracteres o 150 palabras." enterprise_long_desc_length: "%{num} caracteres / recomentdamos hasta 600" enterprise_limit: Límite de la Organización - enterprise_abn: "ABN" + enterprise_abn: "NIF" enterprise_abn_placeholder: "eg. 99 123 456 789" enterprise_acn: "ACN" enterprise_acn_placeholder: "eg. 123 456 789" @@ -1329,9 +1539,9 @@ es: instagram: "Instagram" instagram_placeholder: "usuario_de_instagram" registration_greeting: "¡Hola!" - registration_intro: "Ahora puede crear un perfil para su Productora o Grupo de Consumo" + registration_intro: "Ahora puedes crear un perfil para tu Productora o Grupo de Consumo" registration_action: "¡Empecemos!" - registration_checklist: "Necesitará" + registration_checklist: "Necesitarás" registration_time: "5-10 minutos" registration_enterprise_address: "Dirección de la organización" registration_contact_details: "Detalles de contacto principal" @@ -1345,8 +1555,6 @@ es: registration_finished_headline: "¡Terminado!" registration_finished_thanks: "Gracias por llenar los detalles de %{enterprise}." registration_finished_login: "Puede cambiar o actualizar su negocio en cualquier etapa iniciando sesión en Open Food Network y yendo a Admin." - registration_finished_activate: "Activar %{enterprise}." - registration_finished_activate_instruction_html: "Hemos enviado un correo de confirmación a %{email} si no has activado tu cuenta ya.
Sigue las instrucciones para hacer tu organización visible en Open Food Network." registration_finished_action: "Página de inicio de Open Food Network" registration_contact_name: 'Nombre de contacto' registration_type_headline: "¡Último paso para agregar %{enterprise}!" @@ -1498,7 +1706,7 @@ es: spree_admin_enterprises_producers_total_products: "Total de Productos" spree_admin_enterprises_producers_active_products: "Productos Activos" spree_admin_enterprises_producers_order_cycles: "Productos en OCs" - spree_admin_enterprises_tabs_hubs: "Grupos de Consumo" + spree_admin_enterprises_tabs_hubs: "HUBS" spree_admin_enterprises_tabs_producers: "PRODUCTORAS" spree_admin_enterprises_producers_manage_products: "GESTIONAR PRODUCTOS" spree_admin_enterprises_any_active_products_text: "No tienes ningún producto activo" @@ -1557,7 +1765,7 @@ es: admin_share_zipcode: "Código Postal" admin_share_country: "País" admin_share_state: "Estado" - hub_sidebar_hubs: "Grupos de Consumo" + hub_sidebar_hubs: "Hubs" hub_sidebar_none_available: "Ninguno Disponible" hub_sidebar_manage: "Gestionar" hub_sidebar_at_least: "Al menos un grupo debe ser seleccionado" @@ -1571,7 +1779,7 @@ es: report_customers_csv: "Descargar como CSV" report_producers: "Productoras:" report_type: "Tipo de Informe:" - report_hubs: "Grupos de Consumo:" + report_hubs: "Hubs: " report_payment: "Métodos de Pago:" report_distributor: "Distribuidora:" report_payment_by: 'Pagos por Tipo' @@ -1707,8 +1915,8 @@ es: report_header_temp_controlled: ¿Control de Temperatura? report_header_is_producer: ¿Productora? report_header_not_confirmed: No confirmado - report_header_gst_on_income: GST sobre Ingresos - report_header_gst_free_income: Ingresos sin GST + report_header_gst_on_income: IVA sobre Ingresos + report_header_gst_free_income: Ingresos sin IVA report_header_total_untaxable_produce: Total productos sin impuestos report_header_total_taxable_produce: Total productos con impuestos report_header_total_untaxable_fees: Total comisiones sin impuestos @@ -1796,7 +2004,6 @@ es: order_cycles_email_to_producers_notice: 'Los correos electrónicos que se enviarán a las productoras se han puesto en cola para enviarlos.' order_cycles_no_permission_to_coordinate_error: "Ninguna de tus organizaciones tiene permiso para coordinar un ciclo de pedido" order_cycles_no_permission_to_create_error: "No tienes permiso para crear un ciclo de pedido coordinado por esta empresa." - order_cycles_no_permission_to_delete_error: "Ese ciclo de pedido ha sido seleccionado por un cliente y no puede eliminarse. Para evitar que los clientes accedan a él, ciérralo." js: saving: 'Guardando...' changes_saved: 'Cambios guardados.' @@ -1815,6 +2022,7 @@ es: resolve_errors: Resuelve los siguientes errores more_items: "+ %{count} Más" admin: + enterprise_limit_reached: "Has alcanzado el límite estándar de organizaciones por cuenta. Escriba a %{contact_email} si necesita aumentarlo." modals: got_it: Lo entiendo tag_rule_help: @@ -1973,8 +2181,8 @@ es: could_not_delete_customer: 'No se pudo eliminar al consumidor' product_import: confirmation: | - Esto establecerá el nivel de stock a cero en todos los productos para este - organización no está presente en el archivo subido. + Esto establecerá el nivel de stock a cero en todos los productos para este + organización no está presente en el archivo subido. order_cycles: update_success: 'Se ha actualizado su ciclo de pedido.' no_distributors: No hay distribuidores en este ciclo de pedido. Este ciclo de pedido no será visible para las consumidoras hasta que agregues uno. ¿Deseas continuar guardando este ciclo de pedido? ' @@ -1984,12 +2192,16 @@ es: customers: select_shop: 'Seleccione primero una tienda' could_not_create: ¡Lo siento! No se pudo crear + subscriptions: + closes: cierra + closed: cerrado producers: signup: start_free_profile: "Empieze con un perfil gratuito, y amplíelo cuando esté preparado!" spree: email: Email account_updated: "Cuenta actualizada!" + my_account: "Mi cuenta" admin: orders: invoice: @@ -2041,6 +2253,18 @@ es: title: CARGANDO PRODUCTOS no_products: "Todavía no hay productos. Añade algunos antes" no_results: "No se han encontrado resultados" + products_head: + name: Nombre + unit: Unidad + display_as: Mostrar como + category: Categoría + tax_category: Categoría del impuesto + inherits_properties?: Hereda propiedades? + available_on: Disponible en + av_on: "Disp. en" + products_variant: + variant_has_n_overrides: "Esta variante tiene %{n} override(s)" + new_variant: "Nueva variante" product_name: nombre del producto primary_taxon_form: product_category: categoría del producto @@ -2071,7 +2295,7 @@ es: remember_this_card: ¿Recordar esta tarjeta? date_picker: format: '%Y-%m-%d' - js_format: 'aa-mm-dd' + js_format: 'yy-mm-dd' inventory: Inventario orders: bought: @@ -2084,7 +2308,6 @@ es: address: dirección adjustments: ajustes awaiting_return: Esperando retorno - canceled: cancelado cart: carrito complete: completar confirm: Confirmar @@ -2113,13 +2336,15 @@ es: user_mailer: reset_password_instructions: request_sent_text: | - Se ha solicitado el cambio de tu contraseña. - Si tu no lo has solicitado simplemente ignora este email. + Se ha solicitado el cambio de tu contraseña. + Si tu no lo has solicitado simplemente ignora este email. link_text: > Si has solicitado esta acción haz click en el siguiente enlace: issue_text: | - Si el enlace no funciona prueba a copiarlo y pegarlo en tu navegador. - Si los problemas continúan no dudes en contactarnos. + Si el enlace no funciona prueba a copiarlo y pegarlo en tu navegador. + Si los problemas continúan no dudes en contactarnos. + confirmation_instructions: + subject: Por favor, confirma tu cuenta de OFN weight: Peso (en kg) zipcode: Código Postal users: @@ -2133,6 +2358,9 @@ es: settings: Configuración de la cuenta orders: open_orders: Pedidos Abiertos + past_orders: Pedidos anteriores + transactions: + transaction_history: Historial de transacciones open_orders: order: Pedido shop: Tienda @@ -2143,3 +2371,13 @@ es: cancel: Cancelar closed: Cerrado until: Hasta + past_orders: + order: Pedido + shop: Tienda + completed_at: Completado en + items: Artículos + total: Total + paid?: ¿Pagado? + view: Ver + localized_number: + invalid_format: tiene un formato invalido. Por favor introduzca un numero. diff --git a/config/routes.rb b/config/routes.rb index 0ae125dfd9..2062a1d60f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -212,6 +212,8 @@ Openfoodnetwork::Application.routes.draw do resource :status do get :job_queue end + + post '/product_images/:product_id', to: 'product_images#update_product_image' end namespace :open_food_network do @@ -287,7 +289,6 @@ Spree::Core::Engine.routes.prepend do resources :orders do get :managed, on: :collection end - end namespace :admin do diff --git a/db/seeds.rb b/db/seeds.rb index 9f9bc1efca..34e812799b 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -29,3 +29,24 @@ states.each do |state| ) end end + +def create_mail_method + Spree::MailMethod.destroy_all + + CreateMailMethod.new( + environment: Rails.env, + preferred_enable_mail_delivery: true, + preferred_mail_host: ENV.fetch('MAIL_HOST'), + preferred_mail_domain: ENV.fetch('MAIL_DOMAIN'), + preferred_mail_port: ENV.fetch('MAIL_PORT'), + preferred_mail_auth_type: 'login', + preferred_smtp_username: ENV.fetch('SMTP_USERNAME'), + preferred_smtp_password: ENV.fetch('SMTP_PASSWORD'), + preferred_secure_connection_type: 'None', + preferred_mails_from: "no-reply@#{ENV.fetch('MAIL_DOMAIN')}", + preferred_mail_bcc: '', + preferred_intercept_email: '' + ).call +end + +create_mail_method diff --git a/lib/open_food_network/column_preference_defaults.rb b/lib/open_food_network/column_preference_defaults.rb index 645764b27e..ec2953a823 100644 --- a/lib/open_food_network/column_preference_defaults.rb +++ b/lib/open_food_network/column_preference_defaults.rb @@ -58,6 +58,7 @@ module OpenFoodNetwork def products_bulk_edit_columns node = "spree.admin.products.bulk_edit.products_head" { + image: { name: I18n.t("admin.image"), visible: true }, producer: { name: I18n.t("admin.producer"), visible: true }, sku: { name: I18n.t("admin.sku"), visible: false }, name: { name: I18n.t("admin.name"), visible: true }, diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake index bbcbd39400..4bf1c58c3c 100644 --- a/lib/tasks/dev.rake +++ b/lib/tasks/dev.rake @@ -13,11 +13,6 @@ namespace :openfoodnetwork do country = Spree::Country.find_by_iso(ENV.fetch('DEFAULT_COUNTRY_CODE')) state = country.states.first - Spree::MailMethod.create!( - environment: Rails.env, - preferred_mails_from: spree_user.email - ) - # -- Shipping / payment information unless Spree::Zone.find_by_name 'Australia' puts "[#{task_name}] Seeding shipping / payment information" @@ -210,6 +205,5 @@ namespace :openfoodnetwork do spree_user.confirm! end - end end diff --git a/public/OFN.eot b/public/OFN-v2.eot similarity index 100% rename from public/OFN.eot rename to public/OFN-v2.eot diff --git a/public/OFN.svg b/public/OFN-v2.svg similarity index 100% rename from public/OFN.svg rename to public/OFN-v2.svg diff --git a/public/OFN.ttf b/public/OFN-v2.ttf similarity index 100% rename from public/OFN.ttf rename to public/OFN-v2.ttf diff --git a/public/OFN.woff b/public/OFN-v2.woff similarity index 100% rename from public/OFN.woff rename to public/OFN-v2.woff diff --git a/spec/controllers/api/product_images_controller_spec.rb b/spec/controllers/api/product_images_controller_spec.rb new file mode 100644 index 0000000000..e0d70c2fac --- /dev/null +++ b/spec/controllers/api/product_images_controller_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' +require 'spree/api/testing_support/helpers' + +module Api + describe ProductImagesController, type: :controller do + include AuthenticationWorkflow + render_views + + describe "uploading an image" do + before do + allow(controller).to receive(:spree_current_user) { current_api_user } + end + + image_path = File.open(Rails.root.join('app', 'assets', 'images', 'logo-black.png')) + let(:image) { Rack::Test::UploadedFile.new(image_path, 'image/png') } + let!(:product_without_image) { create(:product) } + let!(:product_with_image) { create(:product_with_image) } + + sign_in_as_admin! + + it "saves a new image when none is present" do + xhr :post, :update_product_image, product_id: product_without_image.id, file: image, use_route: :product_images + + expect(response.status).to eq 201 + expect(product_without_image.images.first.id).to eq json_response['id'] + end + + it "updates an existing product image" do + xhr :post, :update_product_image, product_id: product_with_image.id, file: image, use_route: :product_images + + expect(response.status).to eq 200 + expect(product_with_image.images.first.id).to eq json_response['id'] + end + end + end +end diff --git a/spec/controllers/user_passwords_controller_spec.rb b/spec/controllers/user_passwords_controller_spec.rb index d6abe4619c..59a2f16280 100644 --- a/spec/controllers/user_passwords_controller_spec.rb +++ b/spec/controllers/user_passwords_controller_spec.rb @@ -31,12 +31,16 @@ describe UserPasswordsController, type: :controller do end it "renders Darkswarm" do + Spree::MailMethod.create!(environment: 'test') clear_jobs + user.send_reset_password_instructions flush_jobs # Send the reset password instructions + user.reload spree_get :edit, reset_password_token: user.reset_password_token - response.should render_template "user_passwords/edit" + + expect(response).to render_template "user_passwords/edit" end describe "via ajax" do diff --git a/spec/factories.rb b/spec/factories.rb index 5e2a2da27c..e662e80509 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -425,6 +425,13 @@ FactoryGirl.define do stripe_user_id "abc123" stripe_publishable_key "xyz456" end + + factory :product_with_image, parent: :product do + after(:create) do |product| + image = File.open(Rails.root.join('app', 'assets', 'images', 'logo-white.png')) + Spree::Image.create(attachment: image, viewable_id: product.master.id, viewable_type: 'Spree::Variant') + end + end end diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 062735ccff..91f28d5498 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -744,4 +744,79 @@ feature %q{ expect(v.on_hand).to eq 18 end end + + describe "Updating product image with new upload interface" do + let!(:product) { create(:simple_product, name: "Carrots") } + + it "displays product images and image upload modal" do + quick_login_as_admin + visit '/admin/products/bulk_edit' + + within "table#listing_products tr#p_#{product.id}" do + # Displays product images + expect(page).to have_selector "td.image" + + # Shows default image when no image set + expect(page).to have_css "img[src='/assets/noimage/mini.png']" + @old_thumb_src = page.find("a.image-modal img")['src'] + + # Click image + page.find("a.image-modal").trigger('click') + end + + # Shows upload modal + expect(page).to have_selector "div.reveal-modal.product-image-upload" + + within "div.reveal-modal.product-image-upload" do + # Shows preview of current image + expect(page).to have_css "img.preview" + old_image_src = page.find("img.preview")['src'] + + # Upload a new image file + attach_file 'image-upload', Rails.root.join("public/500.jpg"), visible: false + + # Shows spinner whilst loading + expect(page).to have_css "img.spinner", visible: true + expect(page).to_not have_css "img.spinner", visible: true + + # Shows new image when finished + expect(page).to have_css "img.preview" + @new_image_src = page.find("img.preview")['src'] + expect(old_image_src) != @new_image_src + + # Close modal + page.find("a.close-reveal-modal").click + end + + expect(page).to_not have_selector "div.reveal-modal.product-image-upload" + + within "table#listing_products tr#p_#{product.id}" do + # New thumbnail is shown in image column + @new_thumb_src = page.find("a.image-modal img")['src'] + expect(@old_thumb_src) != @new_thumb_src + + page.find("a.image-modal").trigger('click') + end + + expect(page).to have_selector "div.reveal-modal.product-image-upload" + + within "div.reveal-modal.product-image-upload" do + # Upload another image file + attach_file 'image-upload', Rails.root.join("public/422.jpg"), visible: false + + # Overwrites existing image + expect(page).to have_css "img.preview" + newer_image_src = page.find("img.preview")['src'] + expect(@new_image_src) != newer_image_src + + page.find("a.close-reveal-modal").click + end + + within "table#listing_products tr#p_#{product.id}" do + # Newer thumbnail is shown in image column + newer_thumb_src = page.find("a.image-modal img")['src'] + expect(@new_thumb_src) != newer_thumb_src + end + end + end end diff --git a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee index ce4b967f90..a30eb4eeb8 100644 --- a/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/customers/controllers/customers_controller_spec.js.coffee @@ -10,9 +10,10 @@ describe "CustomersCtrl", -> null shops = [ - { name: "Shop 1", id: 1 } - { name: "Shop 2", id: 2 } - { name: "Shop 3", id: 3 } + { name: "Shop 1", id: 1 }, + { name: "Shop 2", id: 12 }, + { name: "Shop 3", id: 2 }, + { name: "Shop 4", id: 3 } ] availableCountries = [ @@ -39,9 +40,9 @@ describe "CustomersCtrl", -> beforeEach inject (pendingChanges) -> spyOn(pendingChanges, "removeAll") scope.customers_form = jasmine.createSpyObj('customers_form', ['$setPristine']) - http.expectGET('/admin/customers.json?enterprise_id=3').respond 200, customers + http.expectGET('/admin/customers.json?enterprise_id=2').respond 200, customers scope.$apply -> - scope.shop_id = 3 + scope.shop_id = "2" http.flush() it "sets the CurrentShop", inject (CurrentShop) -> @@ -81,7 +82,7 @@ describe "CustomersCtrl", -> { text: 'three' } ] beforeEach -> - http.expectGET('/admin/tag_rules/map_by_tag.json?enterprise_id=3').respond 200, tags + http.expectGET('/admin/tag_rules/map_by_tag.json?enterprise_id=2').respond 200, tags it "retrieves the tag list", -> promise = scope.findTags('') diff --git a/spec/mailers/enterprise_mailer_spec.rb b/spec/mailers/enterprise_mailer_spec.rb index fae627a0d8..a31a9ec0e2 100644 --- a/spec/mailers/enterprise_mailer_spec.rb +++ b/spec/mailers/enterprise_mailer_spec.rb @@ -6,14 +6,16 @@ describe EnterpriseMailer do before do ActionMailer::Base.deliveries = [] + Spree::MailMethod.create!(environment: 'test') end describe "#welcome" do - it "should send a welcome email when given an enterprise" do - EnterpriseMailer.welcome(enterprise).deliver - expect(ActionMailer::Base.deliveries.count).to eq 1 - mail = ActionMailer::Base.deliveries.first - expect(mail.subject).to eq "#{enterprise.name} is now on #{Spree::Config[:site_name]}" + it "sends a welcome email when given an enterprise" do + EnterpriseMailer.welcome(enterprise).deliver + + mail = ActionMailer::Base.deliveries.first + expect(mail.subject) + .to eq "#{enterprise.name} is now on #{Spree::Config[:site_name]}" end end diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index f1198ba39a..d8d9b58744 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -11,6 +11,8 @@ describe Spree::UserMailer do ActionMailer::Base.delivery_method = :test ActionMailer::Base.perform_deliveries = true ActionMailer::Base.deliveries = [] + + Spree::MailMethod.create!(environment: 'test') end it "sends an email when given a user" do diff --git a/spec/services/create_mail_method_spec.rb b/spec/services/create_mail_method_spec.rb new file mode 100644 index 0000000000..d57d5882a4 --- /dev/null +++ b/spec/services/create_mail_method_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe CreateMailMethod do + describe '#call' do + let(:mail_method) { Spree::MailMethod.create(environment: 'test') } + let(:mail_settings) { instance_double(Spree::Core::MailSettings) } + let(:attributes) do + { preferred_smtp_username: "smtp_username", environment: "test" } + end + + before do + allow(Spree::MailMethod) + .to receive(:create).with(environment: 'test').and_return(mail_method) + allow(Spree::Core::MailSettings).to receive(:init) { mail_settings } + end + + context 'unit' do + before do + allow(mail_method).to receive(:update_attributes).with(attributes) + end + + it 'creates a new MailMethod' do + described_class.new(attributes).call + + expect(Spree::MailMethod) + .to have_received(:create).with(environment: 'test') { mail_method } + end + + it 'updates the MailMethod' do + described_class.new(attributes).call + + expect(mail_method) + .to have_received(:update_attributes).with(attributes) { mail_method } + end + + it 'initializes the mail settings' do + described_class.new(attributes).call + expect(Spree::Core::MailSettings).to have_received(:init) + end + end + + context 'integration' do + it 'updates the mail method attributes' do + described_class.new(attributes).call + expect(mail_method.preferred_smtp_username).to eq('smtp_username') + end + end + end +end