Compare commits

..

1 Commits

Author SHA1 Message Date
Maikel Linke
95819c9a0f Avoid asking the cloud if an image exists
https://github.com/openfoodfoundation/openfoodnetwork/issues/4392

Amazon's DNS is failing at the moment and some users can't access the
admin panel because of this error.

While `exists?` asks the storage server if the file is actually there,
`file?` just checks if we have the file name stored in the database
and the file should be there. It's much faster and less error prone.
2019-10-23 11:45:15 +11:00
186 changed files with 3416 additions and 8528 deletions

View File

@@ -139,6 +139,7 @@ Metrics/LineLength:
- lib/open_food_network/payments_report.rb
- lib/open_food_network/permalink_generator.rb
- lib/open_food_network/products_cache.rb
- lib/open_food_network/products_renderer.rb
- lib/open_food_network/reports/bulk_coop_allocation_report.rb
- lib/open_food_network/reports/line_items.rb
- lib/open_food_network/sales_tax_report.rb
@@ -248,10 +249,13 @@ Metrics/LineLength:
- spec/helpers/order_cycles_helper_spec.rb
- spec/helpers/spree/admin/base_helper_spec.rb
- spec/jobs/confirm_order_job_spec.rb
- spec/jobs/products_cache_integrity_checker_job_spec.rb
- spec/jobs/refresh_products_cache_job_spec.rb
- spec/jobs/subscription_confirm_job_spec.rb
- spec/jobs/subscription_placement_job_spec.rb
- spec/lib/open_food_network/address_finder_spec.rb
- spec/lib/open_food_network/bulk_coop_report_spec.rb
- spec/lib/open_food_network/cached_products_renderer_spec.rb
- spec/lib/open_food_network/customers_report_spec.rb
- spec/lib/open_food_network/enterprise_fee_applicator_spec.rb
- spec/lib/open_food_network/enterprise_fee_calculator_spec.rb
@@ -268,6 +272,7 @@ Metrics/LineLength:
- spec/lib/open_food_network/permissions_spec.rb
- spec/lib/open_food_network/products_and_inventory_report_spec.rb
- spec/lib/open_food_network/products_cache_spec.rb
- spec/lib/open_food_network/products_renderer_spec.rb
- spec/lib/open_food_network/proxy_order_syncer_spec.rb
- spec/lib/open_food_network/scope_variant_to_hub_spec.rb
- spec/lib/open_food_network/subscription_payment_updater_spec.rb
@@ -610,6 +615,7 @@ Metrics/MethodLength:
- lib/open_food_network/payments_report.rb
- lib/open_food_network/permissions.rb
- lib/open_food_network/products_and_inventory_report.rb
- lib/open_food_network/products_renderer.rb
- lib/open_food_network/rack_request_blocker.rb
- lib/open_food_network/reports/bulk_coop_allocation_report.rb
- lib/open_food_network/reports/bulk_coop_supplier_report.rb
@@ -664,6 +670,7 @@ Metrics/ModuleLength:
- spec/controllers/api/orders_controller_spec.rb
- spec/controllers/spree/api/products_controller_spec.rb
- spec/lib/open_food_network/address_finder_spec.rb
- spec/lib/open_food_network/cached_products_renderer_spec.rb
- spec/lib/open_food_network/customers_report_spec.rb
- spec/lib/open_food_network/enterprise_fee_calculator_spec.rb
- spec/lib/open_food_network/option_value_namer_spec.rb

View File

@@ -1 +1 @@
2.2.10
2.1.9

View File

