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