@@ -19,10 +19,6 @@ If you want to run the whole test suite, we recommend using a free CI service to
bundle exec rspec spec
## Which issue to pick first?
We have curated all issues interesting for new members of the community within the [Welcome New Developers project board][welcome-dev]. Have a look and pick the one you would prefer working on!
## Internationalisation (i18n)
The locale `en` is maintained in the source code, but other locales are managed at [Transifex][ofn-transifex]. Read more about [internationalisation][i18n] in the developer wiki.
@@ -66,4 +62,3 @@ From here, your pull request will progress through the [Review, Test, Merge & De
[slack-dev]: https://openfoodnetwork.slack.com/messages/C2GQ45KNU
[ofn-transifex]: https://www.transifex.com/open-food-foundation/open-food-network/
[i18n]: https://github.com/openfoodfoundation/openfoodnetwork/wiki/i18n
[welcome-dev]: https://github.com/openfoodfoundation/openfoodnetwork/projects/27

12
Gemfile
View File

@@ -1,9 +1,9 @@
source 'https://rubygems.org'
ruby "2.2.10"
ruby "2.1.9"
git_source(:github) { |repo_name| "https://github.com/#{repo_name}.git" }
gem 'i18n', '~> 0.6.11'
gem 'i18n-js', '~> 3.4.1'
gem 'i18n-js', '~> 3.3.0'
gem 'rails', '~> 3.2.22'
gem 'rails-i18n', '~> 3.0.0'
gem 'rails_safe_tasks', '~> 1.0'
@@ -15,12 +15,12 @@ gem 'nokogiri', '>= 1.6.7.1'
gem "order_management", path: "./engines/order_management"
gem 'web', path: './engines/web'
gem 'activerecord-postgresql-adapter'
gem 'pg', '~> 0.21.0'
gem 'pg'
# OFN-maintained and patched version of Spree v2.0.4. See
# https://github.com/openfoodfoundation/openfoodnetwork/wiki/Spree-2.0-upgrade
# for details.
gem 'spree_api', github: 'openfoodfoundation/spree', branch: '2-0-4-stable'
gem 'spree_backend', github: 'openfoodfoundation/spree', branch: '2-0-4-stable'
gem 'spree_core', github: 'openfoodfoundation/spree', branch: '2-0-4-stable'
@@ -98,8 +98,6 @@ gem 'roo-xls', '~> 1.1.0'
gem 'whenever', require: false
gem 'test-unit', '~> 3.0'
# Gems used only for assets and not required
# in production environments by default.
group :assets do
@@ -138,7 +136,7 @@ group :test, :development do
gem 'capybara', '>= 2.15.4'
gem 'database_cleaner', '0.7.1', require: false
gem "factory_bot_rails", require: false
gem 'fuubar', '~> 2.5.0'
gem 'fuubar', '~> 2.4.1'
gem 'json_spec', '~> 1.1.4'
gem 'knapsack'
gem 'letter_opener', '>= 1.4.1'

View File

@@ -129,10 +129,8 @@ GEM
activesupport (= 3.2.22.5)
arel (~> 3.0.2)
tzinfo (~> 0.3.29)
activerecord-import (1.0.3)
activerecord-import (1.0.2)
activerecord (>= 3.2)
activerecord-postgresql-adapter (0.0.1)
pg
activeresource (3.2.22.5)
activemodel (= 3.2.22.5)
activesupport (= 3.2.22.5)
@@ -166,7 +164,7 @@ GEM
bcrypt-ruby (3.1.5)
bcrypt (>= 3.1.3)
blockenspiel (0.5.0)
bugsnag (6.12.2)
bugsnag (6.12.1)
concurrent-ruby (~> 1.0)
builder (3.0.4)
byebug (9.0.6)
@@ -426,7 +424,7 @@ GEM
foundation-rails (5.5.2.1)
railties (>= 3.1.0)
sass (>= 3.3.0, < 3.5)
fuubar (2.5.0)
fuubar (2.4.1)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
geocoder (1.1.8)
@@ -439,7 +437,7 @@ GEM
httparty (0.16.2)
multi_xml (>= 0.5.2)
i18n (0.6.11)
i18n-js (3.4.1)
i18n-js (3.3.0)
i18n (>= 0.6.6)
immigrant (0.3.6)
activerecord (>= 3.0)
@@ -524,7 +522,6 @@ GEM
polyamorous (0.5.0)
activerecord (~> 3.0)
polyglot (0.3.5)
power_assert (1.1.5)
powerpack (0.1.1)
pry (0.12.2)
coderay (~> 1.1.0)
@@ -674,8 +671,6 @@ GEM
stripe (4.24.0)
faraday (~> 0.13)
net-http-persistent (~> 3.0)
test-unit (3.3.3)
power_assert
thor (0.20.3)
tilt (1.4.1)
timecop (0.9.1)
@@ -727,7 +722,6 @@ DEPENDENCIES
active_model_serializers (= 0.8.4)
activemerchant (~> 1.78)
activerecord-import
activerecord-postgresql-adapter
acts-as-taggable-on (~> 3.4)
andand
angular-rails-templates (~> 0.3.0)
@@ -763,12 +757,12 @@ DEPENDENCIES
foundation-icons-sass-rails
foundation-rails
foundation_rails_helper!
fuubar (~> 2.5.0)
fuubar (~> 2.4.1)
geocoder
gmaps4rails
haml
i18n (~> 0.6.11)
i18n-js (~> 3.4.1)
i18n-js (~> 3.3.0)
immigrant
jquery-migrate-rails
jquery-rails (= 3.0.4)
@@ -789,7 +783,7 @@ DEPENDENCIES
order_management!
paper_trail (~> 5.2.3)
paperclip (~> 3.4.1)
pg (~> 0.21.0)
pg
pry-byebug (>= 3.4.3)
rabl
rack-mini-profiler (< 1.0.0)
@@ -812,6 +806,7 @@ DEPENDENCIES
simple_form!
simplecov
spinjs-rails
spree_api!
spree_backend!
spree_core!
spree_i18n!
@@ -819,7 +814,6 @@ DEPENDENCIES
spring (= 1.7.2)
spring-commands-rspec
stripe
test-unit (~> 3.0)
timecop
truncate_html
turbo-sprockets-rails3
@@ -834,7 +828,7 @@ DEPENDENCIES
wkhtmltopdf-binary
RUBY VERSION
ruby 2.2.10p489
ruby 2.1.9p490
BUNDLED WITH
1.17.2

View File

@@ -3,20 +3,20 @@
# Open Food Network
The Open Food Network is an online marketplace for local food. It enables a network of independent online food stores that connects farmers and food hubs (including co-ops, online farmers markets, independent food businesses, etc) with individuals and local businesses. It gives farmers and food hubs an easier and fairer way to distribute their food.
The Open Food Network is an online marketplace for local food. It enables a network of independent online food stores that connect farmers and food hubs (including coops, online farmers' markets, independent food businesses etc); with individuals and local businesses. It gives farmers and food hubs an easier and fairer way to distribute their food.
Supported by the Open Food Foundation and a network of global affiliates, we are proudly open source and not-for-profit - we're trying to seriously disrupt the concentration of power in global agri-food systems, and we need as many smart people working together on this as possible.
We're part of global movement - get involved!
* Join the conversation [on Slack][slack-invite]. Make sure you introduce yourself in the #general channel.
* Join the conversation [on Slack][slack-invite]. Make sure you introduce yourself in the #general channel
* Head to [https://openfoodnetwork.org](https://openfoodnetwork.org) for more information about the global OFN project.
* Check out the [User Guide](https://guide.openfoodnetwork.org/) for a list of features and tutorials.
* Join our [discussion forum](https://community.openfoodnetwork.org).
## Contributing
If you are interested in contributing to the OFN in any capacity, please introduce yourself [on Slack][slack-invite], and have a look through our [Contributor Guide][contributor-guide].
If you are interested in contributing to the OFN in any capacity, please introducing yourself [on Slack][slack-invite], and have a look through our [Contributor Guide][contributor-guide]
Our [GETTING_STARTED](GETTING_STARTED.md) and [CONTRIBUTING](CONTRIBUTING.md) guides are the best place to start for developers looking to set up a development environment and make contributions to the codebase.

View File

@@ -1,4 +1,4 @@
angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $filter, $http, $window, BulkProducts, DisplayProperties, DirtyProducts, VariantUnitManager, StatusMessage, producers, Taxons, Columns, tax_categories, RequestMonitor) ->
angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $filter, $http, $window, BulkProducts, DisplayProperties, DirtyProducts, VariantUnitManager, StatusMessage, producers, Taxons, SpreeApiAuth, Columns, tax_categories, RequestMonitor) ->
$scope.StatusMessage = StatusMessage
$scope.columns = Columns.columns
@@ -39,7 +39,12 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
$scope.DisplayProperties = DisplayProperties
$scope.initialise = ->
$scope.fetchProducts()
SpreeApiAuth.authorise()
.then ->
$scope.spree_api_key_ok = true
$scope.fetchProducts()
.catch (message) ->
$scope.api_error_msg = message
$scope.$watchCollection '[query, producerFilter, categoryFilter, importDateFilter, per_page]', ->
$scope.page = 1 # Reset page when changing filters for new search
@@ -103,15 +108,9 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
$scope.categoryFilter = "0"
$scope.importDateFilter = "0"
confirm_unsaved_changes = () ->
(DirtyProducts.count() > 0 and confirm(t("unsaved_changes_confirmation"))) or (DirtyProducts.count() == 0)
editProductUrl = (product, variant) ->
"/admin/products/" + product.permalink_live + ((if variant then "/variants/" + variant.id else "")) + "/edit"
$scope.editWarn = (product, variant) ->
if confirm_unsaved_changes()
window.open(editProductUrl(product, variant), "_blank")
if (DirtyProducts.count() > 0 and confirm(t("unsaved_changes_confirmation"))) or (DirtyProducts.count() == 0)
window.location = "/admin/products/" + product.permalink_live + ((if variant then "/variants/" + variant.id else "")) + "/edit"
$scope.toggleShowAllVariants = ->

View File

@@ -0,0 +1,16 @@
angular.module("admin.indexUtils").factory "SpreeApiAuth", ($q, $http, SpreeApiKey) ->
new class SpreeApiAuth
authorise: ->
deferred = $q.defer()
$http.get("/api/users/authorise_api?token=" + SpreeApiKey)
.success (response) ->
if response?.success == "Use of API Authorised"
$http.defaults.headers.common["X-Spree-Token"] = SpreeApiKey
deferred.resolve()
.error (response) ->
error = response?.error || t('js.unauthorized')
deferred.reject(error)
deferred.promise

View File

@@ -1,4 +1,4 @@
angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", ($scope, $http, $timeout, Indexer, Columns, Views, PagedFetcher, StatusMessage, RequestMonitor, hubs, producers, hubPermissions, InventoryItems, VariantOverrides, DirtyVariantOverrides) ->
angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", ($scope, $http, $timeout, Indexer, Columns, Views, SpreeApiAuth, PagedFetcher, StatusMessage, RequestMonitor, hubs, producers, hubPermissions, InventoryItems, VariantOverrides, DirtyVariantOverrides) ->
$scope.hubs = Indexer.index hubs
$scope.hub_id = if hubs.length == 1 then hubs[0].id else null
$scope.products = []
@@ -39,7 +39,13 @@ angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl",
$scope.producerFilter != 0 || $scope.query != ''
$scope.initialise = ->
$scope.fetchProducts()
SpreeApiAuth.authorise()
.then ->
$scope.spree_api_key_ok = true
$scope.fetchProducts()
.catch (message) ->
$scope.api_error_msg = message
$scope.fetchProducts = ->
url = "/api/products/overridable?page=::page::;per_page=100"

View File

@@ -1,4 +1,4 @@
Darkswarm.controller "OrderCycleCtrl", ($scope, $rootScope, $timeout, OrderCycle) ->
Darkswarm.controller "OrderCycleCtrl", ($scope, $timeout, OrderCycle) ->
$scope.order_cycle = OrderCycle.order_cycle
$scope.OrderCycle = OrderCycle
@@ -6,12 +6,11 @@ Darkswarm.controller "OrderCycleCtrl", ($scope, $rootScope, $timeout, OrderCycle
# This is a hack. We should probably write our own "popover" directive
# That takes an expression instead of a trigger, and binds to that
$timeout =>
$rootScope.$broadcast 'orderCycleSelected'
if !$scope.OrderCycle.selected()
$("#order_cycle_id").trigger("openTrigger")
Darkswarm.controller "OrderCycleChangeCtrl", ($scope, $rootScope, $timeout, OrderCycle, Products, Variants, Cart, ChangeableOrdersAlert) ->
Darkswarm.controller "OrderCycleChangeCtrl", ($scope, $timeout, OrderCycle, Products, Variants, Cart, ChangeableOrdersAlert) ->
# Track previous order cycle id for use with revertOrderCycle()
$scope.previous_order_cycle_id = OrderCycle.order_cycle.order_cycle_id
$scope.$watch 'order_cycle.order_cycle_id', (newValue, oldValue)->
@@ -33,4 +32,3 @@ Darkswarm.controller "OrderCycleChangeCtrl", ($scope, $rootScope, $timeout, Orde
Products.update()
Cart.reloadFinalisedLineItems()
ChangeableOrdersAlert.reload()
$rootScope.$broadcast 'orderCycleSelected'

View File

@@ -1,65 +1,41 @@
Darkswarm.controller "ProductsCtrl", ($scope, $filter, $rootScope, Products, OrderCycle, OrderCycleResource, FilterSelectorsService, Cart, Dereferencer, Taxons, Properties, currentHub, $timeout) ->
Darkswarm.controller "ProductsCtrl", ($scope, $filter, $rootScope, Products, OrderCycle, FilterSelectorsService, Cart, Taxons, Properties) ->
$scope.Products = Products
$scope.Cart = Cart
$scope.query = ""
$scope.taxonSelectors = FilterSelectorsService.createSelectors()
$scope.propertySelectors = FilterSelectorsService.createSelectors()
$scope.filtersActive = true
$scope.page = 1
$scope.per_page = 10
$scope.limit = 10
$scope.order_cycle = OrderCycle.order_cycle
$scope.supplied_taxons = null
$scope.supplied_properties = null
# $scope.infiniteDisabled = true
$rootScope.$on "orderCycleSelected", ->
$scope.update_filters()
$scope.clearAll()
# All of this logic basically just replicates the functionality filtering an ng-repeat
# except that it allows us to filter a separate list before rendering, meaning that
# we can get much better performance when applying filters by resetting the limit on the
# number of products being rendered each time a filter is changed.
$scope.update_filters = ->
order_cycle_id = OrderCycle.order_cycle.order_cycle_id
$scope.$watch "Products.loading", (newValue, oldValue) ->
$scope.updateFilteredProducts()
$scope.$broadcast("loadFilterSelectors") if !newValue
return unless order_cycle_id
$scope.incrementLimit = ->
if $scope.limit < Products.products.length
$scope.limit += 10
$scope.updateVisibleProducts()
params = {
id: order_cycle_id,
distributor: currentHub.id
}
OrderCycleResource.taxons params, (data)=>
$scope.supplied_taxons = {}
data.map( (taxon) ->
$scope.supplied_taxons[taxon.id] = Taxons.taxons_by_id[taxon.id]
)
OrderCycleResource.properties params, (data)=>
$scope.supplied_properties = {}
data.map( (property) ->
$scope.supplied_properties[property.id] = Properties.properties_by_id[property.id]
)
$scope.$watch 'query', -> $scope.updateFilteredProducts()
$scope.$watchCollection 'activeTaxons', -> $scope.updateFilteredProducts()
$scope.$watchCollection 'activeProperties', -> $scope.updateFilteredProducts()
$scope.loadMore = ->
if ($scope.page * $scope.per_page) <= Products.products.length
$scope.loadMoreProducts()
$scope.updateFilteredProducts = ->
$scope.limit = 10
f1 = $filter('products')(Products.products, $scope.query)
f2 = $filter('taxons')(f1, $scope.activeTaxons)
$scope.filteredProducts = $filter('properties')(f2, $scope.activeProperties)
$scope.updateVisibleProducts()
$scope.$watch 'query', (newValue, oldValue) -> $scope.loadProducts() if newValue != oldValue
$scope.$watchCollection 'activeTaxons', (newValue, oldValue) -> $scope.loadProducts() if newValue != oldValue
$scope.$watchCollection 'activeProperties', (newValue, oldValue) -> $scope.loadProducts() if newValue != oldValue
$scope.loadProducts = ->
$scope.page = 1
Products.update($scope.queryParams())
$scope.loadMoreProducts = ->
Products.update($scope.queryParams($scope.page + 1), true)
$scope.page += 1
$scope.queryParams = (page = null) ->
{
id: $scope.order_cycle.order_cycle_id,
page: page || $scope.page,
per_page: $scope.per_page,
'q[name_or_meta_keywords_or_supplier_name_cont]': $scope.query,
'q[properties_id_or_supplier_properties_id_in_any][]': $scope.activeProperties,
'q[primary_taxon_id_in_any][]': $scope.activeTaxons
}
$scope.updateVisibleProducts = ->
$scope.visibleProducts = $filter('limitTo')($scope.filteredProducts, $scope.limit)
$scope.searchKeypress = (e)->
code = e.keyCode || e.which

View File

@@ -1,21 +0,0 @@
Darkswarm.factory 'OrderCycleResource', ($resource) ->
$resource('/api/order_cycles/:id', {}, {
'products':
method: 'GET'
isArray: true
url: '/api/order_cycles/:id/products'
params:
id: '@id'
'taxons':
method: 'GET'
isArray: true
url: '/api/order_cycles/:id/taxons'
params:
id: '@id'
'properties':
method: 'GET'
isArray: true
url: '/api/order_cycles/:id/properties'
params:
id: '@id'
})

View File

@@ -1,34 +1,26 @@
Darkswarm.factory 'Products', (OrderCycleResource, OrderCycle, Shopfront, currentHub, Dereferencer, Taxons, Properties, Cart, Variants) ->
Darkswarm.factory 'Products', ($resource, Shopfront, Dereferencer, Taxons, Properties, Cart, Variants) ->
new class Products
constructor: ->
@update()
products: []
fetched_products: []
# TODO: don't need to scope this into object
# Already on object as far as controller scope is concerned
products: null
loading: true
update: (params = {}, load_more = false) =>
update: =>
@loading = true
order_cycle_id = OrderCycle.order_cycle.order_cycle_id
@products = []
$resource("/shop/products").query (products)=>
@products = products
if order_cycle_id == undefined
@loading = false
return
params['id'] = order_cycle_id
params['distributor'] = currentHub.id
OrderCycleResource.products params, (data)=>
@products = [] unless load_more
@fetched_products = data
@extend()
@dereference()
@registerVariants()
@products = @products.concat(@fetched_products)
@loading = false
extend: ->
for product in @fetched_products
for product in @products
if product.variants?.length > 0
prices = (v.price for v in product.variants)
product.price = Math.min.apply(null, prices)
@@ -38,7 +30,7 @@ Darkswarm.factory 'Products', (OrderCycleResource, OrderCycle, Shopfront, curren
product.largeImage = product.images[0]?.large_url if product.images
dereference: ->
for product in @fetched_products
for product in @products
product.supplier = Shopfront.producers_by_id[product.supplier.id]
Dereferencer.dereference product.taxons, Taxons.taxons_by_id
@@ -48,7 +40,7 @@ Darkswarm.factory 'Products', (OrderCycleResource, OrderCycle, Shopfront, curren
# May return different objects! If the variant has already been registered
# by another service, we fetch those
registerVariants: ->
for product in @fetched_products
for product in @products
if product.variants
product.variant_names = ""
product.variants = for variant in product.variants

View File

@@ -17,7 +17,7 @@
.small-4.medium-2.large-2.columns.variant-price
.table-cell.price
%i.ofn-i_009-close
{{ variant.price_with_fees | localizeCurrency }}
{{ ::variant.price_with_fees | localizeCurrency }}
-# Now in a template in app/assets/javascripts/templates !
%price-breakdown{"price-breakdown" => "_", variant: "variant",

View File

@@ -255,9 +255,6 @@ text-angular {
background-color: #4583bf;
}
}
a {
color: $spree-green
}
}
span.required {

View File

@@ -0,0 +1,27 @@
require 'open_food_network/products_cache_integrity_checker'
module Admin
class CacheSettingsController < Spree::Admin::BaseController
def edit
@results = Exchange.cachable.map do |exchange|
checker = OpenFoodNetwork::ProductsCacheIntegrityChecker
.new(exchange.receiver, exchange.order_cycle)
{
distributor: exchange.receiver,
order_cycle: exchange.order_cycle,
status: checker.ok?,
diff: checker.diff
}
end
end
def update
Spree::Config.set(params[:preferences])
respond_to do |format|
format.html { redirect_to main_app.edit_admin_cache_settings_path }
end
end
end
end

View File

@@ -1,44 +1,16 @@
# Base controller for OFN's API
require_dependency 'spree/api/controller_setup'
# Includes the minimum machinery required by ActiveModelSerializers
module Api
class BaseController < ActionController::Metal
include Spree::Api::ControllerSetup
include Spree::Core::ControllerHelpers::SSL
include ::ActionController::Head
respond_to :json
attr_accessor :current_api_user
before_filter :set_content_type
before_filter :authenticate_user
after_filter :set_jsonp_format
rescue_from Exception, with: :error_during_processing
rescue_from CanCan::AccessDenied, with: :unauthorized
rescue_from ActiveRecord::RecordNotFound, with: :not_found
helper Spree::Api::ApiHelpers
ssl_allowed
# Include these because we inherit from ActionController::Metal
# rather than ActionController::Base and these are required for AMS
class BaseController < Spree::Api::BaseController
# Need to include these because Spree::Api::BaseContoller inherits
# from ActionController::Metal rather than ActionController::Base
# and they are required by ActiveModelSerializers
include ActionController::Serialization
include ActionController::UrlFor
include Rails.application.routes.url_helpers
use_renderers :json
check_authorization
def set_jsonp_format
return unless params[:callback] && request.get?
self.response_body = "#{params[:callback]}(#{response_body})"
headers["Content-Type"] = 'application/javascript'
end
def respond_with_conflict(json_hash)
render json: json_hash, status: :conflict
end
@@ -47,62 +19,16 @@ module Api
# Use logged in user (spree_current_user) for API authentication (current_api_user)
def authenticate_user
return if @current_api_user = try_spree_current_user
if api_key.blank?
# An anonymous user
@current_api_user = Spree.user_class.new
return
end
return if @current_api_user = Spree.user_class.find_by_spree_api_key(api_key.to_s)
invalid_api_key
@current_api_user = try_spree_current_user
super
end
def set_content_type
content_type = case params[:format]
when "json"
"application/json"
when "xml"
"text/xml"
end
headers["Content-Type"] = content_type
end
def error_during_processing(exception)
render(text: { exception: exception.message }.to_json,
status: :unprocessable_entity) && return
end
def current_ability
Spree::Ability.new(current_api_user)
end
def api_key
request.headers["X-Spree-Token"] || params[:token]
end
helper_method :api_key
def invalid_resource!(resource)
@resource = resource
render(json: { error: I18n.t(:invalid_resource, scope: "spree.api"),
errors: @resource.errors },
status: :unprocessable_entity)
end
def invalid_api_key
render(json: { error: I18n.t(:invalid_api_key, key: api_key, scope: "spree.api") },
status: :unauthorized) && return
end
def unauthorized
render(json: { error: I18n.t(:unauthorized, scope: "spree.api") },
status: :unauthorized) && return
end
def not_found
render(json: { error: I18n.t(:resource_not_found, scope: "spree.api") },
status: :not_found) && return
# Allows API access without authentication, but only for OFN controllers which inherit
# from Api::BaseController. @current_api_user will now initialize an empty Spree::User
# unless one is present. We now also apply devise's `check_authorization`. See here for
# details: https://github.com/CanCanCommunity/cancancan/wiki/Ensure-Authorization
def requires_authentication?
false
end
end
end

View File

@@ -1,88 +0,0 @@
module Api
class OrderCyclesController < BaseController
include EnterprisesHelper
respond_to :json
skip_authorization_check
def products
products = ProductsRenderer.new(
distributor,
order_cycle,
customer,
search_params
).products_json
render json: products
rescue ProductsRenderer::NoProducts
render status: :not_found, json: ''
end
def taxons
taxons = Spree::Taxon.
joins(:products).
where(spree_products: { id: distributed_products }).
select('DISTINCT spree_taxons.*')
render json: ActiveModel::ArraySerializer.new(taxons, each_serializer: Api::TaxonSerializer)
end
def properties
render json: ActiveModel::ArraySerializer.new(
product_properties | producer_properties, each_serializer: Api::PropertySerializer
)
end
private
def product_properties
Spree::Property.
joins(:products).
where(spree_products: { id: distributed_products }).
select('DISTINCT spree_properties.*')
end
def producer_properties
producers = Enterprise.
joins(:supplied_products).
where(spree_products: { id: distributed_products })
Spree::Property.
joins(:producer_properties).
where(producer_properties: { producer_id: producers }).
select('DISTINCT spree_properties.*')
end
def search_params
permitted_search_params = params.slice :q, :page, :per_page
if permitted_search_params.key? :q
permitted_search_params[:q].slice!(*permitted_ransack_params)
end
permitted_search_params
end
def permitted_ransack_params
[:name_or_meta_keywords_or_supplier_name_cont,
:properties_id_or_supplier_properties_id_in_any,
:primary_taxon_id_in_any]
end
def distributor
Enterprise.find_by_id(params[:distributor])
end
def order_cycle
OrderCycle.find_by_id(params[:id])
end
def customer
@current_api_user.andand.customer_of(distributor) || nil
end
def distributed_products
OrderCycleDistributedProducts.new(distributor, order_cycle, customer).products_relation
end
end
end

View File

@@ -47,6 +47,7 @@ module Api
render json: @product, serializer: Api::Admin::ProductSerializer, status: 204
end
# TODO: This should be named 'managed'. Is the action above used? Maybe we should remove it.
def bulk_products
product_query = OpenFoodNetwork::Permissions.new(current_api_user).
editable_products.merge(product_scope)
@@ -93,13 +94,10 @@ module Api
private
def find_product(id)
product_scope.find_by_permalink!(id.to_s)
rescue ActiveRecord::RecordNotFound
product_scope.find(id)
end
# Copied and modified from SpreeApi::BaseController to allow
# enterprise users to access inactive products
def product_scope
# This line modified
if current_api_user.has_spree_role?("admin") || current_api_user.enterprises.present?
scope = Spree::Product
if params[:show_deleted]

View File

@@ -1,3 +1,5 @@
require 'open_food_network/cached_products_renderer'
class ShopController < BaseController
layout "darkswarm"
before_filter :require_distributor_chosen, :set_order_cycles, except: :changeable_orders_alert
@@ -7,6 +9,19 @@ class ShopController < BaseController
redirect_to main_app.enterprise_shop_path(current_distributor)
end
def products
renderer = OpenFoodNetwork::CachedProductsRenderer.new(current_distributor,
current_order_cycle)
# If we add any more filtering logic, we should probably
# move it all to a lib class like 'CachedProductsFilterer'
products_json = filter(renderer.products_json)
render json: products_json
rescue OpenFoodNetwork::CachedProductsRenderer::NoProducts
render status: :not_found, json: ''
end
def order_cycle
if request.post?
if oc = OrderCycle.with_distributor(@distributor).active.find_by_id(params[:order_cycle_id])
@@ -24,4 +39,27 @@ class ShopController < BaseController
def changeable_orders_alert
render layout: false
end
private
def filtered_json(products_json)
if applicator.rules.any?
filter(products_json)
else
products_json
end
end
def filter(products_json)
products_hash = JSON.parse(products_json)
applicator.filter!(products_hash)
JSON.unparse(products_hash)
end
def applicator
return @applicator unless @applicator.nil?
@applicator = OpenFoodNetwork::TagRuleApplicator.new(current_distributor,
"FilterProducts",
current_customer.andand.tag_list)
end
end

View File

@@ -46,11 +46,8 @@ Spree::Admin::ProductsController.class_eval do
end
def update
original_supplier_id = @product.supplier_id
delete_stock_params_and_set_after do
super
ExchangeVariantDeleter.new.delete(@product) if original_supplier_id != @product.supplier_id
end
end

View File

@@ -109,7 +109,7 @@ Spree::Admin::ReportsController.class_eval do
end
def orders_and_fulfillment
params[:q] ||= orders_and_fulfillment_default_filters
params[:q] ||= {}
# -- Prepare Form Options
permissions = OpenFoodNetwork::Permissions.new(spree_current_user)
@@ -278,10 +278,4 @@ Spree::Admin::ReportsController.class_eval do
def timestamp
Time.zone.now.strftime("%Y%m%d")
end
def orders_and_fulfillment_default_filters
now = Time.zone.now
{ completed_at_gt: (now - 1.month).beginning_of_day,
completed_at_lt: (now + 1.day).beginning_of_day }
end
end

View File

@@ -58,14 +58,14 @@ module Spree
def generate_api_key
if @user.generate_spree_api_key!
flash[:success] = t('spree.api.key_generated')
flash[:success] = Spree.t('api.key_generated')
end
redirect_to edit_admin_user_path(@user)
end
def clear_api_key
if @user.clear_spree_api_key!
flash[:success] = t('spree.api.key_cleared')
flash[:success] = Spree.t('api.key_cleared')
end
redirect_to edit_admin_user_path(@user)
end

View File

@@ -0,0 +1,130 @@
require_dependency 'spree/api/controller_setup'
module Spree
module Api
class BaseController < ActionController::Metal
include Spree::Api::ControllerSetup
include Spree::Core::ControllerHelpers::SSL
include ::ActionController::Head
self.responder = Spree::Api::Responders::AppResponder
respond_to :json
attr_accessor :current_api_user
before_filter :set_content_type
before_filter :check_for_user_or_api_key, :if => :requires_authentication?
before_filter :authenticate_user
after_filter :set_jsonp_format
rescue_from Exception, :with => :error_during_processing
rescue_from CanCan::AccessDenied, :with => :unauthorized
rescue_from ActiveRecord::RecordNotFound, :with => :not_found
helper Spree::Api::ApiHelpers
ssl_allowed
def set_jsonp_format
if params[:callback] && request.get?
self.response_body = "#{params[:callback]}(#{response_body})"
headers["Content-Type"] = 'application/javascript'
end
end
def map_nested_attributes_keys(klass, attributes)
nested_keys = klass.nested_attributes_options.keys
attributes.inject({}) do |h, (k, v)|
key = nested_keys.include?(k.to_sym) ? "#{k}_attributes" : k
h[key] = v
h
end.with_indifferent_access
end
private
def set_content_type
content_type = case params[:format]
when "json"
"application/json"
when "xml"
"text/xml"
end
headers["Content-Type"] = content_type
end
def check_for_user_or_api_key
# User is already authenticated with Spree, make request this way instead.
return true if @current_api_user = try_spree_current_user ||
!requires_authentication?
return if api_key.present?
render("spree/api/errors/must_specify_api_key", status: :unauthorized) && return
end
def authenticate_user
return if @current_api_user
if requires_authentication? || api_key.present?
unless @current_api_user = Spree.user_class.find_by_spree_api_key(api_key.to_s)
render("spree/api/errors/invalid_api_key", status: :unauthorized) && return
end
else
# An anonymous user
@current_api_user = Spree.user_class.new
end
end
def unauthorized
render("spree/api/errors/unauthorized", status: :unauthorized) && return
end
def error_during_processing(exception)
render(text: { exception: exception.message }.to_json,
status: :unprocessable_entity) && return
end
def requires_authentication?
true
end
def not_found
render("spree/api/errors/not_found", status: :not_found) && return
end
def current_ability
Spree::Ability.new(current_api_user)
end
def invalid_resource!(resource)
@resource = resource
render "spree/api/errors/invalid_resource", status: :unprocessable_entity
end
def api_key
request.headers["X-Spree-Token"] || params[:token]
end
helper_method :api_key
def find_product(id)
product_scope.find_by_permalink!(id.to_s)
rescue ActiveRecord::RecordNotFound
product_scope.find(id)
end
def product_scope
if current_api_user.has_spree_role?("admin")
scope = Product
if params[:show_deleted]
scope = scope.with_deleted
end
else
scope = Product.active
end
scope.includes(:master)
end
end
end
end

View File

@@ -0,0 +1,7 @@
module Spree
module Api
class UsersController < Spree::Api::BaseController
respond_to :json
end
end
end

View File

@@ -0,0 +1,30 @@
require 'open_food_network/products_cache_integrity_checker'
ProductsCacheIntegrityCheckerJob = Struct.new(:distributor_id, :order_cycle_id) do
def perform
unless checker.ok?
exception = RuntimeError.new(
"Products JSON differs from cached version for distributor: #{distributor_id}, " \
"order cycle: #{order_cycle_id}"
)
Bugsnag.notify(exception) do |report|
report.add_tab(:products_cache, diff: checker.diff.to_s(:text))
end
end
end
private
def checker
OpenFoodNetwork::ProductsCacheIntegrityChecker.new(distributor, order_cycle)
end
def distributor
Enterprise.find distributor_id
end
def order_cycle
OrderCycle.find order_cycle_id
end
end

View File

@@ -0,0 +1,21 @@
require 'open_food_network/products_renderer'
RefreshProductsCacheJob = Struct.new(:distributor_id, :order_cycle_id) do
def perform
Rails.cache.write(key, products_json)
rescue ActiveRecord::RecordNotFound
true
end
private
def key
"products-json-#{distributor_id}-#{order_cycle_id}"
end
def products_json
distributor = Enterprise.find distributor_id
order_cycle = OrderCycle.find order_cycle_id
OpenFoodNetwork::ProductsRenderer.new(distributor, order_cycle).products_json
end
end

View File

@@ -1,4 +1,13 @@
class CoordinatorFee < ActiveRecord::Base
belongs_to :order_cycle
belongs_to :enterprise_fee
after_save :refresh_products_cache
after_destroy :refresh_products_cache
private
def refresh_products_cache
order_cycle.refresh_products_cache
end
end

View File

@@ -10,6 +10,10 @@ class EnterpriseFee < ActiveRecord::Base
has_many :exchange_fees, dependent: :destroy
has_many :exchanges, through: :exchange_fees
after_save :refresh_products_cache
# After destroy, the products cache is refreshed via the after_destroy hook for
# coordinator_fees and exchange_fees
attr_accessible :enterprise_id, :fee_type, :name, :tax_category_id, :calculator_type, :inherits_tax_category
FEE_TYPES = %w(packing transport admin sales fundraising).freeze
@@ -55,4 +59,8 @@ class EnterpriseFee < ActiveRecord::Base
end
true
end
def refresh_products_cache
OpenFoodNetwork::ProductsCache.enterprise_fee_changed self
end
end

View File

@@ -23,6 +23,9 @@ class Exchange < ActiveRecord::Base
validates :order_cycle, :sender, :receiver, presence: true
validates :sender_id, uniqueness: { scope: [:order_cycle_id, :receiver_id, :incoming] }
after_save :refresh_products_cache
after_destroy :refresh_products_cache_from_destroy
accepts_nested_attributes_for :variants
scope :in_order_cycle, lambda { |order_cycle| where(order_cycle_id: order_cycle) }
@@ -95,4 +98,12 @@ class Exchange < ActiveRecord::Base
def participant
incoming? ? sender : receiver
end
def refresh_products_cache
OpenFoodNetwork::ProductsCache.exchange_changed self
end
def refresh_products_cache_from_destroy
OpenFoodNetwork::ProductsCache.exchange_destroyed self
end
end

View File

@@ -1,4 +1,13 @@
class ExchangeFee < ActiveRecord::Base
belongs_to :exchange
belongs_to :enterprise_fee
after_save :refresh_products_cache
after_destroy :refresh_products_cache
private
def refresh_products_cache
exchange.refresh_products_cache
end
end

View File

@@ -1,3 +1,5 @@
require 'open_food_network/products_cache'
class InventoryItem < ActiveRecord::Base
attr_accessible :enterprise, :enterprise_id, :variant, :variant_id, :visible
@@ -11,4 +13,12 @@ class InventoryItem < ActiveRecord::Base
scope :visible, -> { where(visible: true) }
scope :hidden, -> { where(visible: false) }
after_save :refresh_products_cache
private
def refresh_products_cache
OpenFoodNetwork::ProductsCache.inventory_item_changed self
end
end

View File

@@ -23,6 +23,8 @@ class OrderCycle < ActiveRecord::Base
validates :name, :coordinator_id, presence: true
validate :orders_close_at_after_orders_open_at?
after_save :refresh_products_cache
preference :product_selection_from_coordinator_inventory_only, :boolean, default: false
scope :active, lambda {
@@ -245,6 +247,10 @@ class OrderCycle < ActiveRecord::Base
coordinator.users.include? user
end
def refresh_products_cache
OpenFoodNetwork::ProductsCache.order_cycle_changed self
end
def items_bought_by_user(user, distributor)
# The Spree::Order.complete scope only checks for completed_at date
# it does not ensure state is "complete"

View File

@@ -4,6 +4,9 @@ class ProducerProperty < ActiveRecord::Base
default_scope order("#{table_name}.position")
after_save :refresh_products_cache
after_destroy :refresh_products_cache_from_destroy
scope :ever_sold_by, ->(shop) {
joins(producer: { supplied_products: { variants: { exchanges: :order_cycle } } }).
merge(Exchange.outgoing).
@@ -26,4 +29,14 @@ class ProducerProperty < ActiveRecord::Base
Spree::Property.create(name: name, presentation: name)
end
end
private
def refresh_products_cache
OpenFoodNetwork::ProductsCache.producer_property_changed self
end
def refresh_products_cache_from_destroy
OpenFoodNetwork::ProductsCache.producer_property_destroyed self
end
end

View File

@@ -2,9 +2,16 @@ Spree::Classification.class_eval do
belongs_to :product, class_name: "Spree::Product", touch: true
before_destroy :dont_destroy_if_primary_taxon
after_destroy :refresh_products_cache
after_save :refresh_products_cache
private
def refresh_products_cache
product = Spree::Product.with_deleted.find(product_id) if product.blank?
product.refresh_products_cache
end
def dont_destroy_if_primary_taxon
if product.primary_taxon == taxon
errors.add :base, I18n.t(:spree_classification_primary_taxon_error, taxon: taxon.name, product: product.name)

View File

@@ -1,4 +1,7 @@
Spree::Image.class_eval do
after_save :refresh_products_cache
after_destroy :refresh_products_cache
# Spree stores attachent definitions in JSON. This converts the style name and format to
# strings. However, when paperclip encounters these, it doesn't recognise the format.
# Here we solve that problem by converting format and style name to symbols.
@@ -20,4 +23,10 @@ Spree::Image.class_eval do
end
reformat_styles
private
def refresh_products_cache
viewable.try :refresh_products_cache
end
end

View File

@@ -1,5 +1,12 @@
module Spree
OptionType.class_eval do
has_many :products, through: :product_option_types
after_save :refresh_products_cache
private
def refresh_products_cache
products(:reload).each(&:refresh_products_cache)
end
end
end

View File

@@ -0,0 +1,18 @@
module Spree
OptionValue.class_eval do
after_save :refresh_products_cache
around_destroy :refresh_products_cache_from_destroy
private
def refresh_products_cache
variants(:reload).each(&:refresh_products_cache)
end
def refresh_products_cache_from_destroy
vs = variants(:reload).to_a
yield
vs.each(&:refresh_products_cache)
end
end
end

View File

@@ -0,0 +1,30 @@
require 'open_food_network/products_cache'
module Spree
Preference.class_eval do
after_save :refresh_products_cache
# When the setting preferred_product_selection_from_inventory_only has changed, we want to
# refresh all active exchanges for this enterprise.
def refresh_products_cache
if product_selection_from_inventory_only_changed?
OpenFoodNetwork::ProductsCache.distributor_changed(enterprise)
end
end
private
def product_selection_from_inventory_only_changed?
!!(key =~ product_selection_from_inventory_only_regex)
end
def enterprise
enterprise_id = key.match(product_selection_from_inventory_only_regex)[1]
Enterprise.find(enterprise_id)
end
def product_selection_from_inventory_only_regex
/^enterprise\/product_selection_from_inventory_only\/(\d+)$/
end
end
end

View File

@@ -2,6 +2,8 @@ module Spree
Price.class_eval do
acts_as_paranoid without_default_scope: true
after_save :refresh_products_cache
# Allow prices to access associated soft-deleted variants.
def variant
Spree::Variant.unscoped { super }
@@ -14,5 +16,9 @@ module Spree
self.currency = Spree::Config[:currency]
end
end
def refresh_products_cache
variant.andand.refresh_products_cache
end
end
end

View File

@@ -38,6 +38,7 @@ Spree::Product.class_eval do
after_save :remove_previous_primary_taxon_from_taxons
after_save :ensure_standard_variant
after_save :update_units
after_save :refresh_products_cache
# -- Joins
scope :with_order_cycles_outer, -> {
@@ -191,17 +192,23 @@ Spree::Product.class_eval do
def destroy_with_delete_from_order_cycles
transaction do
touch_distributors
OpenFoodNetwork::ProductsCache.product_deleted(self) do
touch_distributors
ExchangeVariant.
where('exchange_variants.variant_id IN (?)', variants_including_master.with_deleted.
select(:id)).destroy_all
ExchangeVariant.
where('exchange_variants.variant_id IN (?)', variants_including_master.with_deleted.
select(:id)).destroy_all
destroy_without_delete_from_order_cycles
destroy_without_delete_from_order_cycles
end
end
end
alias_method_chain :destroy, :delete_from_order_cycles
def refresh_products_cache
OpenFoodNetwork::ProductsCache.product_changed self
end
private
def set_available_on_to_now

View File

@@ -1,5 +1,10 @@
module Spree
ProductProperty.class_eval do
belongs_to :product, class_name: "Spree::Product", touch: true
after_save :refresh_products_cache
after_destroy :refresh_products_cache
delegate :refresh_products_cache, to: :product
end
end

View File

@@ -3,20 +3,6 @@ class Spree::ProductSet < ModelSet
super(Spree::Product, [], attributes, proc { |attrs| attrs[:product_id].blank? })
end
def save
@collection_hash.each_value.all? do |product_attributes|
update_attributes(product_attributes)
end
end
def collection_attributes=(attributes)
@collection = Spree::Product
.where(id: attributes.each_value.map { |product| product[:id] })
@collection_hash = attributes
end
private
# A separate method of updating products was required due to an issue with
# the way Rails' assign_attributes and updates_attributes behave when
# delegated attributes of a nested object are updated via the parent object
@@ -33,11 +19,14 @@ class Spree::ProductSet < ModelSet
def update_attributes(attributes)
split_taxon_ids!(attributes)
product = find_model(@collection, attributes[:id])
if product.nil?
found_model = @collection.find do |model|
model.id.to_s == attributes[:id].to_s && model.persisted?
end
if found_model.nil?
@klass.new(attributes).save unless @reject_if.andand.call(attributes)
else
update_product(product, attributes)
update_product(found_model, attributes)
end
end
@@ -45,34 +34,28 @@ class Spree::ProductSet < ModelSet
attributes[:taxon_ids] = attributes[:taxon_ids].split(',') if attributes[:taxon_ids].present?
end
def update_product(product, attributes)
original_supplier = product.supplier_id
return false unless update_product_only_attributes(product, attributes)
ExchangeVariantDeleter.new.delete(product) if original_supplier != product.supplier_id
update_product_variants(product, attributes) &&
update_product_master(product, attributes)
def update_product(found_model, attributes)
update_product_only_attributes(found_model, attributes) &&
update_product_variants(found_model, attributes) &&
update_product_master(found_model, attributes)
end
def update_product_only_attributes(product, attributes)
variant_related_attrs = [:id, :variants_attributes, :master_attributes]
product_related_attrs = attributes.except(*variant_related_attrs)
return true if product_related_attrs.blank?
product.assign_attributes(product_related_attrs)
validate_presence_of_unit_value_in_product(product)
product.errors.empty? && product.save
end
def validate_presence_of_unit_value_in_product(product)
product.variants.each do |variant|
validate_presence_of_unit_value_in_variant(product, variant)
validate_presence_of_unit_value(product, variant)
end
product.save if errors.empty?
end
def validate_presence_of_unit_value_in_variant(product, variant)
def validate_presence_of_unit_value(product, variant)
return unless %w(weight volume).include?(product.variant_unit)
return if variant.unit_value.present?
@@ -96,9 +79,12 @@ class Spree::ProductSet < ModelSet
end
def create_or_update_variant(product, variant_attributes)
variant = find_model(product.variants_including_master, variant_attributes[:id])
if variant.present?
variant.update_attributes(variant_attributes.except(:id))
found_variant = product.variants_including_master.find do |variant|
variant.id.to_s == variant_attributes[:id].to_s && variant.persisted?
end
if found_variant.present?
found_variant.update_attributes(variant_attributes.except(:id))
else
create_variant(product, variant_attributes)
end
@@ -129,9 +115,15 @@ class Spree::ProductSet < ModelSet
end
end
def find_model(collection, model_id)
collection.find do |model|
model.id.to_s == model_id.to_s && model.persisted?
def collection_attributes=(attributes)
@collection = Spree::Product
.where(id: attributes.each_value.map { |product| product[:id] })
@collection_hash = attributes
end
def save
@collection_hash.each_value.all? do |product_attributes|
update_attributes(product_attributes)
end
end
end

View File

@@ -20,8 +20,19 @@ module Spree
merge(OrderCycle.active)
}
after_save :refresh_products_cache
# When a Property is destroyed, dependent-destroy will destroy all ProductProperties,
# which will take care of refreshing the products cache
def property
self
end
private
def refresh_products_cache
product_properties(:reload).each(&:refresh_products_cache)
end
end
end

View File

@@ -0,0 +1,10 @@
Spree::StockMovement.class_eval do
after_save :refresh_products_cache
private
def refresh_products_cache
return if stock_item.variant.blank?
OpenFoodNetwork::ProductsCache.variant_changed stock_item.variant
end
end

View File

@@ -4,6 +4,8 @@ Spree::Taxon.class_eval do
attachment_definitions[:icon][:path] = 'public/images/spree/taxons/:id/:style/:basename.:extension'
attachment_definitions[:icon][:url] = '/images/spree/taxons/:id/:style/:basename.:extension'
after_save :refresh_products_cache
# Indicate which filters should be used for this taxon
def applicable_filters
fs = []
@@ -47,4 +49,10 @@ Spree::Taxon.class_eval do
ts[t.enterprise_id.to_i] << t.id
end
end
private
def refresh_products_cache
products(:reload).each(&:refresh_products_cache)
end
end

View File

@@ -1,6 +1,7 @@
require 'open_food_network/enterprise_fee_calculator'
require 'open_food_network/variant_and_line_item_naming'
require 'concerns/variant_stock'
require 'open_food_network/products_cache'
Spree::Variant.class_eval do
extend Spree::LocalizedNumber
@@ -29,6 +30,7 @@ Spree::Variant.class_eval do
before_validation :update_weight_from_unit_value, if: ->(v) { v.product.present? }
after_save :update_units
after_save :refresh_products_cache
around_destroy :destruction
scope :with_order_cycles_inner, -> { joins(exchanges: :order_cycle) }
@@ -106,6 +108,14 @@ Spree::Variant.class_eval do
OpenFoodNetwork::EnterpriseFeeCalculator.new(distributor, order_cycle).fees_by_type_for self
end
def refresh_products_cache
if is_master?
product.refresh_products_cache
else
OpenFoodNetwork::ProductsCache.variant_changed self
end
end
private
def update_weight_from_unit_value
@@ -113,7 +123,21 @@ Spree::Variant.class_eval do
end
def destruction
exchange_variants(:reload).destroy_all
yield
if is_master?
exchange_variants(:reload).destroy_all
yield
product.refresh_products_cache
else
OpenFoodNetwork::ProductsCache.variant_destroyed(self) do
# Remove this association here instead of using dependent: :destroy because
# dependent-destroy acts before this around_filter is called, so ProductsCache
# has no way of knowing which exchanges the variant was a member of.
exchange_variants(:reload).destroy_all
# Destroy the variant
yield
end
end
end
end

View File

@@ -12,6 +12,9 @@ class VariantOverride < ActiveRecord::Base
# Default stock can be nil, indicating stock should not be reset or zero, meaning reset to zero. Need to ensure this can be set by the user.
validates :default_stock, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
after_save :refresh_products_cache_from_save
after_destroy :refresh_products_cache_from_destroy
default_scope where(permission_revoked_at: nil)
scope :for_hubs, lambda { |hubs|
@@ -70,4 +73,14 @@ class VariantOverride < ActiveRecord::Base
end
self
end
private
def refresh_products_cache_from_save
OpenFoodNetwork::ProductsCache.variant_override_changed self
end
def refresh_products_cache_from_destroy
OpenFoodNetwork::ProductsCache.variant_override_destroyed self
end
end

View File

@@ -1,11 +1,43 @@
require 'open_food_network/scope_variant_to_hub'
class Api::ProductSerializer < ActiveModel::Serializer
# TODO
# Prices can't be cached? How?
def serializable_hash
cached_serializer_hash.merge(uncached_serializer_hash)
end
private
def cached_serializer_hash
Api::CachedProductSerializer.new(object, @options).serializable_hash
end
def uncached_serializer_hash
Api::UncachedProductSerializer.new(object, @options).serializable_hash
end
end
class Api::UncachedProductSerializer < ActiveModel::Serializer
attributes :price
def price
if options[:enterprise_fee_calculator]
object.master.price + options[:enterprise_fee_calculator].indexed_fees_for(object.master)
else
object.master.price_with_fees(options[:current_distributor], options[:current_order_cycle])
end
end
end
class Api::CachedProductSerializer < ActiveModel::Serializer
# cached
# delegate :cache_key, to: :object
include ActionView::Helpers::SanitizeHelper
attributes :id, :name, :permalink, :meta_keywords
attributes :group_buy, :notes, :description, :description_html
attributes :properties_with_values, :price
attributes :properties_with_values
has_many :variants, serializer: Api::VariantSerializer
has_one :master, serializer: Api::VariantSerializer
@@ -38,12 +70,4 @@ class Api::ProductSerializer < ActiveModel::Serializer
def master
options[:master_variants][object.id].andand.first
end
def price
if options[:enterprise_fee_calculator]
object.master.price + options[:enterprise_fee_calculator].indexed_fees_for(object.master)
else
object.master.price_with_fees(options[:current_distributor], options[:current_order_cycle])
end
end
end

View File

@@ -1,7 +0,0 @@
class ExchangeVariantDeleter
def delete(product)
ExchangeVariant.
where(variant_id: product.variants.select(:id)).
delete_all
end
end

View File

@@ -1,42 +1,36 @@
# Returns a (paginatable) AR object for the products or variants in stock for a given shop and OC.
# The stock-checking includes on_demand and stock level overrides from variant_overrides.
class OrderCycleDistributedProducts
def initialize(distributor, order_cycle, customer)
def initialize(distributor, order_cycle)
@distributor = distributor
@order_cycle = order_cycle
@customer = customer
end
def products_relation
Spree::Product.where(id: stocked_products).group("spree_products.id")
Spree::Product.where(id: stocked_products)
end
def variants_relation
order_cycle.
variants_distributed_by(distributor).
merge(stocked_variants_and_overrides).
select("DISTINCT spree_variants.*")
@order_cycle.
variants_distributed_by(@distributor).
merge(stocked_variants_and_overrides)
end
private
attr_reader :distributor, :order_cycle, :customer
def stocked_products
order_cycle.
variants_distributed_by(distributor).
@order_cycle.
variants_distributed_by(@distributor).
merge(stocked_variants_and_overrides).
select("DISTINCT spree_variants.product_id")
end
def stocked_variants_and_overrides
stocked_variants = Spree::Variant.
Spree::Variant.
joins("LEFT OUTER JOIN variant_overrides ON variant_overrides.variant_id = spree_variants.id
AND variant_overrides.hub_id = #{distributor.id}").
AND variant_overrides.hub_id = #{@distributor.id}").
joins(:stock_items).
where(query_stock_with_overrides)
ProductTagRulesFilterer.new(distributor, customer, stocked_variants).call
end
def query_stock_with_overrides

View File

@@ -1,111 +0,0 @@
# Takes a Spree::Variant AR object and filters results based on applicable tag rules.
# Tag rules exists in the context of enterprise, customer, and variant_overrides,
# and are applied to variant_overrides only. Returns a Spree::Variant AR object.
class ProductTagRulesFilterer
def initialize(distributor, customer, variants_relation)
@distributor = distributor
@customer = customer
@variants_relation = variants_relation
end
def call
return variants_relation unless distributor_rules.any?
filter(variants_relation)
end
private
attr_accessor :distributor, :customer, :variants_relation
def distributor_rules
@distributor_rules ||= TagRule::FilterProducts.prioritised.for(distributor).all
end
def filter(variants_relation)
return variants_relation unless overrides_to_hide.any?
variants_relation.where(query_with_tag_rules)
end
def query_with_tag_rules
"#{variant_not_overriden} OR ( #{variant_overriden}
AND ( #{override_not_hidden_by_rule}
OR #{override_shown_by_rule} ) )"
end
def variant_not_overriden
"variant_overrides.id IS NULL"
end
def variant_overriden
"variant_overrides.id IS NOT NULL"
end
def override_not_hidden_by_rule
return "FALSE" unless overrides_to_hide.any?
"variant_overrides.id NOT IN (#{overrides_to_hide.join(',')})"
end
def override_shown_by_rule
return "FALSE" unless overrides_to_show.any?
"variant_overrides.id IN (#{overrides_to_show.join(',')})"
end
def overrides_to_hide
@overrides_to_hide ||= VariantOverride.where(hub_id: distributor.id).
tagged_with(default_rule_tags + hide_rule_tags, any: true).
pluck(:id)
end
def overrides_to_show
@overrides_to_show ||= VariantOverride.where(hub_id: distributor.id).
tagged_with(show_rule_tags, any: true).
pluck(:id)
end
def default_rule_tags
default_rules.map(&:preferred_variant_tags)
end
def hide_rule_tags
hide_rules.map(&:preferred_variant_tags)
end
def show_rule_tags
show_rules.map(&:preferred_variant_tags)
end
def default_rules
# These rules hide a variant_override with tag X and apply to all customers
distributor_rules.select(&:is_default?)
end
def non_default_rules
# These rules show or hide a variant_override with tag X for customer with tag Y
distributor_rules.reject(&:is_default?)
end
def customer_applicable_rules
# Rules which apply specifically to the current customer
@customer_applicable_rules ||= non_default_rules.select{ |rule| customer_tagged?(rule) }
end
def hide_rules
@hide_rules ||= customer_applicable_rules.
select{ |rule| rule.preferred_matched_variants_visibility == 'hidden' }
end
def show_rules
customer_applicable_rules - hide_rules
end
def customer_tagged?(rule)
customer_tag_list.include? rule.preferred_customer_tags
end
def customer_tag_list
customer.andand.tag_list || []
end
end

View File

@@ -1,98 +0,0 @@
require 'open_food_network/scope_product_to_hub'
class ProductsRenderer
class NoProducts < RuntimeError; end
DEFAULT_PAGE = 1
DEFAULT_PER_PAGE = 10
def initialize(distributor, order_cycle, customer, args = {})
@distributor = distributor
@order_cycle = order_cycle
@customer = customer
@args = args
end
def products_json
raise NoProducts unless order_cycle && distributor && products
ActiveModel::ArraySerializer.new(products,
each_serializer: Api::ProductSerializer,
current_order_cycle: order_cycle,
current_distributor: distributor,
variants: variants_for_shop_by_id,
master_variants: master_variants_for_shop_by_id,
enterprise_fee_calculator: enterprise_fee_calculator).to_json
end
private
attr_reader :order_cycle, :distributor, :customer, :args
def products
return unless order_cycle
@products ||= begin
results = distributed_products.products_relation.order(taxon_order)
filter_and_paginate(results).
each { |product| product_scoper.scope(product) } # Scope results with variant_overrides
end
end
def product_scoper
OpenFoodNetwork::ScopeProductToHub.new(distributor)
end
def enterprise_fee_calculator
OpenFoodNetwork::EnterpriseFeeCalculator.new distributor, order_cycle
end
def filter_and_paginate(query)
query.
ransack(args[:q]).
result.
page(args[:page] || DEFAULT_PAGE).
per(args[:per_page] || DEFAULT_PER_PAGE)
end
def distributed_products
OrderCycleDistributedProducts.new(distributor, order_cycle, customer)
end
def taxon_order
if distributor.preferred_shopfront_taxon_order.present?
distributor
.preferred_shopfront_taxon_order
.split(",").map { |id| "spree_products.primary_taxon_id=#{id} DESC" }
.join(", ") + ", spree_products.name ASC, spree_products.id ASC"
else
"spree_products.name ASC"
end
end
def variants_for_shop
@variants_for_shop ||= begin
scoper = OpenFoodNetwork::ScopeVariantToHub.new(distributor)
distributed_products.variants_relation.
includes(:default_price, :stock_locations, :product).
where(product_id: products).
each { |v| scoper.scope(v) } # Scope results with variant_overrides
end
end
def variants_for_shop_by_id
index_by_product_id variants_for_shop.reject(&:is_master)
end
def master_variants_for_shop_by_id
index_by_product_id variants_for_shop.select(&:is_master)
end
def index_by_product_id(variants)
variants.each_with_object({}) do |v, vs|
vs[v.product_id] ||= []
vs[v.product_id] << v
end
end
end

View File

@@ -0,0 +1,31 @@
- content_for :page_title do
= t(:cache_settings)
= form_tag main_app.admin_cache_settings_path, :method => :put do
.field
= hidden_field_tag 'preferences[enable_products_cache?]', '0'
= check_box_tag 'preferences[enable_products_cache?]', '1', Spree::Config[:enable_products_cache?]
= label_tag nil, t('.enable_products_cache')
.form-buttons
= button t(:update), 'icon-refresh'
%br
%br
%h4= t(:cache_state)
%br
%table.index
%thead
%tr
%th= t('.distributor')
%th= t('.order_cycle')
%th= t('.status')
%th= t('.diff')
%tbody
- @results.each do |result|
%tr
%td= result[:distributor].name
%td= result[:order_cycle].name
%td= result[:status] ? t(:ok) : t('.error')
%td
%pre= result[:diff].to_s(:text)

View File

@@ -1,4 +1,4 @@
- content_for :page_title do
= content_for :page_title do
= t 'admin_enterprise_groups'
- if admin_user?

View File

@@ -1,7 +1,6 @@
.row
.alpha.two.columns
= f.label :name, t('.name')
%span.required *
.six.columns.omega
- if viewing_as_coordinator_of?(@order_cycle)
= f.text_field :name, 'ng-model' => 'order_cycle.name', 'required' => true, 'ng-disabled' => '!loaded()'

View File

@@ -3,7 +3,6 @@
.row
.alpha.two.columns
= label_tag t('.ready_for')
%span.required *
.six.columns
= text_field_tag 'order_cycle_outgoing_exchange_0_pickup_time', '', 'id' => 'order_cycle_outgoing_exchange_0_pickup_time', 'required' => 'required', 'placeholder' => t('.ready_for_placeholder'), 'ng-model' => 'outgoing_exchange.pickup_time', 'size' => 30, 'maxlength' => 35
%span.icon-question-sign{'ofn-with-tip' => t('admin.order_cycles.exchange_form.pickup_time_tip')}

View File

@@ -1,4 +1,4 @@
- content_for :page_title do
= content_for :page_title do
= t :admin_order_cycles
- content_for :main_ng_app_name do

View File

@@ -2,7 +2,7 @@
#{t('admin.product_import.title')}
= render partial: 'ams_data'
= render partial: 'spree/admin/shared/product_sub_menu'
= render partial: 'admin/shared/product_sub_menu'
.import-wrapper{ng: {app: 'admin.productImport', controller: 'ImportFormCtrl'}}

View File

@@ -1,7 +1,7 @@
- content_for :page_title do
#{t('admin.product_import.title')}
= render partial: 'spree/admin/shared/product_sub_menu'
= render partial: 'admin/shared/product_sub_menu'
= render 'upload_sidebar'

View File

@@ -1,4 +1,4 @@
- content_for :sub_menu do
= content_for :sub_menu do
%ul#sub_nav.inline-menu{"data-hook" => "admin_enterprise_sub_tabs"}
= tab :enterprises, url: main_app.admin_enterprises_path
= tab :enterprise_relationships, url: main_app.admin_enterprise_relationships_path

View File

@@ -0,0 +1,8 @@
= content_for :sub_menu do
%ul#sub_nav.inline-menu
= tab :products, match_path: '/products'
= tab :option_types, match_path: '/option_types'
= tab :properties
= tab :prototypes
= tab :variant_overrides, url: main_app.admin_inventory_path, match_path: '/inventory'
= tab :import, url: main_app.admin_product_import_path, match_path: '/product_import'

View File

@@ -1,4 +1,4 @@
- content_for :sub_menu do
= content_for :sub_menu do
%ul#sub_nav.inline-menu{"data-hook" => "admin_user_sub_tabs"}
= tab :users, url: spree.admin_users_path
= tab :roles, url: main_app.admin_enterprise_roles_path, match_path: '/enterprise_roles'

View File

@@ -5,4 +5,4 @@
%h1.page-title= t("admin.variant_overrides.index.title")
%a.with-tip{ 'data-powertip' => "#{t("admin.variant_overrides.index.description")}" }=t('admin.whats_this')
= render partial: 'spree/admin/shared/product_sub_menu'
= render partial: 'admin/shared/product_sub_menu'

View File

@@ -1,6 +1,6 @@
%form{ name: 'variant_overrides_form', ng: { show: "views.inventory.visible" } }
%save-bar{ dirty: "customers_form.$dirty", persist: "false" }
%input.red{ type: "button", value: t(:save_changes), ng: { click: "update()", disabled: "!variant_overrides_form.$dirty" } }
%input.red{ type: "button", value: "Save Changes", ng: { click: "update()", disabled: "!variant_overrides_form.$dirty" } }
%table.index.bulk#variant-overrides
%col.producer{ width: "20%", ng: { show: 'columns.producer.visible' } }
%col.product{ width: "20%", ng: { show: 'columns.product.visible' } }

View File

@@ -14,7 +14,7 @@
%td.reset{ ng: { show: 'columns.reset.visible' } }
%input{name: 'variant-overrides-{{ variant.id }}-resettable', type: 'checkbox', ng: {model: 'variantOverrides[hub_id][variant.id].resettable'}, placeholder: '{{ variant.resettable }}', 'ofn-track-variant-override' => 'resettable'}
%td.reset{ ng: { show: 'columns.reset.visible' } }
%input{name: 'variant-overrides-{{ variant.id }}-default_stock', type: 'text', ng: {model: 'variantOverrides[hub_id][variant.id].default_stock'}, placeholder: '{{ variant.default_stock ? variant.default_stock : ("admin.variant_overrides.index.default_stock" | t)}}', 'ofn-track-variant-override' => 'default_stock'}
%input{name: 'variant-overrides-{{ variant.id }}-default_stock', type: 'text', ng: {model: 'variantOverrides[hub_id][variant.id].default_stock'}, placeholder: '{{ variant.default_stock ? variant.default_stock : "Default stock"}}', 'ofn-track-variant-override' => 'default_stock'}
%td.inheritance{ ng: { show: 'columns.inheritance.visible' } }
%input.field{ :type => 'checkbox', name: 'variant-overrides-{{ variant.id }}-inherit', ng: { model: 'inherit' }, 'track-inheritance' => true }
%td.tags{ ng: { show: 'columns.tags.visible' } }

View File

@@ -1,17 +1,18 @@
%h3
= "#{t(".email_welcome")}!"
= "#{t(:email_welcome)}!"
%p.lead
%strong
= @enterprise.name
= "#{t(".email_registered")} #{ Spree::Config.site_name }!"
= "#{t(:email_registered)} #{ Spree::Config.site_name }!"
%p
= t(".email_userguide_html", link: link_to(t(".userguide"), ContentConfig.user_guide_link))
%p
= t(".email_admin_html", link: link_to(t(".admin_panel"), spree.admin_url))
= t :email_userguide_html, link: link_to('Open Food Network User Guide', ContentConfig.user_guide_link)
%p
= t(".email_community_html", link: link_to(t(".join_community"), ContentConfig.community_forum_url))
= t :email_admin_html, link: link_to('Admin Panel', spree.admin_url)
%p
= t :email_community_html, link: link_to(t(:join_community), ContentConfig.community_forum_url)
= render 'shared/mailers/signoff'

View File

@@ -1,5 +1,5 @@
.filter-shopfront.taxon-selectors.text-right{ng: {show: 'supplied_taxons != null'}}
%single-line-selectors{ selectors: "taxonSelectors", objects: "supplied_taxons", "active-selectors" => "activeTaxons"}
.filter-shopfront.taxon-selectors.text-right
%single-line-selectors{ selectors: "taxonSelectors", objects: "Products.products | products:query | properties:activeProperties | taxonsOf", "active-selectors" => "activeTaxons"}
.filter-shopfront.property-selectors.text-right{ng: {show: 'supplied_properties != null'}}
%single-line-selectors{ selectors: "propertySelectors", objects: "supplied_properties", "active-selectors" => "activeProperties"}
.filter-shopfront.property-selectors.text-right
%single-line-selectors{ selectors: "propertySelectors", objects: "Products.products | products:query | taxons:activeTaxons | propertiesOf", "active-selectors" => "activeProperties"}

View File

@@ -22,14 +22,14 @@
.small-12.medium-6.large-5.columns
%input#search.text{"ng-model" => "query",
placeholder: t(:products_search),
"ng-debounce" => "200",
"ng-debounce" => "100",
"ofn-disable-enter" => true}
.small-12.medium-6.large-6.large-offset-1.columns
= render partial: "shop/products/filters"
%div.pad-top{ "infinite-scroll" => "loadMore()", "infinite-scroll-distance" => "1", "infinite-scroll-disabled" => 'Products.loading' }
%product.animate-repeat{"ng-controller" => "ProductNodeCtrl", "ng-repeat" => "product in Products.products track by product.id", "id" => "product-{{ product.id }}"}
%div.pad-top{ "infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1", "infinite-scroll-disabled" => 'filteredProducts.length <= limit' }
%product.animate-repeat{"ng-controller" => "ProductNodeCtrl", "ng-repeat" => "product in visibleProducts track by product.id", "id" => "product-{{ product.id }}"}
= render "shop/products/summary"
%shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants | orderBy: ['name_to_display','unit_value'] track by variant.id", "id" => "variant-{{ variant.id }}", "ng-class" => "{'out-of-stock': !variant.on_demand && variant.on_hand == 0}"}
@@ -41,7 +41,7 @@
.small-12.columns.text-center
%img.spinner{ src: "/assets/spinning-circles.svg" }
%div{"ng-show" => "Products.products.length == 0 && !Products.loading"}
%div{"ng-show" => "filteredProducts.length == 0 && !Products.loading"}
.row.summary
.small-12.columns
%p.no-results

View File

@@ -10,7 +10,7 @@
= line_item.single_money.to_html
%td.item-qty-show.align-center
- item.states.each do |state,count|
= "#{count} x #{t(state.humanize.downcase, scope: [:spree, :shipment_states], default: [:missing, "none"])}"
= "#{count} x #{t(state.humanize.downcase)}"
- unless shipment.shipped?
%td.item-qty-edit.hidden
= number_field_tag :quantity, item.quantity, :min => 0, :class => "line_item_quantity", :size => 5

View File

@@ -73,8 +73,6 @@
{{'js.admin.orders.shipment_states.' + order.shipment_state | t}}
%td
= mail_to "{{order.email}}"
%br
{{order.full_name}}
%td.align-center
%span{'ng-bind-html' => 'order.display_total'}
%td.actions

View File

@@ -7,11 +7,15 @@
- content_for :page_actions do
%ul.tollbar.inline-menu
%li
= link_to_add_fields t('.add_product_properties'), 'tbody#product_properties', class: 'icon-plus button'
= link_to_add_fields Spree.t(:add_product_properties), 'tbody#product_properties', class: 'icon-plus button'
%li
%span#new_ptype_link
= link_to Spree.t(:select_from_prototype), available_admin_prototypes_url, remote: true, 'data-update' => 'prototypes', class: 'button icon-copy'
= form_for @product, url: admin_product_url(@product), method: :put do |f|
%fieldset.no-border-top
.add_product_properties
#prototypes
= image_tag 'select2-spinner.gif', plugin: 'spree', style: 'display:none;', id: 'busy_indicator'
%table.index.sortable{"data-sortable-link" => update_positions_admin_product_product_properties_url}
@@ -27,7 +31,7 @@
= render partial: 'product_property_fields', locals: { f: pp_form }
= f.check_box :inherits_properties
= f.label :inherits_properties, t('.inherits_properties_checkbox_hint', supplier: @product.supplier.name)
= f.label :inherits_properties, t(".inherits_properties_checkbox_hint", supplier: @product.supplier.name)
%br
%br

View File

@@ -1,17 +1,17 @@
.row{"data-hook" => "admin_product_meta_form"}
.alpha.eleven.columns
= f.field_container :meta_keywords do
= f.label :meta_keywords, t('admin.products.seo.product_search_keywords')
%span.icon-question-sign{ 'ofn-with-tip' => t('admin.products.seo.product_search_tip') }
= f.label :meta_keywords, t(:product_search_keywords)
%span.icon-question-sign{ 'ofn-with-tip' => t('admin.products.product_search_tip') }
%br/
= f.text_field :meta_keywords, :class => 'fullwidth', :rows => 6
= f.field_container :meta_description do
= f.label :meta_description, t('admin.products.seo.SEO_keywords')
%span.icon-question-sign{ 'ofn-with-tip' => t('admin.products.seo.seo_tip') }
= f.label :meta_description, t(:SEO_keywords)
%span.icon-question-sign{ 'ofn-with-tip' => t('admin.products.seo_tip') }
%br/
= f.text_field :meta_description, :class => 'fullwidth', :rows => 6
.alpha.eleven.columns
= f.field_container :notes do
= f.label :notes, t(:notes)
= f.text_area :notes, { :class => 'fullwidth', rows: 5 }
= f.error_message_on :notes
= f.error_message_on :notes

View File

@@ -1,4 +1,4 @@
= render partial: 'spree/admin/shared/product_sub_menu'
= render partial: 'admin/shared/product_sub_menu'
= render :partial => 'spree/admin/shared/product_tabs', :locals => { :current => 'Group Buy Options' }
= render :partial => 'spree/shared/error_messages', :locals => { :target => @product }

View File

@@ -7,6 +7,6 @@
%li#new_product_link
= button_link_to t(:new_product), new_object_url, { :remote => true, :icon => 'icon-plus', :id => 'admin_new_product' }
= render partial: 'spree/admin/shared/product_sub_menu'
= render partial: 'admin/shared/product_sub_menu'
%div#new_product(data-hook)

View File

@@ -1,3 +1,6 @@
%div{ 'ng-show' => '!spree_api_key_ok' }
{{ api_error_msg }}
%div.sixteen.columns.alpha#loading{ 'ng-if' => 'RequestMonitor.loading' }
%br
%img.spinner{ src: "/assets/spinning-circles.svg" }

View File

@@ -101,3 +101,19 @@
angular.element(document.getElementById("new_product")).ready(function() {
angular.bootstrap(document.getElementById("new_product"), ['admin.products']);
});
:javascript
(function($){
var base_url = "#{admin_prototypes_url}";
var prototype_select = $('#product_prototype_id');
prototype_select.change(function() {
var id = prototype_select.val();
if (id.length) {
$('#product-from-prototype').load([ base_url, id ].join("/"));
} else {
$('#product-from-prototype').empty();
}
})
if (prototype_select.html() == "") {
prototype_select.change();
}
})(jQuery);

View File

@@ -1,5 +1,5 @@
= render partial: 'spree/admin/shared/product_sub_menu'
= render :partial => 'spree/admin/shared/product_tabs', :locals => { :current => t(:search) }
= render partial: 'admin/shared/product_sub_menu'
= render :partial => 'spree/admin/shared/product_tabs', :locals => { :current => t(:Search) }
= render :partial => 'spree/shared/error_messages', :locals => { :target => @product }
%div{ 'ng-app' => 'ofn.admin' }

View File

@@ -21,6 +21,7 @@
= configurations_sidebar_menu_item Spree.t(:shipping_categories), admin_shipping_categories_path
= configurations_sidebar_menu_item t(:enterprise_fees), main_app.admin_enterprise_fees_path
= configurations_sidebar_menu_item Spree.t(:analytics_trackers), admin_trackers_path
= configurations_sidebar_menu_item t('admin.cache_settings.edit.title'), main_app.edit_admin_cache_settings_path
= configurations_sidebar_menu_item t('admin.contents.edit.title'), main_app.edit_admin_contents_path
= configurations_sidebar_menu_item t('admin.invoice_settings.edit.title'), main_app.edit_admin_invoice_settings_path
= configurations_sidebar_menu_item t('admin.matomo_settings.edit.title'), main_app.edit_admin_matomo_settings_path

View File

@@ -1,4 +1,4 @@
.form-buttons.filter-actions.actions
= button t(:update), 'icon-refresh'
%span.or= t(:or)
= button_link_to t(:cancel), collection_url, icon: 'icon-remove'
= button Spree.t('actions.update'), 'icon-refresh'
%span.or= Spree.t(:or)
= button_link_to Spree.t('actions.cancel'), collection_url, icon: 'icon-remove'

View File

@@ -33,14 +33,14 @@
%dd#shipment_status
- shipment_state_classes = "state #{@order.shipment_state}"
%span{ class: shipment_state_classes }
= t(@order.shipment_state, scope: [:spree, :shipment_states], default: [:missing, "none"])
= t(@order.shipment_state, scope: :shipment_states, default: [:missing, "none"])
%dt
= t(:payment)
\:
%dd#payment_status
- payment_state_classes = "state #{@order.payment_state}"
%span{ class: payment_state_classes }
= t(@order.payment_state, scope: [:spree, :payment_states], default: [:missing, "none"])
= t(@order.payment_state, scope: :payment_states, default: [:missing, "none"])
%dt
= t(:date_completed)
\:

View File

@@ -1,6 +1,6 @@
- content_for :sub_menu do
%ul#sub_nav.inline-menu
= tab :products, match_path: '/products'
= tab :option_types, match_path: '/option_types'
= tab :properties
= tab :variant_overrides, url: main_app.admin_inventory_path, match_path: '/inventory'
= tab :import, url: main_app.admin_product_import_path, match_path: '/product_import'
= tab :prototypes

View File

@@ -1,33 +1,33 @@
- content_for :page_title do
= t('admin.products.editing_product')
= content_for :page_title do
= Spree.t(:editing_product)
= "\"#{@product.name}\""
- content_for :sidebar_title do
= content_for :sidebar_title do
%span.sku
= @product.sku
- content_for :sidebar do
= content_for :sidebar do
%nav.menu
%ul
- if can?(:admin, Spree::Product)
- klass = current == 'Product Details' ? 'active' : ''
%li{:class => klass}
= link_to_with_icon 'icon-edit', t('admin.products.tabs.product_details'), edit_admin_product_url(@product)
= link_to_with_icon 'icon-edit', Spree.t(:product_details), edit_admin_product_url(@product)
- if can?(:admin, Spree::Image)
- klass = current == 'Images' ? 'active' : ''
%li{:class => klass}
= link_to_with_icon 'icon-picture', t('admin.products.tabs.images'), admin_product_images_url(@product)
= link_to_with_icon 'icon-picture', Spree.t(:images), admin_product_images_url(@product)
- if can?(:admin, Spree::Variant)
- klass = current == 'Variants' ? 'active' : ''
%li{:class => klass}
= link_to_with_icon 'icon-th-large', t('admin.products.tabs.variants'), admin_product_variants_url(@product)
= link_to_with_icon 'icon-th-large', Spree.t(:variants), admin_product_variants_url(@product)
- if can?(:admin, Spree::ProductProperty)
- klass = current == 'Product Properties' ? 'active' : ''
%li{:class => klass}
= link_to_with_icon 'icon-tasks', t('admin.products.tabs.product_properties'), admin_product_product_properties_url(@product)
= link_to_with_icon 'icon-tasks', Spree.t(:product_properties), admin_product_product_properties_url(@product)
- klass = current == 'Group Buy Options' ? 'active' : ''
%li{:class => klass}
= link_to_with_icon 'icon-tasks', t('admin.products.tabs.group_buy_options'), group_buy_options_admin_product_url(@product)
- klass = current == t(:search) ? 'active' : ''
= link_to_with_icon 'icon-tasks', t('admin.products.group_buy_options'), group_buy_options_admin_product_url(@product)
- klass = current == t(:Search) ? 'active' : ''
%li{:class => klass}
= link_to_with_icon 'icon-tasks', t(:search), seo_admin_product_url(@product)
= link_to_with_icon 'icon-tasks', t(:Search), seo_admin_product_url(@product)

View File

@@ -3,6 +3,6 @@
:variants_search => spree.admin_search_variants_path(:format => 'json'),
:taxons_search => main_app.api_taxons_path(:format => 'json'),
:user_search => spree.admin_search_users_path(:format => 'json'),
:orders_api => main_app.api_orders_path
:orders_api => spree.api_orders_path(:format => 'json')
}.to_json %>;
</script>

View File

@@ -1,5 +1,5 @@
= tab :dashboard, :route => :admin, :icon => 'icon-dashboard'
= tab :products, :option_types, :properties, :variants, :product_properties, :taxons, :url => admin_products_path, :icon => 'icon-th-large'
= tab :products , :option_types, :properties, :prototypes, :variants, :product_properties, :taxons, :url => admin_products_path, :icon => 'icon-th-large'
= tab :order_cycles, :url => main_app.admin_order_cycles_path, :icon => 'icon-refresh'
= tab :orders, :payments, :creditcard_payments, :shipments, :credit_cards, :return_authorizations, :url => admin_orders_path('q[s]' => 'completed_at desc'), :icon => 'icon-shopping-cart'
= tab :reports, :icon => 'icon-file'

View File

@@ -17,7 +17,7 @@
= label_tag nil, t("spree.tree")
%br/
:javascript
Spree.routes.taxonomy_taxons_path = "#{main_app.api_taxonomy_taxons_path(@taxonomy)}";
Spree.routes.taxonomy_taxons_path = "#{spree.api_taxonomy_taxons_path(@taxonomy)}";
Spree.routes.admin_taxonomy_taxons_path = "#{spree.admin_taxonomy_taxons_path(@taxonomy)}";
#taxonomy_tree.tree
#progress{style: "display:none;"}

View File

@@ -1,18 +0,0 @@
%fieldset.omega.six.columns
%legend= t('spree.api.access')
- if @user.spree_api_key.present?
.field
= label_tag t('spree.api.key')
= ":"
= @user.spree_api_key
.filter-actions.actions
= form_tag spree.clear_api_key_admin_user_path(@user), method: :put do
= button t('spree.api.clear_key'), 'icon-trash'
%span.or= t(:or)
= form_tag spree.generate_api_key_admin_user_path(@user), method: :put do
= button t('spree.api.regenerate_key'), 'icon-refresh'
- else
.no-objects-found= t('spree.api.no_key')
.filter-actions.actions
= form_tag spree.generate_api_key_admin_user_path(@user), method: :put do
= button t('spree.api.generate_key'), 'icon-key'

View File

@@ -13,5 +13,3 @@
= render partial: "form", locals: { f: f }
%div{"data-hook" => "admin_user_edit_form_button"}
= render partial: "spree/admin/shared/edit_resource_links"
= render partial: 'spree/admin/users/api_fields'

View File

@@ -1,7 +1,7 @@
- content_for :page_title do
= content_for :page_title do
= Spree.t(:new_user)
- content_for :page_actions do
= content_for :page_actions do
%li
= button_link_to Spree.t(:back_to_users_list), spree.admin_users_path, icon: 'icon-arrow-left'

View File

@@ -1,9 +1,9 @@
.label-block.left.six.columns.alpha{'ng-app' => 'admin.products'}
.field
= f.label :display_name, t('.display_name')
= f.label :display_name, t(:display_name)
= f.text_field :display_name, class: "fullwidth"
.field
= f.label :display_as, t('.display_as')
= f.label :display_as, t(:display_as)
= f.text_field :display_as, class: "fullwidth"
- if product_has_variant_unit_option_type?(@product)
@@ -29,13 +29,13 @@
- if opt = @variant.option_values.detect {|o| o.option_type == option_type }.try(:presentation)
= text_field(:new_variant, option_type.presentation, value: opt, disabled: 'disabled', class: 'fullwidth')
.field
= f.label :sku, t('.sku')
= f.label :sku, Spree.t(:sku)
= f.text_field :sku, class: 'fullwidth'
.field
= f.label :price, t('.price')
= f.label :price, Spree.t(:price)
= f.text_field :price, value: number_to_currency(@variant.price, unit: ''), class: 'fullwidth'
.field
= f.label :cost_price, t('.cost_price')
= f.label :cost_price, Spree.t(:cost_price)
= f.text_field :cost_price, value: number_to_currency(@variant.cost_price, unit: ''), class: 'fullwidth'
%div{ 'set-on-demand' => '' }
@@ -53,7 +53,7 @@
.right.six.columns.omega.label-block
- if @product.variant_unit != 'weight'
.field
= f.label 'weight', t(:weight)+' (kg)'
= f.label 'weight', t('weight')+' (kg)'
- value = number_with_precision(@variant.weight, precision: 2)
= f.text_field 'weight', value: value, class: 'fullwidth'

View File

@@ -14,9 +14,9 @@
%col{style: "width: 15%"}/
%thead
%tr
%th{colspan: "2"}= t('.options')
%th= t('.price')
%th= t('.sku')
%th{colspan: "2"}= Spree.t(:options)
%th= Spree.t(:price)
%th= Spree.t(:sku)
%th.actions
%tbody
- @variants.each do |variant|
@@ -31,24 +31,24 @@
= link_to_delete(variant, no_text: true) unless variant.deleted?
- unless @product.has_variants?
%tr
%td{colspan: "5"}= t(:none)
%td{colspan: "5"}= Spree.t(:none)
- else
.alpha.twelve.columns.no-objects-found
= t('.no_results')
= Spree.t(:no_results)
\.
- if @product.empty_option_values?
%p.first_add_option_types.no-objects-found
= t('.to_add_variants_you_must_first_define')
= link_to t('.option_types'), admin_product_url(@product)
= t('.and')
= link_to t('.option_values'), admin_option_types_url
= Spree.t(:to_add_variants_you_must_first_define)
= link_to Spree.t(:option_types), admin_product_url(@product)
= Spree.t(:and)
= link_to Spree.t(:option_values), admin_option_types_url
- else
- content_for :page_actions do
%ul.inline-menu
%li#new_var_link
= link_to_with_icon('icon-plus', t('.new_variant'), new_admin_product_variant_url(@product), remote: true, 'data-update' => 'new_variant', class: 'button')
= link_to_with_icon('icon-plus', Spree.t(:new_variant), new_admin_product_variant_url(@product), remote: true, 'data-update' => 'new_variant', class: 'button')
%li= link_to_with_icon('icon-filter', @deleted.blank? ? t('.show_deleted') : t('.show_active'), admin_product_variants_url(@product, deleted: @deleted.blank? ? "on" : "off"), class: 'button')
%li= link_to_with_icon('icon-filter', @deleted.blank? ? Spree.t(:show_deleted) : Spree.t(:show_active), admin_product_variants_url(@product, deleted: @deleted.blank? ? "on" : "off"), class: 'button')

View File

@@ -2,6 +2,6 @@
= form_for [:admin, @product, @variant] do |f|
%fieldset{'data-hook' => "admin_variant_new_form"}
%legend{align: "center"}= t('.new_variant')
%legend{align: "center"}= Spree.t(:new_variant)
= render partial: 'form', locals: { f: f }
= render partial: 'spree/admin/shared/new_resource_links'

View File

@@ -0,0 +1,2 @@
object false
node(:success) { "Use of API Authorised" }

File diff suppressed because it is too large Load Diff

View File

@@ -111,14 +111,6 @@ ca:
subject: "Sisplau, confirma l'adreça electrònica d'%{enterprise}"
welcome:
subject: "%{enterprise} és ara %{sitename}"
email_welcome: "Benvinguda"
email_registered: "ara forma part de"
email_userguide_html: "La Guia d'usuari amb suport detallat per configurar una productora o grup de consum és aquí: %{link}"
userguide: "Guia d'usuaris d'Open Food Network"
email_admin_html: "Pots gestionar el teu compte iniciant sessió a l'%{link} o fent clic a la pestanya a la part superior dreta de la pàgina d'inici i seleccionant Administració."
admin_panel: "Tauler dadministració"
email_community_html: "També tenim un fòrum en línia per debats de la comunitat relacionada amb el programari OFN i els desafiaments únics de triar endavant una organització alimentària. T'animem a unir-t'hi. Estem en constant evolució i les teves contribucions en aquest fòrum donaran forma al que passi a en el futur. %{link}"
join_community: "Uneix-te a la comunitat"
invite_manager:
subject: "%{enterprise} t'ha convidat a ser administrador"
producer_mailer:
@@ -328,6 +320,15 @@ ca:
number_localization:
number_localization_settings: "Configuració de localització numèrica"
enable_localized_number: "Utilitzeu l'estàndard internacional per separar milers/decimals"
cache_settings:
edit:
title: "Emmagatzematge ocult"
distributor: "Distribuïdora"
order_cycle: "Cicle de comanda"
status: "Estat"
diff: "Diferència"
error: "Error"
enable_products_cache: "Habilitar la memòria cau de productes?"
invoice_settings:
edit:
title: "Configuració de la factura"
@@ -421,23 +422,19 @@ ca:
av_on: "Disp. via"
import_date: S'ha importat
upload_an_image: Penja una imatge
seo:
product_search_keywords: "Paraules clau de cerca de producte"
product_search_tip: "Escriviu paraules per ajudar-vos a cercar els vostres productes a les botigues. Utilitzeu espai per separar cada paraula clau."
SEO_keywords: "Paraules clau de SEO"
seo_tip: "Escriviu paraules per ajudar-vos a cercar els vostres productes a la web. Utilitzeu espai per separar cada paraula clau."
search: "Cerca"
product_search_keywords: Paraules clau de cerca de producte
product_search_tip: Escriviu paraules per ajudar-vos a cercar els vostres productes a les botigues. Utilitzeu espai per separar cada paraula clau.
SEO_keywords: Paraules clau de SEO
seo_tip: Escriviu paraules per ajudar-vos a cercar els vostres productes a la web. Utilitzeu espai per separar cada paraula clau.
Search: Cerca
properties:
property_name: "Nom de la propietat"
inherited_property: "Propietat heretada"
property_name: Nom de la propietat
inherited_property: Propietat heretada
variants:
infinity: "Infinit"
to_order_tip: "Els articles preparats per encàrrec no tenen un nivell fixat d'existències, com ara pa fet sota comanda."
group_buy_options: "Opcions de compra en grup"
back_to_products_list: "Torna a la llista de productes"
tabs:
group_buy_options: "Opcions de compra en grup"
images: "Imatges"
product_properties: "Propietats del producte"
product_import:
title: Importació de productes
file_not_found: No s'ha trobat el fitxer o no s'ha pogut obrir
@@ -539,7 +536,6 @@ ca:
title: Inventari
description: Utilitzeu aquesta pàgina per administrar els inventaris de la vostres organitzacions. Tots els detalls del producte aquí establerts substituiran els establerts a la pàgina "Productes"
enable_reset?: Habilitar la restauració de valors de stock?
default_stock: "Estoc per defecte"
inherit?: Heredar?
add: Afegeix
hide: Amaga
@@ -1383,7 +1379,13 @@ ca:
products_in: "en %{oc}"
products_at: "a %{distributor}"
products_elsewhere: "Productes trobats en altres llocs"
email_welcome: "Benvinguda"
email_confirmed: "Gràcies per confirmar la teva adreça de correu electrònic."
email_registered: "ara forma part de"
email_userguide_html: "La Guia d'usuari amb suport detallat per configurar una productora o grup de consum és aquí: %{link}"
email_admin_html: "Pots gestionar el teu compte iniciant sessió a l'%{link} o fent clic a la pestanya a la part superior dreta de la pàgina d'inici i seleccionant Administració."
email_community_html: "També tenim un fòrum en línia per debats de la comunitat relacionada amb el programari OFN i els desafiaments únics de triar endavant una organització alimentària. T'animem a unir-t'hi. Estem en constant evolució i les teves contribucions en aquest fòrum donaran forma al que passi a en el futur. %{link}"
join_community: "Uneix-te a la comunitat"
email_confirmation_activate_account: "Abans de poder activar el compte nou hem de confirmar la teva adreça de correu electrònic."
email_confirmation_greeting: "Hola, %{contact}!"
email_confirmation_profile_created: "S'ha creat exitosament un perfil per %{name}. Per activar el teu perfil hem de confirmar aquesta adreça de correu electrònic."
@@ -2812,6 +2814,7 @@ ca:
products: "Productes "
option_types: "Tipus d'opcions"
properties: "Propietats"
prototypes: "Prototips"
variant_overrides: "Inventari"
reports: "Informes"
configuration: "Configuració"
@@ -3012,15 +3015,6 @@ ca:
email_confirmation:
confirmation_pending: "La confirmació de correu electrònic està pendent. Hem enviat un correu electrònic de confirmació a %{address}."
variants:
index:
sku: "Número de referència (SKU)"
price: "Preu"
no_results: "Sense resultats"
option_types: "Tipus d'opcions"
form:
sku: "Número de referència (SKU)"
price: "Preu"
display_as: "Mostra com"
autocomplete:
producer_name: "Productor"
unit: "Unitat"

View File

@@ -111,10 +111,6 @@ de_DE:
subject: "Bitte bestätigen Sie die E-Mail-Adresse für %{enterprise}"
welcome:
subject: "%{enterprise} ist jetzt auf %{sitename}"
email_welcome: "Willkommen"
email_registered: "ist jetzt Teil von"
email_community_html: "Wir haben auch ein Online-Forum für Community-Diskussionen in Bezug auf OFN-Software und die einzigartigen Herausforderungen eines Lebensmittelunternehmens. Reden Sie doch mit. Wir entwickeln uns ständig weiter und Ihr Beitrag in diesem Forum prägt, was als nächstes passiert. %{link}"
join_community: "Treten Sie der Community bei"
invite_manager:
subject: "%{enterprise} hat Sie eingeladen, ein Manager zu sein"
producer_mailer:
@@ -324,6 +320,15 @@ de_DE:
number_localization:
number_localization_settings: "Nummernlokalisierungseinstellungen"
enable_localized_number: "Verwenden Sie die internationale Tausendertrennungslogik"
cache_settings:
edit:
title: "Cachen"
distributor: "Verteiler"
order_cycle: "Bestellrunde"
status: "Status"
diff: "Diff"
error: "Fehler"
enable_products_cache: "Produktcache aktivieren?"
invoice_settings:
edit:
title: "Rechnungseinstellungen"
@@ -417,23 +422,19 @@ de_DE:
av_on: "Verfüg. am"
import_date: Importiert
upload_an_image: Bild hochladen
seo:
product_search_keywords: "Stichwörter für die Produktsuche"
product_search_tip: "Geben Sie Wörter ein, um Ihre Produkte in den Geschäften zu suchen. Verwenden Sie Leerzeichen, um jedes Keyword zu trennen."
SEO_keywords: "SEO Schlüsselwörter"
seo_tip: "Geben Sie Wörter ein, um Ihre Produkte im Internet zu durchsuchen. Verwenden Sie Leerzeichen, um jedes Keyword zu trennen."
search: "Suche"
product_search_keywords: Stichwörter für die Produktsuche
product_search_tip: Geben Sie Wörter ein, um Ihre Produkte in den Geschäften zu suchen. Verwenden Sie Leerzeichen, um jedes Keyword zu trennen.
SEO_keywords: SEO Schlüsselwörter
seo_tip: Geben Sie Wörter ein, um Ihre Produkte im Internet zu durchsuchen. Verwenden Sie Leerzeichen, um jedes Keyword zu trennen.
Search: Suche
properties:
property_name: "Name der Eigenschaft"
inherited_property: "Vererbte Eigenschaft"
property_name: Name der Eigenschaft
inherited_property: Vererbte Eigenschaft
variants:
infinity: "Unendlichkeit"
to_order_tip: "Artikel, die auf Bestellung hergestellt werden, haben keinen festgelegten Lagerbestand."
group_buy_options: "Gruppenkaufoptionen"
back_to_products_list: "Zurück zur Produktliste"
tabs:
group_buy_options: "Gruppenkaufoptionen"
images: "Bilder"
product_properties: "Produkteigenschaften"
product_import:
title: Produkte importieren
file_not_found: Datei nicht gefunden oder konnte nicht geöffnet werden
@@ -1378,7 +1379,13 @@ de_DE:
products_in: "in %{oc}"
products_at: "bei %{distributor}"
products_elsewhere: "Produkte an anderer Stelle"
email_welcome: "Willkommen"
email_confirmed: "Vielen Dank für die Bestätigung Ihrer E-Mail-Adresse."
email_registered: "ist jetzt Teil von"
email_userguide_html: "Das Benutzerhandbuch mit ausführlicher Unterstützung für die Einrichtung Ihres Producer oder Hub finden Sie hier: %{link}"
email_admin_html: "Sie können Ihr Konto verwalten, indem Sie sich bei %{link} anmelden oder indem Sie oben rechts auf der Startseite auf das Zahnrad klicken und Administration auswählen."
email_community_html: "Wir haben auch ein Online-Forum für Community-Diskussionen in Bezug auf OFN-Software und die einzigartigen Herausforderungen eines Lebensmittelunternehmens. Reden Sie doch mit. Wir entwickeln uns ständig weiter und Ihr Beitrag in diesem Forum prägt, was als nächstes passiert. %{link}"
join_community: "Treten Sie der Community bei"
email_confirmation_activate_account: "Bevor wir Ihr neues Konto aktivieren können, müssen wir Ihre E-Mail-Adresse bestätigen."
email_confirmation_greeting: "Hallo, %{contact}!"
email_confirmation_profile_created: "Ein Profil für %{name} wurde erfolgreich erstellt! Um Ihr Profil zu aktivieren, müssen wir diese E-Mail-Adresse bestätigen."
@@ -2807,6 +2814,7 @@ de_DE:
products: "Produkte"
option_types: "Optionstypen"
properties: "Eigenschaften"
prototypes: "Prototypen"
variant_overrides: "Katalog"
reports: "Berichte"
configuration: "Aufbau"
@@ -3007,13 +3015,6 @@ de_DE:
email_confirmation:
confirmation_pending: "E-Mail-Bestätigung steht aus. Wir haben eine Bestätigungs-E-Mail an %{address} gesendet."
variants:
index:
sku: "Artikelnummer"
price: "Preis"
form:
sku: "Artikelnummer"
price: "Preis"
display_as: "Angezeigt als"
autocomplete:
producer_name: "Produzent"
unit: "Einheit"

View File

@@ -136,14 +136,6 @@ en:
subject: "Please confirm the email address for %{enterprise}"
welcome:
subject: "%{enterprise} is now on %{sitename}"
email_welcome: "Welcome"
email_registered: "is now part of"
email_userguide_html: "The User Guide with detailed support for setting up your Producer or Hub is here: %{link}"
userguide: "Open Food Network User Guide"
email_admin_html: "You can manage your account by logging into the %{link} or by clicking on the cog in the top right hand side of the homepage, and selecting Administration."
admin_panel: "Admin Panel"
email_community_html: "We also have an online forum for community discussion related to OFN software and the unique challenges of running a food enterprise. You are encouraged to join in. We are constantly evolving and your input into this forum will shape what happens next. %{link}"
join_community: "Join the community"
invite_manager:
subject: "%{enterprise} has invited you to be a manager"
producer_mailer:
@@ -373,6 +365,16 @@ en:
number_localization_settings: "Number Localization Settings"
enable_localized_number: "Use the international thousand/decimal separator logic"
cache_settings:
edit:
title: "Caching"
distributor: "Distributor"
order_cycle: "Order Cycle"
status: "Status"
diff: "Diff"
error: "Error"
enable_products_cache: "Enable Products Cache?"
invoice_settings:
edit:
title: "Invoice Settings"
@@ -474,26 +476,19 @@ en:
av_on: "Av. On"
import_date: Imported
upload_an_image: Upload an image
seo:
product_search_keywords: "Product Search Keywords"
product_search_tip: "Type words to help search your products in the shops. Use space to separate each keyword."
SEO_keywords: "SEO Keywords"
seo_tip: "Type words to help search your products in the web. Use space to separate each keyword."
search: "Search"
product_search_keywords: Product Search Keywords
product_search_tip: Type words to help search your products in the shops. Use space to separate each keyword.
SEO_keywords: SEO Keywords
seo_tip: Type words to help search your products in the web. Use space to separate each keyword.
Search: Search
properties:
property_name: "Property Name"
inherited_property: "Inherited Property"
property_name: Property Name
inherited_property: Inherited Property
variants:
infinity: "Infinity"
to_order_tip: "Items made to order do not have a set stock level, such as loaves of bread made fresh to order."
group_buy_options: "Group Buy Options"
back_to_products_list: "Back to products list"
editing_product: "Editing Product"
tabs:
product_details: "Product Details"
group_buy_options: "Group Buy Options"
images: "Images"
variants: "Variants"
product_properties: "Product Properties"
product_import:
title: Product Import
@@ -597,7 +592,6 @@ en:
title: Inventory
description: Use this page to manage inventories for your enterprises. Any product details set here will override those set on the 'Products' page
enable_reset?: Enable Stock Reset?
default_stock: "Default stock"
inherit?: Inherit?
add: Add
hide: Hide
@@ -1482,7 +1476,15 @@ en:
products_at: "at %{distributor}"
products_elsewhere: "Products found elsewhere"
email_welcome: "Welcome"
email_confirmed: "Thank you for confirming your email address."
email_registered: "is now part of"
email_userguide_html: "The User Guide with detailed support for setting up your Producer or Hub is here:
%{link}"
email_admin_html: "You can manage your account by logging into the %{link} or by clicking on the cog in the top right hand side of the homepage, and selecting Administration."
email_community_html: "We also have an online forum for community discussion related to OFN software and the unique challenges of running a food enterprise. You are encouraged to join in. We are constantly evolving and your input into this forum will shape what happens next.
%{link}"
join_community: "Join the community"
email_confirmation_activate_account: "Before we can activate your new account, we need to confirm your email address."
email_confirmation_greeting: "Hi, %{contact}!"
email_confirmation_profile_created: "A profile for %{name} has been successfully created!
@@ -2938,13 +2940,6 @@ See the %{link} to find out more about %{sitename}'s features and to start using
normal_amount: "Normal Amount"
discount_amount: "Discount Amount"
no_images_found: "No Images Found"
new_image: "New Image"
filename: "Filename"
alt_text: "Alternative Text"
thumbnail: "Thumbnail"
back_to_images_list: "Back To Images List"
# TODO: remove `email` key once we get to Spree 2.0
email: Email
# TODO: remove 'account_updated' key once we get to Spree 2.0
@@ -2958,7 +2953,6 @@ See the %{link} to find out more about %{sitename}'s features and to start using
zipcode: Postcode
weight: Weight (per kg)
error_user_destroy_with_orders: "Users with completed orders may not be deleted"
options: "Options"
actions:
update: "Update"
@@ -2978,6 +2972,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using
products: "Products"
option_types: "Option Types"
properties: "Properties"
prototypes: "Prototypes"
variant_overrides: "Inventory"
reports: "Reports"
configuration: "Configuration"
@@ -2991,8 +2986,6 @@ See the %{link} to find out more about %{sitename}'s features and to start using
product_properties:
index:
inherits_properties_checkbox_hint: "Inherit properties from %{supplier}? (unless overridden above)"
add_product_properties: "Add Product Properties"
select_from_prototype: "Select From Prototype"
orders:
index:
listing_orders: "Listing Orders"
@@ -3180,26 +3173,6 @@ See the %{link} to find out more about %{sitename}'s features and to start using
email_confirmation:
confirmation_pending: "Email confirmation is pending. We've sent a confirmation email to %{address}."
variants:
index:
sku: "SKU"
price: "Price"
options: "Options"
no_results: "No results"
to_add_variants_you_must_first_define: "To add variants, you must first define"
option_types: "Option Types"
option_values: "Option Values"
and: "and"
new_variant: "New Variant"
show_active: "Show Active"
show_deleted: "Show Deleted"
new:
new_variant: "New Variant"
form:
cost_price: "Cost Price"
sku: "SKU"
price: "Price"
display_as: "Display As"
display_name: "Display Name"
autocomplete:
producer_name: "Producer"
unit: "Unit"
@@ -3339,19 +3312,3 @@ See the %{link} to find out more about %{sitename}'s features and to start using
allow_charges?: "Allow Charges?"
localized_number:
invalid_format: has an invalid format. Please enter a number.
api:
invalid_api_key: "Invalid API key (%{key}) specified."
unauthorized: "You are not authorized to perform that action."
invalid_resource: "Invalid resource. Please fix errors and try again."
resource_not_found: "The resource you were looking for could not be found."
access: "API Access"
key: "Key"
clear_key: "Clear key"
regenerate_key: "Regenerate Key"
no_key: "No key"
generate_key: "Generate API key"
key_generated: "Key generated"
key_cleared: "Key cleared"
shipment:
cannot_ready: "Cannot ready shipment."
invalid_taxonomy_id: "Invalid taxonomy id."

View File

@@ -111,12 +111,6 @@ en_AU:
subject: "Please confirm the email address for %{enterprise}"
welcome:
subject: "%{enterprise} is now on %{sitename}"
email_welcome: "Welcome"
email_registered: "is now part of"
email_userguide_html: "The User Guide with detailed support for setting up your Producer or Hub is here: %{link}"
email_admin_html: "You can manage your account by logging into the %{link} or by clicking on the cog in the top right hand side of the homepage, and selecting Administration."
email_community_html: "We also have an online forum for community discussion related to OFN software and the unique challenges of running a food enterprise. You are encouraged to join in. We are constantly evolving and your input into this forum will shape what happens next. %{link}"
join_community: "Join the community"
invite_manager:
subject: "%{enterprise} has invited you to be a manager"
producer_mailer:
@@ -325,6 +319,15 @@ en_AU:
number_localization:
number_localization_settings: "Number Localization Settings"
enable_localized_number: "Use the international thousand/decimal separator logic"
cache_settings:
edit:
title: "Caching"
distributor: "Distributor"
order_cycle: "Order Cycle"
status: "Status"
diff: "Diff"
error: "Error"
enable_products_cache: "Enable Products Cache?"
invoice_settings:
edit:
title: "Invoice Settings"
@@ -418,23 +421,19 @@ en_AU:
av_on: "Av. On"
import_date: Imported
upload_an_image: Upload an image
seo:
product_search_keywords: "Product Search Keywords"
product_search_tip: "Type words to help search your products in the shops. Use space to separate each keyword."
SEO_keywords: "SEO Keywords"
seo_tip: "Type words to help search your products in the web. Use space to separate each keyword."
search: "Search"
product_search_keywords: Product Search Keywords
product_search_tip: Type words to help search your products in the shops. Use space to separate each keyword.
SEO_keywords: SEO Keywords
seo_tip: Type words to help search your products in the web. Use space to separate each keyword.
Search: Search
properties:
property_name: "Property Name"
inherited_property: "Inherited Property"
property_name: Property Name
inherited_property: Inherited Property
variants:
infinity: "Infinity"
to_order_tip: "Items made to order do not have a set stock level, such as loaves of bread made fresh to order."
group_buy_options: "Group Buy Options"
back_to_products_list: "Back to products list"
tabs:
group_buy_options: "Group Buy Options"
images: "Images"
product_properties: "Product Properties"
product_import:
title: Product Import
file_not_found: File not found or could not be opened
@@ -1377,7 +1376,13 @@ en_AU:
products_in: "in %{oc}"
products_at: "at %{distributor}"
products_elsewhere: "Products found elsewhere"
email_welcome: "Welcome"
email_confirmed: "Thank you for confirming your email address."
email_registered: "is now part of"
email_userguide_html: "The User Guide with detailed support for setting up your Producer or Hub is here: %{link}"
email_admin_html: "You can manage your account by logging into the %{link} or by clicking on the cog in the top right hand side of the homepage, and selecting Administration."
email_community_html: "We also have an online forum for community discussion related to OFN software and the unique challenges of running a food enterprise. You are encouraged to join in. We are constantly evolving and your input into this forum will shape what happens next. %{link}"
join_community: "Join the community"
email_confirmation_activate_account: "Before we can activate your new account, we need to confirm your email address."
email_confirmation_greeting: "Hi, %{contact}!"
email_confirmation_profile_created: "A profile for %{name} has been successfully created! To activate your Profile we need to confirm this email address."
@@ -2800,6 +2805,7 @@ en_AU:
products: "Products"
option_types: "Option Types"
properties: "Properties"
prototypes: "Prototypes"
variant_overrides: "Inventory"
reports: "Reports"
configuration: "Configuration"
@@ -3000,15 +3006,6 @@ en_AU:
email_confirmation:
confirmation_pending: "Email confirmation is pending. We've sent a confirmation email to %{address}."
variants:
index:
sku: "SKU"
price: "Price"
no_results: "No results"
option_types: "Option Types"
form:
sku: "SKU"
price: "Price"
display_as: "Display As"
autocomplete:
producer_name: "Producer"
unit: "Unit"

View File

@@ -110,12 +110,6 @@ en_BE:
subject: "Please confirm the email address for %{enterprise}"
welcome:
subject: "%{enterprise} is now on %{sitename}"
email_welcome: "Welcome"
email_registered: "is now part of"
email_userguide_html: "The User Guide with detailed support for setting up your Producer or Hub is here: %{link}"
email_admin_html: "You can manage your account by logging into the %{link} or by clicking on the cog in the top right hand side of the homepage, and selecting Administration."
email_community_html: "We also have an online forum for community discussion related to OFN software and the unique challenges of running a food enterprise. You are encouraged to join in. We are constantly evolving and your input into this forum will shape what happens next. %{link}"
join_community: "Join the community"
invite_manager:
subject: "%{enterprise} has invited you to be a manager"
producer_mailer:
@@ -323,6 +317,15 @@ en_BE:
number_localization:
number_localization_settings: "Number Localization Settings"
enable_localized_number: "Use the international thousand/decimal separator logic"
cache_settings:
edit:
title: "Caching"
distributor: "Distributor"
order_cycle: "Order Cycle"
status: "Status"
diff: "Diff"
error: "Error"
enable_products_cache: "Enable Products Cache?"
invoice_settings:
edit:
title: "Invoice Settings"
@@ -416,23 +419,19 @@ en_BE:
av_on: "Av. On"
import_date: Imported
upload_an_image: Upload an image
seo:
product_search_keywords: "Product Search Keywords"
product_search_tip: "Type words to help search your products in the shops. Use space to separate each keyword."
SEO_keywords: "SEO Keywords"
seo_tip: "Type words to help search your products in the web. Use space to separate each keyword."
search: "Search"
product_search_keywords: Product Search Keywords
product_search_tip: Type words to help search your products in the shops. Use space to separate each keyword.
SEO_keywords: SEO Keywords
seo_tip: Type words to help search your products in the web. Use space to separate each keyword.
Search: Search
properties:
property_name: "Property Name"
inherited_property: "Inherited Property"
property_name: Property Name
inherited_property: Inherited Property
variants:
infinity: "Infinity"
to_order_tip: "Items made to order do not have a set stock level, such as loaves of bread made fresh to order."
group_buy_options: "Group Buy Options"
back_to_products_list: "Back to products list"
tabs:
group_buy_options: "Group Buy Options"
images: "Images"
product_properties: "Product Properties"
product_import:
title: Product Import
file_not_found: File not found or could not be opened
@@ -1298,12 +1297,12 @@ en_BE:
home_shop: Shop Now
brandstory_headline: "Food, unincorporated."
brandstory_intro: "Sometimes the best way to fix the system is to start a new one…"
brandstory_part1: "This is what we do by creating a platform that wants to connect:\n● passionate farmers, committed to sustainable and regenerative agriculture,\n● distributors of local products, followers of short circuits, who act in complete transparency and ensure fair remuneration of producers,\n● buyers who want to change the world by eating better,"
brandstory_part2: "This platform is called the Open Food Network."
brandstory_part3: "Transparent and open source, it promotes fair relations between farmers and consumers. Its objective is to create a quality food network thanks to an efficient and constantly evolving IT tool.\nIt is deployed in several countries. In Belgium, it is developed by Oxfam-World Stores to empower those who grow, sell and buy food."
brandstory_part4: "You are\n● A producer: register and present your products to promote them in your region. You energize the platform and connect with new customers.\n● A distributor: register and present your project to make it known and convince new customers.\n● A consumer: find products near you and discover their stories."
brandstory_part1: "We begin from the ground up. With farmers and growers ready to tell their stories proudly and truly. With distributors ready to connect people with products fairly and honestly. With buyers who believe that better weekly shopping decisions can seriously change the world."
brandstory_part2: "Then we need a way to make it real. A way to empower everyone who grows, sells and buys food. A way to tell all the stories, to handle all the logistics. A way to turn transaction into transformation every day."
brandstory_part3: "So we build an online marketplace that levels the playing field. Its transparent, so it creates real relationships. Its open source, so its owned by everyone. It scales to regions and nations, so people start versions across the world."
brandstory_part4: "It works everywhere. It changes everything."
brandstory_part5_strong: "We call it Open Food Network."
brandstory_part6: "We can all participate in the construction of a fairer food system that respects mankind and preserves the planet."
brandstory_part6: "We all love food. Now we can love our food system too."
learn_body: "Explore models, stories and resources to support you to develop your fair food business or organisation. Find training, events and other opportunities to learn from peers."
learn_cta: "Get Inspired"
connect_body: "Search our full directories of producers, hubs and groups to find fair food traders near you. List your business or organisation on the OFN so buyers can find you. Join the community to get advice and solve problems together."
@@ -1371,7 +1370,13 @@ en_BE:
products_in: "in %{oc}"
products_at: "at %{distributor}"
products_elsewhere: "Products found elsewhere"
email_welcome: "Welcome"
email_confirmed: "Thank you for confirming your email address."
email_registered: "is now part of"
email_userguide_html: "The User Guide with detailed support for setting up your Producer or Hub is here: %{link}"
email_admin_html: "You can manage your account by logging into the %{link} or by clicking on the cog in the top right hand side of the homepage, and selecting Administration."
email_community_html: "We also have an online forum for community discussion related to OFN software and the unique challenges of running a food enterprise. You are encouraged to join in. We are constantly evolving and your input into this forum will shape what happens next. %{link}"
join_community: "Join the community"
email_confirmation_activate_account: "Before we can activate your new account, we need to confirm your email address."
email_confirmation_greeting: "Hi, %{contact}!"
email_confirmation_profile_created: "A profile for %{name} has been successfully created! To activate your Profile we need to confirm this email address."
@@ -2793,6 +2798,7 @@ en_BE:
products: "Products"
option_types: "Option Types"
properties: "Properties"
prototypes: "Prototypes"
variant_overrides: "Inventory"
reports: "Reports"
configuration: "Configuration"
@@ -2993,15 +2999,6 @@ en_BE:
email_confirmation:
confirmation_pending: "Email confirmation is pending. We've sent a confirmation email to %{address}."
variants:
index:
sku: "SKU"
price: "Price"
no_results: "No results"
option_types: "Option Types"
form:
sku: "SKU"
price: "Price"
display_as: "Display As"
autocomplete:
producer_name: "Producer"
unit: "Unit"

View File

@@ -111,12 +111,6 @@ en_CA:
subject: "Please confirm the email address for %{enterprise}"
welcome:
subject: "%{enterprise} is now on %{sitename}"
email_welcome: "Welcome"
email_registered: "is now part of"
email_userguide_html: "The User Guide with detailed support for setting up your Producer or Hub is here: %{link}"
email_admin_html: "You can manage your account by logging into the %{link} or by clicking on the cog in the top right hand side of the homepage, and selecting Administration."
email_community_html: "We also have an online forum for community discussion related to OFN software and the unique challenges of running a food enterprise. You are encouraged to join in. We are constantly evolving and your input into this forum will shape what happens next. %{link}"
join_community: "Join the community"
invite_manager:
subject: "%{enterprise} has invited you to be a manager"
producer_mailer:
@@ -326,6 +320,15 @@ en_CA:
number_localization:
number_localization_settings: "Number Localization Settings"
enable_localized_number: "Use the international thousand/decimal separator logic"
cache_settings:
edit:
title: "Caching"
distributor: "Distributor"
order_cycle: "Order Cycle"
status: "Status"
diff: "Diff"
error: "Error"
enable_products_cache: "Enable Products Cache?"
invoice_settings:
edit:
title: "Invoice Settings"
@@ -419,23 +422,19 @@ en_CA:
av_on: "Av. On"
import_date: Imported
upload_an_image: Upload an image
seo:
product_search_keywords: "Product Search Keywords"
product_search_tip: "Type words to help search your products in the shops. Use space to separate each keyword."
SEO_keywords: "SEO Keywords"
seo_tip: "Type words to help search your products in the web. Use space to separate each keyword."
search: "Search"
product_search_keywords: Product Search Keywords
product_search_tip: Type words to help search your products in the shops. Use space to separate each keyword.
SEO_keywords: SEO Keywords
seo_tip: Type words to help search your products in the web. Use space to separate each keyword.
Search: Search
properties:
property_name: "Property Name"
inherited_property: "Inherited Property"
property_name: Property Name
inherited_property: Inherited Property
variants:
infinity: "Infinity"
to_order_tip: "Items made to order do not have a set stock level, such as loaves of bread made fresh to order."
group_buy_options: "Group Buy Options"
back_to_products_list: "Back to products list"
tabs:
group_buy_options: "Group Buy Options"
images: "Images"
product_properties: "Product Properties"
product_import:
title: Product Import
file_not_found: File not found or could not be opened
@@ -1378,7 +1377,13 @@ en_CA:
products_in: "in %{oc}"
products_at: "at %{distributor}"
products_elsewhere: "Products found elsewhere"
email_welcome: "Welcome"
email_confirmed: "Thank you for confirming your email address."
email_registered: "is now part of"
email_userguide_html: "The User Guide with detailed support for setting up your Producer or Hub is here: %{link}"
email_admin_html: "You can manage your account by logging into the %{link} or by clicking on the cog in the top right hand side of the homepage, and selecting Administration."
email_community_html: "We also have an online forum for community discussion related to OFN software and the unique challenges of running a food enterprise. You are encouraged to join in. We are constantly evolving and your input into this forum will shape what happens next. %{link}"
join_community: "Join the community"
email_confirmation_activate_account: "Before we can activate your new account, we need to confirm your email address."
email_confirmation_greeting: "Hi, %{contact}!"
email_confirmation_profile_created: "A profile for %{name} has been successfully created! To activate your Profile we need to confirm this email address."
@@ -2804,6 +2809,7 @@ en_CA:
products: "Products"
option_types: "Option Types"
properties: "Properties"
prototypes: "Prototypes"
variant_overrides: "Inventory"
reports: "Reports"
configuration: "Configuration"
@@ -3004,15 +3010,6 @@ en_CA:
email_confirmation:
confirmation_pending: "Email confirmation is pending. We've sent a confirmation email to %{address}."
variants:
index:
sku: "SKU"
price: "Price"
no_results: "No results"
option_types: "Option Types"
form:
sku: "SKU"
price: "Price"
display_as: "Display As"
autocomplete:
producer_name: "Producer"
unit: "Unit"

Some files were not shown because too many files have changed in this diff Show More