mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-14 18:56:49 +00:00
Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0ee4aab01 | ||
|
|
452593b6f1 | ||
|
|
23740ef908 | ||
|
|
ba04208999 | ||
|
|
590ce67f38 | ||
|
|
e11ea929c3 | ||
|
|
fc9f61ecf8 | ||
|
|
6d283ac839 | ||
|
|
725807f66d | ||
|
|
149df6569c | ||
|
|
7daa7032aa | ||
|
|
8b7119beea | ||
|
|
201e87bf12 | ||
|
|
0fffd6b4e3 | ||
|
|
c516d40d4a | ||
|
|
07d4528276 | ||
|
|
4ace780431 | ||
|
|
b69c3fd826 | ||
|
|
51df8de64f | ||
|
|
d4a5829858 | ||
|
|
ff5fe66994 | ||
|
|
37e50a68e4 | ||
|
|
a72c662b97 | ||
|
|
ff2db0c5f8 | ||
|
|
e9c60a33b9 | ||
|
|
8e059d3c69 | ||
|
|
806ba94a2e | ||
|
|
4bec583bff | ||
|
|
90256f9c28 | ||
|
|
eb284c1742 | ||
|
|
b614e17f48 | ||
|
|
1a450733a3 | ||
|
|
ffde7a38df | ||
|
|
8b4b0621db | ||
|
|
5259eaae5f | ||
|
|
b0ad0fccfa | ||
|
|
2a83ad8689 | ||
|
|
c127110192 | ||
|
|
0470725112 | ||
|
|
0623bab084 | ||
|
|
4a0df684c7 | ||
|
|
7dccb5ba90 | ||
|
|
5a4be24df0 | ||
|
|
5cb5967977 | ||
|
|
aeb8d30dae | ||
|
|
1822fd97a6 | ||
|
|
4ff3e9fe10 | ||
|
|
a63994440d | ||
|
|
f6d0de1454 | ||
|
|
9b0e27a9d1 | ||
|
|
415d88f302 | ||
|
|
f9c98ea9a1 | ||
|
|
369a5a8a2f | ||
|
|
62341c6381 | ||
|
|
fa1becb791 | ||
|
|
50a1704994 | ||
|
|
302538c370 | ||
|
|
0f80b6ce12 | ||
|
|
69fb8b2afe | ||
|
|
1df8fc903e | ||
|
|
9a2dcb89af | ||
|
|
1661591f6c | ||
|
|
6dde720039 | ||
|
|
a54b725d6d | ||
|
|
265e76e8ca | ||
|
|
1516069888 | ||
|
|
cd263b761c | ||
|
|
c952ad16ad | ||
|
|
ca09c58f26 | ||
|
|
7d21d88dc9 | ||
|
|
31b62d6296 | ||
|
|
2dfcedad56 | ||
|
|
b9ddb39edc | ||
|
|
4aa6c673ff | ||
|
|
aa3c1aa0fe | ||
|
|
31bac9641f | ||
|
|
b7f7038934 | ||
|
|
6c054e6078 | ||
|
|
18974c68e1 | ||
|
|
78ab852141 | ||
|
|
4497173213 | ||
|
|
4d74d246e8 | ||
|
|
cc51537e93 | ||
|
|
07aececdcf | ||
|
|
c3fbf9cdf9 | ||
|
|
180598c603 | ||
|
|
69a5527e24 | ||
|
|
e4a6b3880f | ||
|
|
96ce4deb45 | ||
|
|
a3c179bd3f | ||
|
|
a57504ba1f | ||
|
|
25451eed6b | ||
|
|
50765563f8 | ||
|
|
2ae75ce13e | ||
|
|
18aa16650d | ||
|
|
314ed50e0f | ||
|
|
7346a49982 | ||
|
|
5182286218 | ||
|
|
a267848394 | ||
|
|
104bd31f9b | ||
|
|
8bc9985edb | ||
|
|
6dfc927730 | ||
|
|
3771e26eba | ||
|
|
fd21d35aee | ||
|
|
1417b924d2 | ||
|
|
2912c1b87d | ||
|
|
e746a0db7d | ||
|
|
84a2886003 | ||
|
|
c668677b8a | ||
|
|
2490cbfccb | ||
|
|
20a46a791c | ||
|
|
0e4fe08ac4 | ||
|
|
cf0f716534 | ||
|
|
b70cfa5968 | ||
|
|
f77beb50ff | ||
|
|
a941280982 | ||
|
|
9d40ee49e6 | ||
|
|
6abbdecb97 | ||
|
|
660ce92c27 | ||
|
|
c5bcef6ae4 | ||
|
|
d26a0b6b73 | ||
|
|
c464b21d76 | ||
|
|
c83d249147 | ||
|
|
2d872c25bf | ||
|
|
0a88738faa | ||
|
|
4d6af57f79 | ||
|
|
110fd3ecdf | ||
|
|
1cb065f829 | ||
|
|
1cfa499b0e | ||
|
|
3fc0d4a666 | ||
|
|
de6c96d138 | ||
|
|
11974689ef | ||
|
|
4398ea12b8 |
@@ -341,7 +341,6 @@ Metrics/LineLength:
|
||||
- spec/performance/orders_controller_spec.rb
|
||||
- spec/performance/shop_controller_spec.rb
|
||||
- spec/requests/checkout/failed_checkout_spec.rb
|
||||
- spec/requests/checkout/stripe_connect_spec.rb
|
||||
- spec/requests/embedded_shopfronts_headers_spec.rb
|
||||
- spec/requests/shop_spec.rb
|
||||
- spec/serializers/admin/customer_serializer_spec.rb
|
||||
@@ -482,63 +481,26 @@ Metrics/AbcSize:
|
||||
|
||||
Metrics/BlockLength:
|
||||
Max: 25
|
||||
ExcludedMethods: ["class_eval", "collection", "context", "describe", "it", "member", "namespace", "resource", "resources"]
|
||||
ExcludedMethods: [
|
||||
"class_eval",
|
||||
"collection",
|
||||
"context",
|
||||
"describe",
|
||||
"feature",
|
||||
"it",
|
||||
"member",
|
||||
"namespace",
|
||||
"resource",
|
||||
"resources",
|
||||
"scenario"
|
||||
]
|
||||
Exclude:
|
||||
- lib/tasks/data.rake
|
||||
- lib/tasks/dev.rake
|
||||
- spec/controllers/spree/admin/invoices_controller_spec.rb
|
||||
- spec/factories/variant_factory.rb
|
||||
- spec/features/admin/adjustments_spec.rb
|
||||
- spec/features/admin/bulk_order_management_spec.rb
|
||||
- spec/features/admin/bulk_product_update_spec.rb
|
||||
- spec/features/admin/caching_spec.rb
|
||||
- spec/features/admin/content_spec.rb
|
||||
- spec/features/admin/customers_spec.rb
|
||||
- spec/features/admin/enterprise_fees_spec.rb
|
||||
- spec/features/admin/enterprise_groups_spec.rb
|
||||
- spec/features/admin/enterprise_relationships_spec.rb
|
||||
- spec/features/admin/enterprise_roles_spec.rb
|
||||
- spec/features/admin/enterprises/images_spec.rb
|
||||
- spec/features/admin/enterprises/index_spec.rb
|
||||
- spec/features/admin/enterprises_spec.rb
|
||||
- spec/features/admin/enterprise_user_spec.rb
|
||||
- spec/features/admin/multilingual_spec.rb
|
||||
- spec/features/admin/order_cycles_spec.rb
|
||||
- spec/features/admin/orders_spec.rb
|
||||
- spec/features/admin/overview_spec.rb
|
||||
- spec/features/admin/payment_method_spec.rb
|
||||
- spec/features/admin/product_import_spec.rb
|
||||
- spec/features/admin/products_spec.rb
|
||||
- spec/features/admin/reports/enterprise_fee_summaries_spec.rb
|
||||
- spec/features/admin/reports_spec.rb
|
||||
- spec/features/admin/schedules_spec.rb
|
||||
- spec/features/admin/shipping_methods_spec.rb
|
||||
- spec/features/admin/subscriptions_spec.rb
|
||||
- spec/features/admin/tag_rules_spec.rb
|
||||
- spec/features/admin/tax_settings_spec.rb
|
||||
- spec/features/admin/users_spec.rb
|
||||
- spec/features/admin/variant_overrides_spec.rb
|
||||
- spec/features/admin/variants_spec.rb
|
||||
- spec/features/consumer/account/cards_spec.rb
|
||||
- spec/features/consumer/account/settings_spec.rb
|
||||
- spec/features/consumer/account_spec.rb
|
||||
- spec/features/consumer/authentication_spec.rb
|
||||
- spec/features/consumer/cookies_spec.rb
|
||||
- spec/features/consumer/external_services_spec.rb
|
||||
- spec/features/consumer/groups_spec.rb
|
||||
- spec/features/consumer/multilingual_spec.rb
|
||||
- spec/features/consumer/producers_spec.rb
|
||||
- spec/features/consumer/registration_spec.rb
|
||||
- spec/features/consumer/shopping/cart_spec.rb
|
||||
- spec/features/consumer/shopping/checkout_auth_spec.rb
|
||||
- spec/features/consumer/shopping/checkout_spec.rb
|
||||
- spec/features/consumer/shopping/embedded_groups_spec.rb
|
||||
- spec/features/consumer/shopping/embedded_shopfronts_spec.rb
|
||||
- spec/features/consumer/shopping/orders_spec.rb
|
||||
- spec/features/consumer/shopping/products_spec.rb
|
||||
- spec/features/consumer/shopping/shopping_spec.rb
|
||||
- spec/features/consumer/shopping/variant_overrides_spec.rb
|
||||
- spec/features/consumer/shops_spec.rb
|
||||
- spec/lib/open_food_network/group_buy_report_spec.rb
|
||||
- spec/models/tag_rule/discount_order_spec.rb
|
||||
- spec/spec_helper.rb
|
||||
|
||||
@@ -186,7 +186,19 @@ Metrics/AbcSize:
|
||||
|
||||
Metrics/BlockLength:
|
||||
Max: 25
|
||||
ExcludedMethods: ["class_eval", "collection", "context", "describe", "it", "member", "namespace", "resource", "resources"]
|
||||
ExcludedMethods: [
|
||||
"class_eval",
|
||||
"collection",
|
||||
"context",
|
||||
"describe",
|
||||
"feature",
|
||||
"it",
|
||||
"member",
|
||||
"namespace",
|
||||
"resource",
|
||||
"resources",
|
||||
"scenario"
|
||||
]
|
||||
|
||||
Metrics/BlockNesting:
|
||||
Max: 3
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config --exclude-limit 1400`
|
||||
# on 2019-05-28 16:29:07 +0100 using RuboCop version 0.57.2.
|
||||
# on 2019-07-23 14:09:18 +0100 using RuboCop version 0.57.2.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
@@ -32,15 +32,6 @@ Layout/EndAlignment:
|
||||
Layout/IndentHash:
|
||||
EnforcedStyle: consistent
|
||||
|
||||
# Offense count: 7
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
||||
# SupportedStyles: aligned, indented, indented_relative_to_receiver
|
||||
Layout/MultilineMethodCallIndentation:
|
||||
Exclude:
|
||||
- 'app/models/spree/line_item_decorator.rb'
|
||||
- 'app/models/spree/product_decorator.rb'
|
||||
|
||||
# Offense count: 4
|
||||
Lint/AmbiguousOperator:
|
||||
Exclude:
|
||||
@@ -55,7 +46,7 @@ Lint/DuplicateMethods:
|
||||
- 'lib/discourse/single_sign_on.rb'
|
||||
- 'lib/open_food_network/subscription_summary.rb'
|
||||
|
||||
# Offense count: 15
|
||||
# Offense count: 8
|
||||
Lint/IneffectiveAccessModifier:
|
||||
Exclude:
|
||||
- 'app/models/column_preference.rb'
|
||||
@@ -79,7 +70,13 @@ Lint/UnderscorePrefixedVariableName:
|
||||
Exclude:
|
||||
- 'spec/support/cancan_helper.rb'
|
||||
|
||||
# Offense count: 6
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
Lint/UnneededCopDisableDirective:
|
||||
Exclude:
|
||||
- 'app/models/product_import/entry_validator.rb'
|
||||
|
||||
# Offense count: 5
|
||||
# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods.
|
||||
Lint/UselessAccessModifier:
|
||||
Exclude:
|
||||
@@ -89,28 +86,47 @@ Lint/UselessAccessModifier:
|
||||
- 'lib/open_food_network/reports/bulk_coop_report.rb'
|
||||
- 'spec/lib/open_food_network/reports/report_spec.rb'
|
||||
|
||||
# Offense count: 91
|
||||
# Offense count: 8
|
||||
# Configuration parameters: CheckForMethodsWithNoSideEffects.
|
||||
Lint/Void:
|
||||
Exclude:
|
||||
- 'app/serializers/api/enterprise_serializer.rb'
|
||||
- 'spec/features/admin/bulk_product_update_spec.rb'
|
||||
- 'spec/features/admin/enterprise_groups_spec.rb'
|
||||
- 'spec/features/admin/enterprises/index_spec.rb'
|
||||
- 'spec/features/admin/enterprises_spec.rb'
|
||||
- 'spec/features/admin/order_cycles_spec.rb'
|
||||
- 'spec/features/admin/payment_method_spec.rb'
|
||||
- 'spec/features/admin/products_spec.rb'
|
||||
- 'spec/features/admin/variant_overrides_spec.rb'
|
||||
- 'spec/features/admin/variants_spec.rb'
|
||||
- 'spec/features/consumer/shopping/checkout_spec.rb'
|
||||
- 'spec/features/consumer/shopping/shopping_spec.rb'
|
||||
- 'spec/features/consumer/shopping/variant_overrides_spec.rb'
|
||||
|
||||
# Offense count: 109
|
||||
# Offense count: 15
|
||||
Metrics/AbcSize:
|
||||
Max: 36
|
||||
|
||||
# Offense count: 13
|
||||
# Configuration parameters: CountComments, ExcludedMethods.
|
||||
Metrics/BlockLength:
|
||||
Max: 774
|
||||
Max: 115
|
||||
|
||||
# Offense count: 2
|
||||
# Configuration parameters: CountComments.
|
||||
Metrics/ClassLength:
|
||||
Max: 169
|
||||
|
||||
# Offense count: 1
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 8
|
||||
|
||||
# Offense count: 8
|
||||
# Configuration parameters: CountComments.
|
||||
Metrics/MethodLength:
|
||||
Max: 31
|
||||
|
||||
# Offense count: 2
|
||||
# Configuration parameters: CountComments.
|
||||
Metrics/ModuleLength:
|
||||
Max: 208
|
||||
|
||||
# Offense count: 2
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 11
|
||||
|
||||
# Offense count: 7
|
||||
Naming/AccessorMethodName:
|
||||
@@ -160,7 +176,7 @@ Naming/PredicateName:
|
||||
- 'lib/open_food_network/packing_report.rb'
|
||||
- 'lib/tasks/data.rake'
|
||||
|
||||
# Offense count: 12
|
||||
# Offense count: 11
|
||||
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
|
||||
# AllowedNames: io, id, to, by, on, in, at
|
||||
Naming/UncommunicativeMethodParamName:
|
||||
@@ -288,13 +304,12 @@ Style/CaseEquality:
|
||||
- 'app/helpers/angular_form_helper.rb'
|
||||
- 'spec/models/spree/payment_spec.rb'
|
||||
|
||||
# Offense count: 79
|
||||
# Offense count: 78
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AutoCorrect, EnforcedStyle.
|
||||
# SupportedStyles: nested, compact
|
||||
Style/ClassAndModuleChildren:
|
||||
Exclude:
|
||||
- 'app/controllers/spree/store_controller_decorator.rb'
|
||||
- 'app/helpers/angular_form_helper.rb'
|
||||
- 'app/models/calculator/flat_percent_per_item.rb'
|
||||
- 'app/models/spree/concerns/payment_method_distributors.rb'
|
||||
@@ -379,11 +394,27 @@ Style/CommentedKeyword:
|
||||
Exclude:
|
||||
- 'app/controllers/application_controller.rb'
|
||||
|
||||
# Offense count: 4
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions.
|
||||
# SupportedStyles: assign_to_condition, assign_inside_condition
|
||||
Style/ConditionalAssignment:
|
||||
Exclude:
|
||||
- 'app/controllers/spree/api/products_controller.rb'
|
||||
- 'app/controllers/spree/api/taxons_controller.rb'
|
||||
- 'app/controllers/spree/api/variants_controller.rb'
|
||||
|
||||
# Offense count: 2
|
||||
Style/DateTime:
|
||||
Exclude:
|
||||
- 'lib/open_food_network/users_and_enterprises_report.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
Style/EachWithObject:
|
||||
Exclude:
|
||||
- 'app/controllers/spree/api/base_controller.rb'
|
||||
|
||||
# Offense count: 5
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: annotated, template, unannotated
|
||||
@@ -393,7 +424,7 @@ Style/FormatStringToken:
|
||||
- 'lib/open_food_network/sales_tax_report.rb'
|
||||
- 'spec/models/enterprise_spec.rb'
|
||||
|
||||
# Offense count: 69
|
||||
# Offense count: 68
|
||||
# Configuration parameters: MinBodyLength.
|
||||
Style/GuardClause:
|
||||
Exclude:
|
||||
@@ -410,7 +441,9 @@ Style/GuardClause:
|
||||
- 'app/controllers/spree/admin/products_controller_decorator.rb'
|
||||
- 'app/controllers/spree/admin/resource_controller_decorator.rb'
|
||||
- 'app/controllers/spree/admin/variants_controller_decorator.rb'
|
||||
- 'app/controllers/spree/orders_controller_decorator.rb'
|
||||
- 'app/controllers/spree/api/base_controller.rb'
|
||||
- 'app/controllers/spree/checkout_controller.rb'
|
||||
- 'app/controllers/spree/orders_controller.rb'
|
||||
- 'app/controllers/spree/paypal_controller_decorator.rb'
|
||||
- 'app/jobs/products_cache_integrity_checker_job.rb'
|
||||
- 'app/models/enterprise.rb'
|
||||
@@ -434,12 +467,23 @@ Style/GuardClause:
|
||||
- 'spec/support/request/distribution_helper.rb'
|
||||
- 'spec/support/request/shop_workflow.rb'
|
||||
|
||||
# Offense count: 3
|
||||
# Offense count: 6
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
|
||||
# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys
|
||||
Style/HashSyntax:
|
||||
Exclude:
|
||||
- 'app/controllers/spree/api/base_controller.rb'
|
||||
- 'app/controllers/spree/checkout_controller.rb'
|
||||
- 'app/controllers/spree/orders_controller.rb'
|
||||
|
||||
# Offense count: 4
|
||||
Style/IfInsideElse:
|
||||
Exclude:
|
||||
- 'app/controllers/admin/column_preferences_controller.rb'
|
||||
- 'app/controllers/admin/variant_overrides_controller.rb'
|
||||
- 'app/controllers/spree/admin/products_controller_decorator.rb'
|
||||
- 'app/controllers/spree/api/taxons_controller.rb'
|
||||
|
||||
# Offense count: 1
|
||||
Style/MissingRespondToMissing:
|
||||
@@ -492,9 +536,10 @@ Style/RegexpLiteral:
|
||||
- 'spec/mailers/subscription_mailer_spec.rb'
|
||||
- 'spec/models/content_configuration_spec.rb'
|
||||
|
||||
# Offense count: 243
|
||||
# Offense count: 244
|
||||
Style/Send:
|
||||
Exclude:
|
||||
- 'app/controllers/spree/checkout_controller.rb'
|
||||
- 'app/models/spree/shipping_method_decorator.rb'
|
||||
- 'spec/controllers/admin/subscriptions_controller_spec.rb'
|
||||
- 'spec/controllers/checkout_controller_spec.rb'
|
||||
@@ -541,3 +586,9 @@ Style/Send:
|
||||
Style/StructInheritance:
|
||||
Exclude:
|
||||
- 'lib/open_food_network/enterprise_fee_applicator.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
Style/UnlessElse:
|
||||
Exclude:
|
||||
- 'app/controllers/spree/api/variants_controller.rb'
|
||||
|
||||
4
Gemfile
4
Gemfile
@@ -49,6 +49,10 @@ gem 'delayed_job_web'
|
||||
# When merged, revert to upstream gem
|
||||
gem 'simple_form', github: 'RohanM/simple_form'
|
||||
|
||||
# Spree's default pagination gem (locked to the current version used by Spree)
|
||||
# We use it's methods in OFN code as well, so this is a direct dependency
|
||||
gem 'kaminari', '~> 0.14.1'
|
||||
|
||||
gem 'andand'
|
||||
gem 'angularjs-rails', '1.5.5'
|
||||
gem 'aws-sdk'
|
||||
|
||||
21
Gemfile.lock
21
Gemfile.lock
@@ -141,8 +141,8 @@ GEM
|
||||
activerecord (>= 3.2, < 5)
|
||||
acts_as_list (0.2.0)
|
||||
activerecord (>= 3.0)
|
||||
addressable (2.6.0)
|
||||
public_suffix (>= 2.0.2, < 4.0)
|
||||
addressable (2.7.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
andand (1.3.3)
|
||||
angular-rails-templates (0.3.0)
|
||||
railties (>= 3.1)
|
||||
@@ -164,7 +164,7 @@ GEM
|
||||
bcrypt-ruby (3.1.5)
|
||||
bcrypt (>= 3.1.3)
|
||||
blockenspiel (0.5.0)
|
||||
bugsnag (6.11.1)
|
||||
bugsnag (6.12.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
builder (3.0.4)
|
||||
byebug (9.0.6)
|
||||
@@ -210,7 +210,7 @@ GEM
|
||||
compass (~> 1.0.0)
|
||||
sass-rails (< 5.1)
|
||||
sprockets (< 4.0)
|
||||
concurrent-ruby (1.1.4)
|
||||
concurrent-ruby (1.1.5)
|
||||
connection_pool (2.2.2)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
@@ -231,10 +231,10 @@ GEM
|
||||
nokogiri (~> 1.6.0)
|
||||
polyglot
|
||||
rails (>= 3.1)
|
||||
delayed_job (4.1.5)
|
||||
activesupport (>= 3.0, < 5.3)
|
||||
delayed_job_active_record (4.1.3)
|
||||
activerecord (>= 3.0, < 5.3)
|
||||
delayed_job (4.1.8)
|
||||
activesupport (>= 3.0, < 6.1)
|
||||
delayed_job_active_record (4.1.4)
|
||||
activerecord (>= 3.0, < 6.1)
|
||||
delayed_job (>= 3.0, < 5)
|
||||
delayed_job_web (1.4.3)
|
||||
activerecord (> 3.0.0)
|
||||
@@ -485,7 +485,7 @@ GEM
|
||||
actionpack (>= 3.0.0)
|
||||
activesupport (>= 3.0.0)
|
||||
kgio (2.11.2)
|
||||
knapsack (1.17.2)
|
||||
knapsack (1.18.0)
|
||||
rake
|
||||
launchy (2.4.3)
|
||||
addressable (~> 2.3)
|
||||
@@ -739,7 +739,7 @@ GEM
|
||||
nokogiri (~> 1.6)
|
||||
rubyzip (~> 1.0)
|
||||
selenium-webdriver (~> 3.0)
|
||||
webmock (3.6.2)
|
||||
webmock (3.7.1)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
@@ -808,6 +808,7 @@ DEPENDENCIES
|
||||
jquery-rails (= 3.0.4)
|
||||
json_spec (~> 1.1.4)
|
||||
jwt (~> 2.2)
|
||||
kaminari (~> 0.14.1)
|
||||
knapsack
|
||||
letter_opener (>= 1.4.1)
|
||||
listen (= 3.0.8)
|
||||
|
||||
@@ -1,267 +1,287 @@
|
||||
angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $http, $window, BulkProducts, DisplayProperties, dataFetcher, DirtyProducts, VariantUnitManager, StatusMessage, producers, Taxons, SpreeApiAuth, Columns, tax_categories) ->
|
||||
$scope.loading = true
|
||||
$scope.loadingAllPages = true
|
||||
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.StatusMessage = StatusMessage
|
||||
$scope.columns = Columns.columns
|
||||
|
||||
$scope.columns = Columns.columns
|
||||
$scope.variant_unit_options = VariantUnitManager.variantUnitOptions()
|
||||
|
||||
$scope.variant_unit_options = VariantUnitManager.variantUnitOptions()
|
||||
$scope.RequestMonitor = RequestMonitor
|
||||
$scope.pagination = BulkProducts.pagination
|
||||
$scope.per_page_options = [
|
||||
{id: 15, name: t('js.admin.orders.index.per_page', results: 15)},
|
||||
{id: 50, name: t('js.admin.orders.index.per_page', results: 50)},
|
||||
{id: 100, name: t('js.admin.orders.index.per_page', results: 100)}
|
||||
]
|
||||
|
||||
$scope.filterableColumns = [
|
||||
{ name: t("label_producers"), db_column: "producer_name" },
|
||||
{ name: t("name"), db_column: "name" }
|
||||
]
|
||||
$scope.filterableColumns = [
|
||||
{ name: t("label_producers"), db_column: "producer_name" },
|
||||
{ name: t("name"), db_column: "name" }
|
||||
]
|
||||
|
||||
$scope.filterTypes = [
|
||||
{ name: t("equals"), predicate: "eq" },
|
||||
{ name: t("contains"), predicate: "cont" }
|
||||
]
|
||||
$scope.filterTypes = [
|
||||
{ name: t("equals"), predicate: "eq" },
|
||||
{ name: t("contains"), predicate: "cont" }
|
||||
]
|
||||
|
||||
$scope.optionTabs =
|
||||
filters: { title: t("filter_products"), visible: false }
|
||||
$scope.optionTabs =
|
||||
filters: { title: t("filter_products"), visible: false }
|
||||
|
||||
$scope.producers = producers
|
||||
$scope.taxons = Taxons.all
|
||||
$scope.tax_categories = tax_categories
|
||||
$scope.producerFilter = ""
|
||||
$scope.categoryFilter = ""
|
||||
$scope.importDateFilter = ""
|
||||
$scope.page = 1
|
||||
$scope.per_page = 15
|
||||
$scope.products = BulkProducts.products
|
||||
$scope.query = ""
|
||||
$scope.DisplayProperties = DisplayProperties
|
||||
|
||||
$scope.initialise = ->
|
||||
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
|
||||
|
||||
$scope.changePage = (newPage) ->
|
||||
$scope.page = newPage
|
||||
$scope.fetchProducts()
|
||||
|
||||
$scope.fetchProducts = ->
|
||||
removeClearedValues()
|
||||
params = {
|
||||
'q[name_cont]': $scope.query,
|
||||
'q[supplier_id_eq]': $scope.producerFilter,
|
||||
'q[primary_taxon_id_eq]': $scope.categoryFilter,
|
||||
import_date: $scope.importDateFilter,
|
||||
page: $scope.page,
|
||||
per_page: $scope.per_page
|
||||
}
|
||||
RequestMonitor.load(BulkProducts.fetch(params).$promise).then ->
|
||||
$scope.resetProducts()
|
||||
|
||||
removeClearedValues = ->
|
||||
delete $scope.producerFilter if $scope.producerFilter == "0"
|
||||
delete $scope.categoryFilter if $scope.categoryFilter == "0"
|
||||
delete $scope.importDateFilter if $scope.importDateFilter == "0"
|
||||
|
||||
$timeout ->
|
||||
if $scope.showLatestImport
|
||||
$scope.importDateFilter = $scope.importDates[1].id
|
||||
|
||||
$scope.resetProducts = ->
|
||||
DirtyProducts.clear()
|
||||
StatusMessage.clear()
|
||||
|
||||
$scope.updateOnHand = (product) ->
|
||||
on_demand_variants = []
|
||||
if product.variants
|
||||
on_demand_variants = (variant for id, variant of product.variants when variant.on_demand)
|
||||
|
||||
unless product.on_demand || on_demand_variants.length > 0
|
||||
product.on_hand = $scope.onHand(product)
|
||||
|
||||
|
||||
$scope.producers = producers
|
||||
$scope.taxons = Taxons.all
|
||||
$scope.tax_categories = tax_categories
|
||||
$scope.filterProducers = [{id: "0", name: ""}].concat $scope.producers
|
||||
$scope.filterTaxons = [{id: "0", name: ""}].concat $scope.taxons
|
||||
$scope.onHand = (product) ->
|
||||
onHand = 0
|
||||
if product.hasOwnProperty("variants") and product.variants instanceof Object
|
||||
for id, variant of product.variants
|
||||
onHand = onHand + parseInt(if variant.on_hand > 0 then variant.on_hand else 0)
|
||||
else
|
||||
onHand = "error"
|
||||
onHand
|
||||
|
||||
$scope.shiftTab = (tab) ->
|
||||
$scope.visibleTab.visible = false unless $scope.visibleTab == tab || $scope.visibleTab == undefined
|
||||
tab.visible = !tab.visible
|
||||
$scope.visibleTab = tab
|
||||
|
||||
$scope.resetSelectFilters = ->
|
||||
$scope.query = ""
|
||||
$scope.producerFilter = "0"
|
||||
$scope.categoryFilter = "0"
|
||||
$scope.importDateFilter = "0"
|
||||
$scope.products = BulkProducts.products
|
||||
$scope.filteredProducts = []
|
||||
$scope.currentFilters = []
|
||||
$scope.limit = 15
|
||||
$scope.query = ""
|
||||
$scope.DisplayProperties = DisplayProperties
|
||||
|
||||
$scope.initialise = ->
|
||||
SpreeApiAuth.authorise()
|
||||
.then ->
|
||||
$scope.spree_api_key_ok = true
|
||||
$scope.fetchProducts()
|
||||
.catch (message) ->
|
||||
$scope.api_error_msg = message
|
||||
|
||||
$scope.$watchCollection '[query, producerFilter, categoryFilter, importDateFilter]', ->
|
||||
$scope.limit = 15 # Reset limit whenever searching
|
||||
|
||||
$scope.fetchProducts = ->
|
||||
$scope.loading = true
|
||||
$scope.loadingAllPages = true
|
||||
BulkProducts.fetch($scope.currentFilters, ->
|
||||
$scope.loadingAllPages = false
|
||||
).then ->
|
||||
$scope.resetProducts()
|
||||
$scope.loading = false
|
||||
|
||||
$timeout ->
|
||||
if $scope.showLatestImport
|
||||
$scope.importDateFilter = $scope.importDates[1].id
|
||||
|
||||
$scope.resetProducts = ->
|
||||
DirtyProducts.clear()
|
||||
StatusMessage.clear()
|
||||
|
||||
$scope.updateOnHand = (product) ->
|
||||
on_demand_variants = []
|
||||
if product.variants
|
||||
on_demand_variants = (variant for id, variant of product.variants when variant.on_demand)
|
||||
|
||||
unless product.on_demand || on_demand_variants.length > 0
|
||||
product.on_hand = $scope.onHand(product)
|
||||
$scope.editWarn = (product, variant) ->
|
||||
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.onHand = (product) ->
|
||||
onHand = 0
|
||||
if product.hasOwnProperty("variants") and product.variants instanceof Object
|
||||
for id, variant of product.variants
|
||||
onHand = onHand + parseInt(if variant.on_hand > 0 then variant.on_hand else 0)
|
||||
else
|
||||
onHand = "error"
|
||||
onHand
|
||||
$scope.toggleShowAllVariants = ->
|
||||
showVariants = !DisplayProperties.showVariants 0
|
||||
$scope.products.forEach (product) ->
|
||||
DisplayProperties.setShowVariants product.id, showVariants
|
||||
DisplayProperties.setShowVariants 0, showVariants
|
||||
|
||||
$scope.shiftTab = (tab) ->
|
||||
$scope.visibleTab.visible = false unless $scope.visibleTab == tab || $scope.visibleTab == undefined
|
||||
tab.visible = !tab.visible
|
||||
$scope.visibleTab = tab
|
||||
|
||||
$scope.resetSelectFilters = ->
|
||||
$scope.query = ""
|
||||
$scope.producerFilter = "0"
|
||||
$scope.categoryFilter = "0"
|
||||
$scope.importDateFilter = "0"
|
||||
|
||||
$scope.editWarn = (product, variant) ->
|
||||
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.addVariant = (product) ->
|
||||
product.variants.push
|
||||
id: $scope.nextVariantId()
|
||||
unit_value: null
|
||||
unit_description: null
|
||||
on_demand: false
|
||||
display_as: null
|
||||
display_name: null
|
||||
on_hand: null
|
||||
price: null
|
||||
DisplayProperties.setShowVariants product.id, true
|
||||
|
||||
|
||||
$scope.toggleShowAllVariants = ->
|
||||
showVariants = !DisplayProperties.showVariants 0
|
||||
$scope.filteredProducts.forEach (product) ->
|
||||
DisplayProperties.setShowVariants product.id, showVariants
|
||||
DisplayProperties.setShowVariants 0, showVariants
|
||||
$scope.nextVariantId = ->
|
||||
$scope.variantIdCounter = 0 unless $scope.variantIdCounter?
|
||||
$scope.variantIdCounter -= 1
|
||||
$scope.variantIdCounter
|
||||
|
||||
$scope.addVariant = (product) ->
|
||||
product.variants.push
|
||||
id: $scope.nextVariantId()
|
||||
unit_value: null
|
||||
unit_description: null
|
||||
on_demand: false
|
||||
display_as: null
|
||||
display_name: null
|
||||
on_hand: null
|
||||
price: null
|
||||
DisplayProperties.setShowVariants product.id, true
|
||||
|
||||
|
||||
$scope.nextVariantId = ->
|
||||
$scope.variantIdCounter = 0 unless $scope.variantIdCounter?
|
||||
$scope.variantIdCounter -= 1
|
||||
$scope.variantIdCounter
|
||||
|
||||
$scope.deleteProduct = (product) ->
|
||||
if confirm("Are you sure?")
|
||||
$http(
|
||||
method: "DELETE"
|
||||
url: "/api/products/" + product.id + "/soft_delete"
|
||||
).success (data) ->
|
||||
$scope.products.splice $scope.products.indexOf(product), 1
|
||||
DirtyProducts.deleteProduct product.id
|
||||
$scope.displayDirtyProducts()
|
||||
|
||||
|
||||
$scope.deleteVariant = (product, variant) ->
|
||||
if product.variants.length > 1
|
||||
if !$scope.variantSaved(variant)
|
||||
$scope.removeVariant(product, variant)
|
||||
else
|
||||
if confirm(t("are_you_sure"))
|
||||
$http(
|
||||
method: "DELETE"
|
||||
url: "/api/products/" + product.permalink_live + "/variants/" + variant.id + "/soft_delete"
|
||||
).success (data) ->
|
||||
$scope.removeVariant(product, variant)
|
||||
else
|
||||
alert(t("delete_product_variant"))
|
||||
|
||||
$scope.removeVariant = (product, variant) ->
|
||||
product.variants.splice product.variants.indexOf(variant), 1
|
||||
DirtyProducts.deleteVariant product.id, variant.id
|
||||
$scope.displayDirtyProducts()
|
||||
|
||||
|
||||
$scope.cloneProduct = (product) ->
|
||||
BulkProducts.cloneProduct product
|
||||
|
||||
$scope.hasVariants = (product) ->
|
||||
product.variants.length > 0
|
||||
|
||||
|
||||
$scope.hasUnit = (product) ->
|
||||
product.variant_unit_with_scale?
|
||||
|
||||
|
||||
$scope.variantSaved = (variant) ->
|
||||
variant.hasOwnProperty('id') && variant.id > 0
|
||||
|
||||
|
||||
$scope.hasOnDemandVariants = (product) ->
|
||||
(variant for id, variant of product.variants when variant.on_demand).length > 0
|
||||
|
||||
|
||||
$scope.submitProducts = ->
|
||||
# Pack pack $scope.products, so they will match the list returned from the server,
|
||||
# then pack $scope.dirtyProducts, ensuring that the correct product info is sent to the server.
|
||||
$scope.packProduct product for id, product of $scope.products
|
||||
$scope.packProduct product for id, product of DirtyProducts.all()
|
||||
|
||||
productsToSubmit = filterSubmitProducts(DirtyProducts.all())
|
||||
if productsToSubmit.length > 0
|
||||
$scope.updateProducts productsToSubmit # Don't submit an empty list
|
||||
else
|
||||
StatusMessage.display 'alert', t("products_change")
|
||||
|
||||
|
||||
$scope.updateProducts = (productsToSubmit) ->
|
||||
$scope.displayUpdating()
|
||||
$scope.deleteProduct = (product) ->
|
||||
if confirm("Are you sure?")
|
||||
$http(
|
||||
method: "POST"
|
||||
url: "/admin/products/bulk_update"
|
||||
data:
|
||||
products: productsToSubmit
|
||||
filters: $scope.currentFilters
|
||||
).success((data) ->
|
||||
DirtyProducts.clear()
|
||||
BulkProducts.updateVariantLists(data.products || [])
|
||||
$timeout -> $scope.displaySuccess()
|
||||
).error (data, status) ->
|
||||
if status == 400 && data.errors? && data.errors.length > 0
|
||||
errors = error + "\n" for error in data.errors
|
||||
alert t("products_update_error") + "\n" + errors
|
||||
$scope.displayFailure t("products_update_error")
|
||||
else
|
||||
$scope.displayFailure t("products_update_error_data") + status
|
||||
method: "DELETE"
|
||||
url: "/api/products/" + product.id + "/soft_delete"
|
||||
).success (data) ->
|
||||
$scope.products.splice $scope.products.indexOf(product), 1
|
||||
DirtyProducts.deleteProduct product.id
|
||||
$scope.displayDirtyProducts()
|
||||
|
||||
$scope.cancel = (destination) ->
|
||||
$window.location = destination
|
||||
|
||||
$scope.packProduct = (product) ->
|
||||
if product.variant_unit_with_scale
|
||||
match = product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
|
||||
if match
|
||||
product.variant_unit = match[1]
|
||||
product.variant_unit_scale = parseFloat(match[2])
|
||||
else
|
||||
product.variant_unit = product.variant_unit_with_scale
|
||||
product.variant_unit_scale = null
|
||||
$scope.deleteVariant = (product, variant) ->
|
||||
if product.variants.length > 1
|
||||
if !$scope.variantSaved(variant)
|
||||
$scope.removeVariant(product, variant)
|
||||
else
|
||||
product.variant_unit = product.variant_unit_scale = null
|
||||
if confirm(t("are_you_sure"))
|
||||
$http(
|
||||
method: "DELETE"
|
||||
url: "/api/products/" + product.permalink_live + "/variants/" + variant.id + "/soft_delete"
|
||||
).success (data) ->
|
||||
$scope.removeVariant(product, variant)
|
||||
else
|
||||
alert(t("delete_product_variant"))
|
||||
|
||||
$scope.packVariant product, product.master if product.master
|
||||
|
||||
if product.variants
|
||||
for id, variant of product.variants
|
||||
$scope.packVariant product, variant
|
||||
$scope.removeVariant = (product, variant) ->
|
||||
product.variants.splice product.variants.indexOf(variant), 1
|
||||
DirtyProducts.deleteVariant product.id, variant.id
|
||||
$scope.displayDirtyProducts()
|
||||
|
||||
|
||||
$scope.packVariant = (product, variant) ->
|
||||
if variant.hasOwnProperty("unit_value_with_description")
|
||||
match = variant.unit_value_with_description.match(/^([\d\.]+(?= |$)|)( |)(.*)$/)
|
||||
if match
|
||||
product = BulkProducts.find product.id
|
||||
variant.unit_value = parseFloat(match[1])
|
||||
variant.unit_value = null if isNaN(variant.unit_value)
|
||||
variant.unit_value *= product.variant_unit_scale if variant.unit_value && product.variant_unit_scale
|
||||
variant.unit_description = match[3]
|
||||
$scope.cloneProduct = (product) ->
|
||||
BulkProducts.cloneProduct product
|
||||
|
||||
$scope.incrementLimit = ->
|
||||
if $scope.limit < $scope.products.length
|
||||
$scope.limit = $scope.limit + 5
|
||||
$scope.hasVariants = (product) ->
|
||||
product.variants.length > 0
|
||||
|
||||
|
||||
$scope.displayUpdating = ->
|
||||
StatusMessage.display 'progress', t("saving")
|
||||
$scope.hasUnit = (product) ->
|
||||
product.variant_unit_with_scale?
|
||||
|
||||
|
||||
$scope.displaySuccess = ->
|
||||
StatusMessage.display 'success',t("products_changes_saved")
|
||||
$scope.bulk_product_form.$setPristine()
|
||||
$scope.variantSaved = (variant) ->
|
||||
variant.hasOwnProperty('id') && variant.id > 0
|
||||
|
||||
|
||||
$scope.displayFailure = (failMessage) ->
|
||||
StatusMessage.display 'failure', t("products_update_error_msg") + "#{failMessage}"
|
||||
$scope.hasOnDemandVariants = (product) ->
|
||||
(variant for id, variant of product.variants when variant.on_demand).length > 0
|
||||
|
||||
|
||||
$scope.displayDirtyProducts = ->
|
||||
count = DirtyProducts.count()
|
||||
switch count
|
||||
when 0 then StatusMessage.clear()
|
||||
when 1 then StatusMessage.display 'notice', t("one_product_unsaved")
|
||||
else StatusMessage.display 'notice', t("products_unsaved", n: count)
|
||||
$scope.submitProducts = ->
|
||||
# Pack pack $scope.products, so they will match the list returned from the server,
|
||||
# then pack $scope.dirtyProducts, ensuring that the correct product info is sent to the server.
|
||||
$scope.packProduct product for id, product of $scope.products
|
||||
$scope.packProduct product for id, product of DirtyProducts.all()
|
||||
|
||||
productsToSubmit = filterSubmitProducts(DirtyProducts.all())
|
||||
if productsToSubmit.length > 0
|
||||
$scope.updateProducts productsToSubmit # Don't submit an empty list
|
||||
else
|
||||
StatusMessage.display 'alert', t("products_change")
|
||||
|
||||
|
||||
$scope.updateProducts = (productsToSubmit) ->
|
||||
$scope.displayUpdating()
|
||||
$http(
|
||||
method: "POST"
|
||||
url: "/admin/products/bulk_update"
|
||||
data:
|
||||
products: productsToSubmit
|
||||
filters:
|
||||
'q[name_cont]': $scope.query
|
||||
'q[supplier_id_eq]': $scope.producerFilter
|
||||
'q[primary_taxon_id_eq]': $scope.categoryFilter
|
||||
import_date: $scope.importDateFilter
|
||||
page: $scope.page
|
||||
per_page: $scope.per_page
|
||||
).success((data) ->
|
||||
DirtyProducts.clear()
|
||||
BulkProducts.updateVariantLists(data.products || [])
|
||||
$timeout -> $scope.displaySuccess()
|
||||
).error (data, status) ->
|
||||
if status == 400 && data.errors? && data.errors.length > 0
|
||||
errors = error + "\n" for error in data.errors
|
||||
alert t("products_update_error") + "\n" + errors
|
||||
$scope.displayFailure t("products_update_error")
|
||||
else
|
||||
$scope.displayFailure t("products_update_error_data") + status
|
||||
|
||||
$scope.cancel = (destination) ->
|
||||
$window.location = destination
|
||||
|
||||
$scope.packProduct = (product) ->
|
||||
if product.variant_unit_with_scale
|
||||
match = product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
|
||||
if match
|
||||
product.variant_unit = match[1]
|
||||
product.variant_unit_scale = parseFloat(match[2])
|
||||
else
|
||||
product.variant_unit = product.variant_unit_with_scale
|
||||
product.variant_unit_scale = null
|
||||
else
|
||||
product.variant_unit = product.variant_unit_scale = null
|
||||
|
||||
$scope.packVariant product, product.master if product.master
|
||||
|
||||
if product.variants
|
||||
for id, variant of product.variants
|
||||
$scope.packVariant product, variant
|
||||
|
||||
|
||||
$scope.packVariant = (product, variant) ->
|
||||
if variant.hasOwnProperty("unit_value_with_description")
|
||||
match = variant.unit_value_with_description.match(/^([\d\.]+(?= |$)|)( |)(.*)$/)
|
||||
if match
|
||||
product = BulkProducts.find product.id
|
||||
variant.unit_value = parseFloat(match[1])
|
||||
variant.unit_value = null if isNaN(variant.unit_value)
|
||||
variant.unit_value *= product.variant_unit_scale if variant.unit_value && product.variant_unit_scale
|
||||
variant.unit_description = match[3]
|
||||
|
||||
$scope.incrementLimit = ->
|
||||
if $scope.limit < $scope.products.length
|
||||
$scope.limit = $scope.limit + 5
|
||||
|
||||
|
||||
$scope.displayUpdating = ->
|
||||
StatusMessage.display 'progress', t("saving")
|
||||
|
||||
|
||||
$scope.displaySuccess = ->
|
||||
StatusMessage.display 'success',t("products_changes_saved")
|
||||
$scope.bulk_product_form.$setPristine()
|
||||
|
||||
|
||||
$scope.displayFailure = (failMessage) ->
|
||||
StatusMessage.display 'failure', t("products_update_error_msg") + "#{failMessage}"
|
||||
|
||||
|
||||
$scope.displayDirtyProducts = ->
|
||||
count = DirtyProducts.count()
|
||||
switch count
|
||||
when 0 then StatusMessage.clear()
|
||||
when 1 then StatusMessage.display 'notice', t("one_product_unsaved")
|
||||
else StatusMessage.display 'notice', t("products_unsaved", n: count)
|
||||
|
||||
|
||||
filterSubmitProducts = (productsToFilter) ->
|
||||
|
||||
@@ -23,7 +23,7 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor,
|
||||
|
||||
$scope.fetchResults = (page=1) ->
|
||||
$scope.resetSelected()
|
||||
Orders.index({
|
||||
params = {
|
||||
'q[completed_at_lt]': $scope['q']['completed_at_lt'],
|
||||
'q[completed_at_gt]': $scope['q']['completed_at_gt'],
|
||||
'q[state_eq]': $scope['q']['state_eq'],
|
||||
@@ -39,7 +39,8 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor,
|
||||
'q[s]': $scope.sorting || 'completed_at desc',
|
||||
per_page: $scope.per_page,
|
||||
page: page
|
||||
})
|
||||
}
|
||||
RequestMonitor.load(Orders.index(params).$promise)
|
||||
|
||||
$scope.resetSelected = ->
|
||||
$scope.selected_orders.length = 0
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
angular.module("admin.resources").factory 'ProductResource', ($resource) ->
|
||||
$resource('/admin/product/:id/:action.json', {}, {
|
||||
'index':
|
||||
url: '/api/products/bulk_products.json'
|
||||
method: 'GET'
|
||||
})
|
||||
@@ -1,15 +1,13 @@
|
||||
angular.module("ofn.admin").factory "BulkProducts", (PagedFetcher, dataFetcher, $http) ->
|
||||
angular.module("ofn.admin").factory "BulkProducts", (ProductResource, dataFetcher, $http) ->
|
||||
new class BulkProducts
|
||||
products: []
|
||||
pagination: {}
|
||||
|
||||
fetch: (filters, onComplete) ->
|
||||
queryString = filters.reduce (qs,f) ->
|
||||
return qs + "q[#{f.property.db_column}_#{f.predicate.predicate}]=#{f.value};"
|
||||
, ""
|
||||
|
||||
url = "/api/products/bulk_products?page=::page::;per_page=20;#{queryString}"
|
||||
processData = (data) => @addProducts data.products
|
||||
PagedFetcher.fetch url, processData, onComplete
|
||||
fetch: (params) ->
|
||||
ProductResource.index params, (data) =>
|
||||
@products.length = 0
|
||||
@addProducts data.products
|
||||
angular.extend(@pagination, data.pagination)
|
||||
|
||||
cloneProduct: (product) ->
|
||||
$http.post("/api/products/" + product.id + "/clone").success (data) =>
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
@API_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:SS Z"
|
||||
|
||||
Darkswarm.filter "date_in_words", ->
|
||||
(date) ->
|
||||
moment(date).fromNow()
|
||||
(date, dateFormat) ->
|
||||
dateFormat ?= @API_DATETIME_FORMAT
|
||||
moment(date, dateFormat).fromNow()
|
||||
|
||||
Darkswarm.filter "sensible_timeframe", (date_in_wordsFilter)->
|
||||
(date) ->
|
||||
if moment().add(2, 'days') < moment(date)
|
||||
(date, dateFormat) ->
|
||||
dateFormat ?= @API_DATETIME_FORMAT
|
||||
|
||||
if moment().add(2, 'days') < moment(date, dateFormat)
|
||||
t 'orders_open'
|
||||
else
|
||||
t('closing') + date_in_wordsFilter(date)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
.select2-container {
|
||||
.select2-choice {
|
||||
.select2-search-choice-close {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
.select2-arrow {
|
||||
width: 22px;
|
||||
|
||||
146
app/controllers/api/products_controller.rb
Normal file
146
app/controllers/api/products_controller.rb
Normal file
@@ -0,0 +1,146 @@
|
||||
require 'open_food_network/permissions'
|
||||
|
||||
module Api
|
||||
class ProductsController < Api::BaseController
|
||||
respond_to :json
|
||||
DEFAULT_PAGE = 1
|
||||
DEFAULT_PER_PAGE = 15
|
||||
|
||||
skip_authorization_check only: [:show, :bulk_products, :overridable]
|
||||
|
||||
def show
|
||||
@product = find_product(params[:id])
|
||||
render json: @product, serializer: Api::Admin::ProductSerializer
|
||||
end
|
||||
|
||||
def create
|
||||
authorize! :create, Spree::Product
|
||||
params[:product][:available_on] ||= Time.zone.now
|
||||
@product = Spree::Product.new(params[:product])
|
||||
begin
|
||||
if @product.save
|
||||
render json: @product, serializer: Api::Admin::ProductSerializer, status: 201
|
||||
else
|
||||
invalid_resource!(@product)
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
@product.permalink = nil
|
||||
retry
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
authorize! :update, Spree::Product
|
||||
@product = find_product(params[:id])
|
||||
if @product.update_attributes(params[:product])
|
||||
render json: @product, serializer: Api::Admin::ProductSerializer, status: 200
|
||||
else
|
||||
invalid_resource!(@product)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize! :delete, Spree::Product
|
||||
@product = find_product(params[:id])
|
||||
@product.update_attribute(:deleted_at, Time.zone.now)
|
||||
@product.variants_including_master.update_all(deleted_at: Time.zone.now)
|
||||
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)
|
||||
|
||||
if params[:import_date].present?
|
||||
product_query = product_query.imported_on(params[:import_date]).group_by_products_id
|
||||
end
|
||||
|
||||
@products = product_query.order('created_at DESC').
|
||||
ransack(params[:q]).result.
|
||||
page(params[:page] || DEFAULT_PAGE).per(params[:per_page] || DEFAULT_PER_PAGE)
|
||||
|
||||
render_paged_products @products
|
||||
end
|
||||
|
||||
def overridable
|
||||
producers = OpenFoodNetwork::Permissions.new(current_api_user).
|
||||
variant_override_producers.by_name
|
||||
|
||||
@products = paged_products_for_producers producers
|
||||
|
||||
render_paged_products @products
|
||||
end
|
||||
|
||||
def soft_delete
|
||||
authorize! :delete, Spree::Product
|
||||
@product = find_product(params[:product_id])
|
||||
authorize! :delete, @product
|
||||
@product.destroy
|
||||
render json: @product, serializer: Api::Admin::ProductSerializer, status: 204
|
||||
end
|
||||
|
||||
# POST /api/products/:product_id/clone
|
||||
#
|
||||
def clone
|
||||
authorize! :create, Spree::Product
|
||||
original_product = find_product(params[:product_id])
|
||||
authorize! :update, original_product
|
||||
|
||||
@product = original_product.duplicate
|
||||
|
||||
render json: @product, serializer: Api::Admin::ProductSerializer, status: 201
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# 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]
|
||||
scope = scope.with_deleted
|
||||
end
|
||||
else
|
||||
scope = Spree::Product.active
|
||||
end
|
||||
|
||||
scope.includes(:master)
|
||||
end
|
||||
|
||||
def paged_products_for_producers(producers)
|
||||
Spree::Product.scoped.
|
||||
merge(product_scope).
|
||||
where(supplier_id: producers).
|
||||
by_producer.by_name.
|
||||
ransack(params[:q]).result.
|
||||
page(params[:page]).per(params[:per_page])
|
||||
end
|
||||
|
||||
def render_paged_products(products)
|
||||
serializer = ActiveModel::ArraySerializer.new(
|
||||
products,
|
||||
each_serializer: ::Api::Admin::ProductSerializer
|
||||
)
|
||||
|
||||
render text: {
|
||||
products: serializer,
|
||||
# This line is used by the PagedFetcher JS service (inventory).
|
||||
pages: products.num_pages,
|
||||
# This hash is used by the BulkProducts JS service.
|
||||
pagination: pagination_data(products)
|
||||
}.to_json
|
||||
end
|
||||
|
||||
def pagination_data(results)
|
||||
{
|
||||
results: results.total_count,
|
||||
pages: results.num_pages,
|
||||
page: (params[:page] || DEFAULT_PAGE).to_i,
|
||||
per_page: (params[:per_page] || DEFAULT_PER_PAGE).to_i
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
79
app/controllers/api/variants_controller.rb
Normal file
79
app/controllers/api/variants_controller.rb
Normal file
@@ -0,0 +1,79 @@
|
||||
module Api
|
||||
class VariantsController < Api::BaseController
|
||||
respond_to :json
|
||||
|
||||
skip_authorization_check only: [:index, :show]
|
||||
before_filter :product
|
||||
|
||||
def index
|
||||
@variants = scope.includes(:option_values).ransack(params[:q]).result
|
||||
render json: @variants, each_serializer: Api::VariantSerializer
|
||||
end
|
||||
|
||||
def show
|
||||
@variant = scope.includes(:option_values).find(params[:id])
|
||||
render json: @variant, serializer: Api::VariantSerializer
|
||||
end
|
||||
|
||||
def create
|
||||
authorize! :create, Spree::Variant
|
||||
@variant = scope.new(params[:variant])
|
||||
if @variant.save
|
||||
render json: @variant, serializer: Api::VariantSerializer, status: 201
|
||||
else
|
||||
invalid_resource!(@variant)
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
authorize! :update, Spree::Variant
|
||||
@variant = scope.find(params[:id])
|
||||
if @variant.update_attributes(params[:variant])
|
||||
render json: @variant, serializer: Api::VariantSerializer, status: 200
|
||||
else
|
||||
invalid_resource!(@product)
|
||||
end
|
||||
end
|
||||
|
||||
def soft_delete
|
||||
@variant = scope.find(params[:variant_id])
|
||||
authorize! :delete, @variant
|
||||
|
||||
VariantDeleter.new.delete(@variant)
|
||||
render json: @variant, serializer: Api::VariantSerializer, status: 204
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize! :delete, Spree::Variant
|
||||
@variant = scope.find(params[:id])
|
||||
@variant.destroy
|
||||
render json: @variant, serializer: Api::VariantSerializer, status: 204
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def product
|
||||
@product ||= Spree::Product.find_by_permalink(params[:product_id]) if params[:product_id]
|
||||
end
|
||||
|
||||
def scope
|
||||
if @product
|
||||
unless current_api_user.has_spree_role?("admin") || params[:show_deleted]
|
||||
variants = @product.variants_including_master
|
||||
else
|
||||
variants = @product.variants_including_master.with_deleted
|
||||
end
|
||||
else
|
||||
variants = Spree::Variant.scoped
|
||||
if current_api_user.has_spree_role?("admin")
|
||||
unless params[:show_deleted]
|
||||
variants = Spree::Variant.active
|
||||
end
|
||||
else
|
||||
variants = variants.active
|
||||
end
|
||||
end
|
||||
variants
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,6 @@
|
||||
require 'open_food_network/spree_api_key_loader'
|
||||
require 'open_food_network/referer_parser'
|
||||
require 'open_food_network/permissions'
|
||||
|
||||
Spree::Admin::ProductsController.class_eval do
|
||||
include OpenFoodNetwork::SpreeApiKeyLoader
|
||||
@@ -54,7 +55,7 @@ Spree::Admin::ProductsController.class_eval do
|
||||
product_set.collection.each { |p| authorize! :update, p }
|
||||
|
||||
if product_set.save
|
||||
redirect_to "/api/products/bulk_products?page=1;per_page=500;#{bulk_index_query(params)}"
|
||||
redirect_to main_app.bulk_products_api_products_path( bulk_index_query(params) )
|
||||
else
|
||||
if product_set.errors.present?
|
||||
render json: { errors: product_set.errors }, status: :bad_request
|
||||
@@ -109,13 +110,7 @@ Spree::Admin::ProductsController.class_eval do
|
||||
end
|
||||
|
||||
def bulk_index_query(params)
|
||||
params[:filters] ||= {}
|
||||
params[:filters].reduce("") do |string, filter|
|
||||
filter_db_column = filter[:property][:db_column]
|
||||
filter_predicate = filter[:predicate][:predicate]
|
||||
filter_value = filter[:value]
|
||||
"#{string}q[#{filter_db_column}_#{filter_predicate}]=#{filter_value};"
|
||||
end
|
||||
params[:filters].to_h.merge(page: params[:page], per_page: params[:per_page])
|
||||
end
|
||||
|
||||
def load_form_data
|
||||
|
||||
130
app/controllers/spree/api/base_controller.rb
Normal file
130
app/controllers/spree/api/base_controller.rb
Normal 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
|
||||
@@ -1,13 +0,0 @@
|
||||
Spree::Api::LineItemsController.class_eval do
|
||||
around_filter :apply_enterprise_fees_with_lock, only: :update
|
||||
|
||||
private
|
||||
|
||||
def apply_enterprise_fees_with_lock
|
||||
authorize! :read, order
|
||||
order.with_lock do
|
||||
yield
|
||||
order.update_distribution_charge!
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,86 +0,0 @@
|
||||
require 'open_food_network/permissions'
|
||||
|
||||
Spree::Api::ProductsController.class_eval do
|
||||
def managed
|
||||
authorize! :admin, Spree::Product
|
||||
authorize! :read, Spree::Product
|
||||
|
||||
@products = product_scope.ransack(params[:q]).result.managed_by(current_api_user).page(params[:page]).per(params[:per_page])
|
||||
respond_with(@products, default_template: :index)
|
||||
end
|
||||
|
||||
# TODO: This should be named 'managed'. Is the action above used? Maybe we should remove it.
|
||||
def bulk_products
|
||||
@products = OpenFoodNetwork::Permissions.new(current_api_user).editable_products.
|
||||
merge(product_scope).
|
||||
order('created_at DESC').
|
||||
ransack(params[:q]).result.
|
||||
page(params[:page]).per(params[:per_page])
|
||||
|
||||
render_paged_products @products
|
||||
end
|
||||
|
||||
def overridable
|
||||
producers = OpenFoodNetwork::Permissions.new(current_api_user).
|
||||
variant_override_producers.by_name
|
||||
|
||||
@products = paged_products_for_producers producers
|
||||
|
||||
render_paged_products @products
|
||||
end
|
||||
|
||||
def soft_delete
|
||||
authorize! :delete, Spree::Product
|
||||
@product = find_product(params[:product_id])
|
||||
authorize! :delete, @product
|
||||
@product.destroy
|
||||
respond_with(@product, status: 204)
|
||||
end
|
||||
|
||||
# POST /api/products/:product_id/clone
|
||||
#
|
||||
def clone
|
||||
authorize! :create, Spree::Product
|
||||
original_product = find_product(params[:product_id])
|
||||
authorize! :update, original_product
|
||||
|
||||
@product = original_product.duplicate
|
||||
|
||||
respond_with(@product, status: 201, default_template: :show)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Copied and modified from Spree::Api::BaseController to allow
|
||||
# enterprise users to access inactive products
|
||||
def product_scope
|
||||
if current_api_user.has_spree_role?("admin") || current_api_user.enterprises.present? # This line modified
|
||||
scope = Spree::Product
|
||||
if params[:show_deleted]
|
||||
scope = scope.with_deleted
|
||||
end
|
||||
else
|
||||
scope = Spree::Product.active
|
||||
end
|
||||
|
||||
scope.includes(:master)
|
||||
end
|
||||
|
||||
def paged_products_for_producers(producers)
|
||||
Spree::Product.scoped.
|
||||
merge(product_scope).
|
||||
where(supplier_id: producers).
|
||||
by_producer.by_name.
|
||||
ransack(params[:q]).result.
|
||||
page(params[:page]).per(params[:per_page])
|
||||
end
|
||||
|
||||
def render_paged_products(products)
|
||||
serializer = ActiveModel::ArraySerializer.new(
|
||||
products,
|
||||
each_serializer: Api::Admin::ProductSerializer
|
||||
)
|
||||
|
||||
render text: { products: serializer, pages: products.num_pages }.to_json
|
||||
end
|
||||
end
|
||||
108
app/controllers/spree/api/shipments_controller.rb
Normal file
108
app/controllers/spree/api/shipments_controller.rb
Normal file
@@ -0,0 +1,108 @@
|
||||
require 'open_food_network/scope_variant_to_hub'
|
||||
|
||||
module Spree
|
||||
module Api
|
||||
class ShipmentsController < Spree::Api::BaseController
|
||||
respond_to :json
|
||||
|
||||
before_filter :find_order
|
||||
before_filter :find_and_update_shipment, only: [:ship, :ready, :add, :remove]
|
||||
|
||||
def create
|
||||
variant = scoped_variant(params[:variant_id])
|
||||
quantity = params[:quantity].to_i
|
||||
@shipment = get_or_create_shipment(params[:stock_location_id])
|
||||
|
||||
@order.contents.add(variant, quantity, nil, @shipment)
|
||||
|
||||
@shipment.refresh_rates
|
||||
@shipment.save!
|
||||
|
||||
respond_with(@shipment.reload, default_template: :show)
|
||||
end
|
||||
|
||||
def update
|
||||
authorize! :read, Shipment
|
||||
@shipment = @order.shipments.find_by_number!(params[:id])
|
||||
params[:shipment] ||= []
|
||||
unlock = params[:shipment].delete(:unlock)
|
||||
|
||||
if unlock == 'yes'
|
||||
@shipment.adjustment.open
|
||||
end
|
||||
|
||||
@shipment.update_attributes(params[:shipment])
|
||||
|
||||
if unlock == 'yes'
|
||||
@shipment.adjustment.close
|
||||
end
|
||||
|
||||
@shipment.reload
|
||||
respond_with(@shipment, default_template: :show)
|
||||
end
|
||||
|
||||
def ready
|
||||
authorize! :read, Shipment
|
||||
unless @shipment.ready?
|
||||
if @shipment.can_ready?
|
||||
@shipment.ready!
|
||||
else
|
||||
render "spree/api/shipments/cannot_ready_shipment", status: :unprocessable_entity
|
||||
return
|
||||
end
|
||||
end
|
||||
respond_with(@shipment, default_template: :show)
|
||||
end
|
||||
|
||||
def ship
|
||||
authorize! :read, Shipment
|
||||
unless @shipment.shipped?
|
||||
@shipment.ship!
|
||||
end
|
||||
respond_with(@shipment, default_template: :show)
|
||||
end
|
||||
|
||||
def add
|
||||
variant = scoped_variant(params[:variant_id])
|
||||
quantity = params[:quantity].to_i
|
||||
|
||||
@order.contents.add(variant, quantity, nil, @shipment)
|
||||
|
||||
respond_with(@shipment, default_template: :show)
|
||||
end
|
||||
|
||||
def remove
|
||||
variant = scoped_variant(params[:variant_id])
|
||||
quantity = params[:quantity].to_i
|
||||
|
||||
@order.contents.remove(variant, quantity, @shipment)
|
||||
@shipment.reload if @shipment.persisted?
|
||||
|
||||
respond_with(@shipment, default_template: :show)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_order
|
||||
@order = Spree::Order.find_by_number!(params[:order_id])
|
||||
authorize! :read, @order
|
||||
end
|
||||
|
||||
def find_and_update_shipment
|
||||
@shipment = @order.shipments.find_by_number!(params[:id])
|
||||
@shipment.update_attributes(params[:shipment])
|
||||
@shipment.reload
|
||||
end
|
||||
|
||||
def scoped_variant(variant_id)
|
||||
variant = Spree::Variant.find(variant_id)
|
||||
OpenFoodNetwork::ScopeVariantToHub.new(@order.distributor).scope(variant)
|
||||
variant
|
||||
end
|
||||
|
||||
def get_or_create_shipment(stock_location_id)
|
||||
@order.shipment || @order.shipments.create(stock_location_id: stock_location_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,47 +0,0 @@
|
||||
require 'open_food_network/scope_variant_to_hub'
|
||||
|
||||
Spree::Api::ShipmentsController.class_eval do
|
||||
def create
|
||||
variant = scoped_variant(params[:variant_id])
|
||||
quantity = params[:quantity].to_i
|
||||
@shipment = get_or_create_shipment(params[:stock_location_id])
|
||||
|
||||
@order.contents.add(variant, quantity, nil, @shipment)
|
||||
|
||||
@shipment.refresh_rates
|
||||
@shipment.save!
|
||||
|
||||
respond_with(@shipment.reload, default_template: :show)
|
||||
end
|
||||
|
||||
def add
|
||||
variant = scoped_variant(params[:variant_id])
|
||||
quantity = params[:quantity].to_i
|
||||
|
||||
@order.contents.add(variant, quantity, nil, @shipment)
|
||||
|
||||
respond_with(@shipment, default_template: :show)
|
||||
end
|
||||
|
||||
def remove
|
||||
variant = scoped_variant(params[:variant_id])
|
||||
quantity = params[:quantity].to_i
|
||||
|
||||
@order.contents.remove(variant, quantity, @shipment)
|
||||
@shipment.reload if @shipment.persisted?
|
||||
|
||||
respond_with(@shipment, default_template: :show)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scoped_variant(variant_id)
|
||||
variant = Spree::Variant.find(variant_id)
|
||||
OpenFoodNetwork::ScopeVariantToHub.new(@order.distributor).scope(variant)
|
||||
variant
|
||||
end
|
||||
|
||||
def get_or_create_shipment(stock_location_id)
|
||||
@order.shipment || @order.shipments.create(stock_location_id: stock_location_id)
|
||||
end
|
||||
end
|
||||
75
app/controllers/spree/api/taxons_controller.rb
Normal file
75
app/controllers/spree/api/taxons_controller.rb
Normal file
@@ -0,0 +1,75 @@
|
||||
module Spree
|
||||
module Api
|
||||
class TaxonsController < Spree::Api::BaseController
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
if taxonomy
|
||||
@taxons = taxonomy.root.children
|
||||
else
|
||||
if params[:ids]
|
||||
@taxons = Taxon.where(id: params[:ids].split(","))
|
||||
else
|
||||
@taxons = Taxon.ransack(params[:q]).result
|
||||
end
|
||||
end
|
||||
respond_with(@taxons)
|
||||
end
|
||||
|
||||
def show
|
||||
@taxon = taxon
|
||||
respond_with(@taxon)
|
||||
end
|
||||
|
||||
def jstree
|
||||
show
|
||||
end
|
||||
|
||||
def create
|
||||
authorize! :create, Taxon
|
||||
@taxon = Taxon.new(params[:taxon])
|
||||
@taxon.taxonomy_id = params[:taxonomy_id]
|
||||
taxonomy = Taxonomy.find_by_id(params[:taxonomy_id])
|
||||
|
||||
if taxonomy.nil?
|
||||
@taxon.errors[:taxonomy_id] = I18n.t(:invalid_taxonomy_id, scope: 'spree.api')
|
||||
invalid_resource!(@taxon) && return
|
||||
end
|
||||
|
||||
@taxon.parent_id = taxonomy.root.id unless params[:taxon][:parent_id]
|
||||
|
||||
if @taxon.save
|
||||
respond_with(@taxon, status: 201, default_template: :show)
|
||||
else
|
||||
invalid_resource!(@taxon)
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
authorize! :update, Taxon
|
||||
if taxon.update_attributes(params[:taxon])
|
||||
respond_with(taxon, status: 200, default_template: :show)
|
||||
else
|
||||
invalid_resource!(taxon)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize! :delete, Taxon
|
||||
taxon.destroy
|
||||
respond_with(taxon, status: 204)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def taxonomy
|
||||
return if params[:taxonomy_id].blank?
|
||||
@taxonomy ||= Taxonomy.find(params[:taxonomy_id])
|
||||
end
|
||||
|
||||
def taxon
|
||||
@taxon ||= taxonomy.taxons.find(params[:id])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,9 +0,0 @@
|
||||
Spree::Api::VariantsController.class_eval do
|
||||
def soft_delete
|
||||
@variant = scope.find(params[:variant_id])
|
||||
authorize! :delete, @variant
|
||||
|
||||
VariantDeleter.new.delete(@variant)
|
||||
respond_with @variant, status: 204
|
||||
end
|
||||
end
|
||||
@@ -29,7 +29,7 @@ module Spree
|
||||
|
||||
def load_order
|
||||
@order = current_order
|
||||
redirect_to main_app.cart_path && return unless @order
|
||||
redirect_to(main_app.cart_path) && return unless @order
|
||||
|
||||
if params[:state]
|
||||
redirect_to checkout_state_path(@order.state) if @order.can_go_to_state?(params[:state])
|
||||
|
||||
120
app/helpers/spree/api/api_helpers.rb
Normal file
120
app/helpers/spree/api/api_helpers.rb
Normal file
@@ -0,0 +1,120 @@
|
||||
module Spree
|
||||
module Api
|
||||
module ApiHelpers
|
||||
def required_fields_for(model)
|
||||
required_fields = model._validators.select do |_field, validations|
|
||||
validations.any? { |v| v.is_a?(ActiveModel::Validations::PresenceValidator) }
|
||||
end.map(&:first) # get fields that are invalid
|
||||
# Permalinks presence is validated, but are really automatically generated
|
||||
# Therefore we shouldn't tell API clients that they MUST send one through
|
||||
required_fields.map!(&:to_s).delete("permalink")
|
||||
required_fields
|
||||
end
|
||||
|
||||
def product_attributes
|
||||
[:id, :name, :description, :price, :available_on, :permalink, :meta_description,
|
||||
:meta_keywords, :shipping_category_id, :taxon_ids]
|
||||
end
|
||||
|
||||
def product_property_attributes
|
||||
[:id, :product_id, :property_id, :value, :property_name]
|
||||
end
|
||||
|
||||
def variant_attributes
|
||||
[:id, :name, :sku, :price, :weight, :height, :width, :depth,
|
||||
:is_master, :cost_price, :permalink]
|
||||
end
|
||||
|
||||
def image_attributes
|
||||
[:id, :position, :attachment_content_type, :attachment_file_name,
|
||||
:type, :attachment_updated_at, :attachment_width, :attachment_height, :alt]
|
||||
end
|
||||
|
||||
def option_value_attributes
|
||||
[:id, :name, :presentation, :option_type_name, :option_type_id]
|
||||
end
|
||||
|
||||
def order_attributes
|
||||
[:id, :number, :item_total, :total, :state, :adjustment_total, :user_id,
|
||||
:created_at, :updated_at, :completed_at, :payment_total,
|
||||
:shipment_state, :payment_state, :email, :special_instructions, :token]
|
||||
end
|
||||
|
||||
def line_item_attributes
|
||||
[:id, :quantity, :price, :variant_id]
|
||||
end
|
||||
|
||||
def option_type_attributes
|
||||
[:id, :name, :presentation, :position]
|
||||
end
|
||||
|
||||
def payment_attributes
|
||||
[:id, :source_type, :source_id, :amount, :payment_method_id,
|
||||
:response_code, :state, :avs_response, :created_at, :updated_at]
|
||||
end
|
||||
|
||||
def payment_method_attributes
|
||||
[:id, :name, :description]
|
||||
end
|
||||
|
||||
def shipment_attributes
|
||||
[:id, :tracking, :number, :cost, :shipped_at, :state]
|
||||
end
|
||||
|
||||
def taxonomy_attributes
|
||||
[:id, :name]
|
||||
end
|
||||
|
||||
def taxon_attributes
|
||||
[:id, :name, :pretty_name, :permalink, :position, :parent_id, :taxonomy_id]
|
||||
end
|
||||
|
||||
def inventory_unit_attributes
|
||||
[:id, :lock_version, :state, :variant_id, :shipment_id, :return_authorization_id]
|
||||
end
|
||||
|
||||
def return_authorization_attributes
|
||||
[:id, :number, :state, :amount, :order_id, :reason, :created_at, :updated_at]
|
||||
end
|
||||
|
||||
def country_attributes
|
||||
[:id, :iso_name, :iso, :iso3, :name, :numcode]
|
||||
end
|
||||
|
||||
def state_attributes
|
||||
[:id, :name, :abbr, :country_id]
|
||||
end
|
||||
|
||||
def adjustment_attributes
|
||||
[:id, :source_type, :source_id, :adjustable_type, :adjustable_id, :originator_type,
|
||||
:originator_id, :amount, :label, :mandatory, :locked, :eligible, :created_at, :updated_at]
|
||||
end
|
||||
|
||||
def creditcard_attributes
|
||||
[:id, :month, :year, :cc_type, :last_digits, :first_name, :last_name,
|
||||
:gateway_customer_profile_id, :gateway_payment_profile_id]
|
||||
end
|
||||
|
||||
def user_attributes
|
||||
[:id, :email, :created_at, :updated_at]
|
||||
end
|
||||
|
||||
def property_attributes
|
||||
[:id, :name, :presentation]
|
||||
end
|
||||
|
||||
def stock_location_attributes
|
||||
[:id, :name, :address1, :address2, :city,
|
||||
:state_id, :state_name, :country_id, :zipcode, :phone, :active]
|
||||
end
|
||||
|
||||
def stock_movement_attributes
|
||||
[:id, :quantity, :stock_item_id]
|
||||
end
|
||||
|
||||
def stock_item_attributes
|
||||
[:id, :count_on_hand, :backorderable, :lock_version, :stock_location_id, :variant_id]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -90,7 +90,6 @@ class Enterprise < ActiveRecord::Base
|
||||
validates :permalink, uniqueness: true, presence: true
|
||||
validate :shopfront_taxons
|
||||
validate :enforce_ownership_limit, if: lambda { owner_id_changed? && !owner_id.nil? }
|
||||
validates :description, length: { maximum: 255 }
|
||||
|
||||
before_validation :initialize_permalink, if: lambda { permalink.nil? }
|
||||
before_validation :ensure_owner_is_manager, if: lambda { owner_id_changed? && !owner_id.nil? }
|
||||
|
||||
@@ -4,6 +4,12 @@ module Spree
|
||||
|
||||
private
|
||||
|
||||
def check_price
|
||||
if currency.nil?
|
||||
self.currency = Spree::Config[:currency]
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_products_cache
|
||||
variant.andand.refresh_products_cache
|
||||
end
|
||||
|
||||
@@ -56,6 +56,12 @@ Spree::Product.class_eval do
|
||||
ON (o_order_cycles.id = o_exchanges.order_cycle_id)")
|
||||
}
|
||||
|
||||
scope :imported_on, lambda { |import_date|
|
||||
import_date = Time.zone.parse import_date if import_date.is_a? String
|
||||
import_date = import_date.to_date
|
||||
joins(:variants).merge(Spree::Variant.where(import_date: import_date.beginning_of_day..import_date.end_of_day))
|
||||
}
|
||||
|
||||
scope :with_order_cycles_inner, -> {
|
||||
joins(variants_including_master: { exchanges: :order_cycle })
|
||||
}
|
||||
|
||||
@@ -136,6 +136,16 @@ module Spree
|
||||
has_spree_role?('admin')
|
||||
end
|
||||
|
||||
def generate_spree_api_key!
|
||||
self.spree_api_key = SecureRandom.hex(24)
|
||||
save!
|
||||
end
|
||||
|
||||
def clear_spree_api_key!
|
||||
self.spree_api_key = nil
|
||||
save!
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def password_required?
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
/ replace_contents "td.property_name"
|
||||
|
||||
- if spree_current_user.admin?
|
||||
= f.text_field :property_name, :class => 'autocomplete'
|
||||
- else
|
||||
= f.select :property_name, @properties, { :include_blank => true }, { class: 'select2 fullwidth' }
|
||||
@@ -1,26 +0,0 @@
|
||||
/ insert_after 'table.index.sortable'
|
||||
|
||||
=f.check_box :inherits_properties
|
||||
=f.label :inherits_properties, t(".inherits_properties_checkbox_hint", supplier: @product.supplier.name)
|
||||
%br
|
||||
%br
|
||||
|
||||
#inherited_properties
|
||||
%table.index
|
||||
%thead
|
||||
%tr{"data-hook" => "producer_properties_header"}
|
||||
%th= t('admin.products.properties.inherited_property')
|
||||
%th= t('admin.description')
|
||||
%th.actions
|
||||
%tbody#producer_properties{"data-hook" => ""}
|
||||
- @product.supplier.producer_properties.each do |producer_property|
|
||||
%tr
|
||||
%td= producer_property.property.presentation
|
||||
%td= producer_property.value
|
||||
%td.actions
|
||||
|
||||
:coffee
|
||||
$(document).ready ->
|
||||
$("#inherited_properties").toggle $("input#product_inherits_properties").is(':checked')
|
||||
$("input#product_inherits_properties").change ->
|
||||
$("#inherited_properties").toggle $(this).is(':checked')
|
||||
@@ -1,5 +0,0 @@
|
||||
/ replace "tr[data-hook='product_properties_header']"
|
||||
%tr{"data-hook" => "product_properties_header"}
|
||||
%th= t('admin.products.properties.property_name')
|
||||
%th= t('admin.description')
|
||||
%th.actions
|
||||
@@ -1,6 +1,8 @@
|
||||
class Api::VariantSerializer < ActiveModel::Serializer
|
||||
attributes :id, :is_master, :on_hand, :name_to_display, :unit_to_display, :unit_value
|
||||
attributes :options_text, :on_demand, :price, :fees, :price_with_fees, :product_name
|
||||
attributes :id, :is_master, :product_name, :sku
|
||||
attributes :options_text, :unit_value, :unit_description, :unit_to_display
|
||||
attributes :display_as, :display_name, :name_to_display
|
||||
attributes :price, :on_demand, :on_hand, :fees, :price_with_fees
|
||||
attributes :tag_list
|
||||
|
||||
delegate :price, to: :object
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
object @enterprise
|
||||
|
||||
attributes :id, :name
|
||||
@@ -1,5 +1,5 @@
|
||||
.per-page{'ng-show' => '!RequestMonitor.loading && orders.length > 0'}
|
||||
%input.per-page-select.ofn-select2{type: 'number', data: 'per_page_options', 'ng-model' => 'per_page', 'ng-change' => 'fetchResults()'}
|
||||
%input.per-page-select.ofn-select2{type: 'number', data: 'per_page_options', 'min-search' => 999, 'ng-model' => 'per_page', 'ng-change' => 'fetchResults()'}
|
||||
|
||||
%span.per-page-feedback
|
||||
{{ 'spree.admin.orders.index.results_found' | t:{number: pagination.results} }}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
%tr.product_property.fields{"data-hook" => "product_property", id: "spree_#{dom_id(f.object)}"}
|
||||
%td.no-border
|
||||
%span.handle
|
||||
= f.hidden_field :id
|
||||
%td.property_name
|
||||
- if spree_current_user.admin?
|
||||
= f.text_field :property_name, class: 'autocomplete'
|
||||
- else
|
||||
= f.select :property_name, @properties, { include_blank: true }, { class: 'select2 fullwidth' }
|
||||
%td.value
|
||||
= f.text_field :value, class: 'autocomplete'
|
||||
%td.actions
|
||||
- if f.object.persisted?
|
||||
= link_to_delete f.object, no_text: true
|
||||
71
app/views/spree/admin/product_properties/index.html.haml
Normal file
71
app/views/spree/admin/product_properties/index.html.haml
Normal file
@@ -0,0 +1,71 @@
|
||||
= render partial: 'spree/admin/shared/product_sub_menu'
|
||||
|
||||
= render partial: 'spree/admin/shared/product_tabs', locals: { current: 'Product Properties' }
|
||||
|
||||
= render partial: 'spree/shared/error_messages', locals: { target: @product }
|
||||
|
||||
- content_for :page_actions do
|
||||
%ul.tollbar.inline-menu
|
||||
%li
|
||||
= 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}
|
||||
%thead
|
||||
%tr
|
||||
%th.no-border
|
||||
%th= t('admin.products.properties.property_name')
|
||||
%th= t('admin.description')
|
||||
%th.actions
|
||||
|
||||
%tbody#product_properties
|
||||
= f.fields_for :product_properties do |pp_form|
|
||||
= 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)
|
||||
%br
|
||||
%br
|
||||
|
||||
#inherited_properties
|
||||
%table.index
|
||||
%thead
|
||||
%tr
|
||||
%th= t('admin.products.properties.inherited_property')
|
||||
%th= t('admin.description')
|
||||
%th.actions
|
||||
%tbody#producer_properties
|
||||
- @product.supplier.producer_properties.each do |producer_property|
|
||||
%tr
|
||||
%td= producer_property.property.presentation
|
||||
%td= producer_property.value
|
||||
%td.actions
|
||||
|
||||
|
||||
= render partial: 'spree/admin/shared/edit_resource_links'
|
||||
|
||||
= hidden_field_tag 'clear_product_properties', 'true'
|
||||
|
||||
:coffee
|
||||
$(document).ready ->
|
||||
$("#inherited_properties").toggle $("input#product_inherits_properties").is(':checked')
|
||||
$("input#product_inherits_properties").change ->
|
||||
$("#inherited_properties").toggle $(this).is(':checked')
|
||||
|
||||
:javascript
|
||||
var properties = #{raw(@properties.to_json)};
|
||||
$("#product_properties input.autocomplete").live("keydown", function(){
|
||||
already_auto_completed = $(this).is('ac_input');
|
||||
if (!already_auto_completed) {
|
||||
$(this).autocomplete({source: properties});
|
||||
$(this).focus();
|
||||
}
|
||||
});
|
||||
@@ -4,7 +4,9 @@
|
||||
%div{ ng: { app: 'ofn.admin', controller: 'AdminProductEditCtrl', init: 'initialise()' } }
|
||||
|
||||
= render 'spree/admin/products/index/filters'
|
||||
%hr.divider.sixteen.columns.alpha.omega
|
||||
= render 'spree/admin/products/index/actions'
|
||||
= render 'spree/admin/products/index/indicators'
|
||||
= render 'spree/admin/products/index/products'
|
||||
|
||||
%div{'ng-show' => "!RequestMonitor.loading && products.length > 0" }
|
||||
= render partial: 'admin/shared/angular_pagination'
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
.controls.sixteen.columns.alpha{ 'ng-hide' => 'loading || products.length == 0' }
|
||||
.thirteen.columns
|
||||
%columns-dropdown{ action: "#{controller_name}_#{action_name}" }
|
||||
.controls.sixteen.columns.alpha{ 'ng-hide' => 'RequestMonitor.loading || products.length == 0' }
|
||||
.ten.columns.alpha.index-controls
|
||||
.per-page{'ng-show' => '!RequestMonitor.loading && products.length > 0'}
|
||||
%input.per-page-select.ofn-select2{type: 'number', data: 'per_page_options', 'min-search' => 999, 'ng-model' => 'per_page', 'ng-change' => 'fetchProducts()'}
|
||||
|
||||
%span.per-page-feedback
|
||||
{{ 'spree.admin.orders.index.results_found' | t:{number: pagination.results} }}
|
||||
{{ 'spree.admin.orders.index.viewing' | t:{start: ((pagination.page -1) * pagination.per_page) +1, end: ((pagination.page -1) * pagination.per_page) + products.length} }}
|
||||
|
||||
.six.columns.omega
|
||||
%columns-dropdown{ action: "#{controller_name}_#{action_name}" }
|
||||
|
||||
@@ -1,25 +1,35 @@
|
||||
.filters.sixteen.columns.alpha.omega
|
||||
.quick_search.three.columns.alpha
|
||||
%label{ for: 'quick_filter' }
|
||||
%br
|
||||
%input.quick-search.fullwidth{ ng: {model: 'query'}, name: "quick_filter", type: 'text', placeholder: t('admin.quick_search') }
|
||||
.one.columns
|
||||
.filter_select.three.columns
|
||||
%label{ for: 'producer_filter' }= t 'producer'
|
||||
%br
|
||||
%select.fullwidth{ id: 'producer_filter', 'ofn-select2-min-search' => 5, ng: {model: 'producerFilter', options: 'producer.id as producer.name for producer in filterProducers'} }
|
||||
.filter_select.three.columns
|
||||
%label{ for: 'category_filter' }= t 'category'
|
||||
%br
|
||||
%select.fullwidth{ id: 'category_filter', 'ofn-select2-min-search' => 5, ng: {model: 'categoryFilter', options: 'taxon.id as taxon.name for taxon in filterTaxons'} }
|
||||
.filter_select.three.columns
|
||||
%label{ for: 'import_filter' } Import Date
|
||||
%br
|
||||
%select.fullwidth{ id: 'import_date_filter', 'ofn-select2-min-search' => 5, ng: {model: 'importDateFilter', init: "importDates = #{@import_dates}; showLatestImport = #{@show_latest_import}"}}
|
||||
%option{value: "{{date.id}}", ng: {repeat: "date in importDates track by date.id" }}
|
||||
{{date.name}}
|
||||
%fieldset
|
||||
%legend{align: 'center'}= t(:search)
|
||||
|
||||
.filter_clear.three.columns.omega
|
||||
%label{ for: 'clear_all_filters' }
|
||||
%br
|
||||
%input.fullwidth.red{ :type => 'button', :id => 'clear_all_filters', :value => t('admin.clear_filters'), 'ng-click' => "resetSelectFilters()" }
|
||||
.filters.sixteen.columns.alpha.omega
|
||||
.quick_search.three.columns.alpha
|
||||
%label{ for: 'quick_filter' }
|
||||
%br
|
||||
%input.quick-search.fullwidth{ ng: {model: 'query'}, name: "quick_filter", type: 'text', placeholder: t('admin.quick_search') }
|
||||
.one.columns
|
||||
.filter_select.three.columns
|
||||
%label{ for: 'producer_filter' }= t 'producer'
|
||||
%br
|
||||
%select.fullwidth{ id: 'producer_filter', 'ofn-select2-min-search' => 5, ng: {model: 'producerFilter', options: 'producer.id as producer.name for producer in producers'} }
|
||||
.filter_select.three.columns
|
||||
%label{ for: 'category_filter' }= t 'category'
|
||||
%br
|
||||
%select.fullwidth{ id: 'category_filter', 'ofn-select2-min-search' => 5, ng: {model: 'categoryFilter', options: 'taxon.id as taxon.name for taxon in taxons'} }
|
||||
.filter_select.three.columns
|
||||
%label{ for: 'import_filter' } Import Date
|
||||
%br
|
||||
%select.fullwidth{ id: 'import_date_filter', 'ofn-select2-min-search' => 5, ng: {model: 'importDateFilter', init: "importDates = #{@import_dates}; showLatestImport = #{@show_latest_import}"}}
|
||||
%option{value: "{{date.id}}", ng: {repeat: "date in importDates" }}
|
||||
{{date.name}}
|
||||
|
||||
.filter_clear.three.columns.omega
|
||||
%label{ for: 'clear_all_filters' }
|
||||
%br
|
||||
%input.fullwidth.red{ :type => 'button', :id => 'clear_all_filters', :value => t('admin.clear_filters'), 'ng-click' => "resetSelectFilters()" }
|
||||
|
||||
.clearfix
|
||||
|
||||
.actions.filter-actions
|
||||
%div
|
||||
%a.button.icon-search{'ng-click' => 'fetchProducts()'}
|
||||
= t(:filter_results)
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
%div{ 'ng-show' => '!spree_api_key_ok' }
|
||||
{{ api_error_msg }}
|
||||
|
||||
%div.sixteen.columns.alpha#loading{ 'ng-if' => 'loading' }
|
||||
%div.sixteen.columns.alpha#loading{ 'ng-if' => 'RequestMonitor.loading' }
|
||||
%br
|
||||
%img.spinner{ src: "/assets/spinning-circles.svg" }
|
||||
%h1= t('.title')
|
||||
|
||||
%div.sixteen.columns.alpha{ 'ng-show' => '!loadingAllPages && filteredProducts.length == 0 && query.length==0' }
|
||||
%div.sixteen.columns.alpha{ 'ng-show' => '!RequestMonitor.loading && products.length == 0 && query.length==0' }
|
||||
%h1#no_results= t('.no_products')
|
||||
|
||||
%div.sixteen.columns.alpha{ 'ng-show' => '!loadingAllPages && filteredProducts.length == 0 && query.length!=0' }
|
||||
%div.sixteen.columns.alpha{ 'ng-show' => '!RequestMonitor.loading && products.length == 0 && query.length!=0' }
|
||||
%h1#no_results
|
||||
= t('.no_results')
|
||||
'
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
%div.sixteen.columns.alpha{ 'ng-hide' => 'loading || filteredProducts.length == 0' }
|
||||
%div.sixteen.columns.alpha{ 'ng-hide' => 'RequestMonitor.loading || products.length == 0' }
|
||||
%form{ name: 'bulk_product_form' }
|
||||
%save-bar{ dirty: "bulk_product_form.$dirty", persist: "false" }
|
||||
%input.red{ type: "button", value: t(:save_changes), ng: { click: "submitProducts()", disabled: "!bulk_product_form.$dirty" } }
|
||||
%input{ type: "button", value: t(:close), 'ng-click' => "cancel('#{admin_products_path}')" }
|
||||
|
||||
%table.index#listing_products.bulk{ "infinite-scroll" => "incrementLimit()", "infinite-scroll-distance" => "1" }
|
||||
%table.index#listing_products.bulk
|
||||
|
||||
= render 'spree/admin/products/index/products_head'
|
||||
|
||||
%tbody{ 'ng-repeat' => 'product in filteredProducts = ( products | filter:query | producer: producerFilter | category: categoryFilter | importDate: importDateFilter | limitTo:limit )', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" }
|
||||
%tbody{ 'ng-repeat' => 'product in products', 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'" }
|
||||
|
||||
= render 'spree/admin/products/index/products_product'
|
||||
= render 'spree/admin/products/index/products_variant'
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
%div.sixteen.columns.alpha{ 'ng-hide' => 'loading || products.length == 0', style: "margin-bottom: 10px" }
|
||||
%div.sixteen.columns.alpha{ 'ng-hide' => 'RequestMonitor.loading || products.length == 0', style: "margin-bottom: 10px" }
|
||||
%div.four.columns.alpha
|
||||
%input.four.columns.alpha{ :type => 'button', :value => t(:save_changes), 'ng-click' => 'submitProducts()'}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.form-buttons.filter-actions.actions
|
||||
= button Spree.t('actions.update'), 'icon-refresh'
|
||||
%span.or= Spree.t(:or)
|
||||
= button_link_to Spree.t('actions.cancel'), collection_url, icon: 'icon-remove'
|
||||
6
app/views/spree/admin/shared/_product_sub_menu.html.haml
Normal file
6
app/views/spree/admin/shared/_product_sub_menu.html.haml
Normal file
@@ -0,0 +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 :prototypes
|
||||
@@ -3,11 +3,6 @@
|
||||
:variants_search => spree.admin_search_variants_path(:format => 'json'),
|
||||
:taxons_search => spree.api_taxons_path(:format => 'json'),
|
||||
:user_search => spree.admin_search_users_path(:format => 'json'),
|
||||
:product_search => spree.api_products_path(:format => 'json'),
|
||||
:option_type_search => spree.api_option_types_path(:format => 'json'),
|
||||
:states_search => spree.api_states_path(:format => 'json'),
|
||||
:orders_api => spree.api_orders_path(:format => 'json'),
|
||||
:stock_locations_api => spree.api_stock_locations_path(:format => 'json'),
|
||||
:variants_api => spree.api_variants_path(:format => 'json')
|
||||
:orders_api => spree.api_orders_path(:format => 'json')
|
||||
}.to_json %>;
|
||||
</script>
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
collection @products.order('id ASC')
|
||||
extends "spree/api/products/bulk_show"
|
||||
@@ -1,27 +0,0 @@
|
||||
object @product
|
||||
|
||||
# TODO: This is used by bulk product edit when a product is cloned.
|
||||
# But the list of products is serialized by Api::Admin::ProductSerializer.
|
||||
# This should probably be unified.
|
||||
|
||||
attributes :id, :name, :sku, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand, :inherits_properties
|
||||
attributes :on_hand, :price, :available_on, :permalink_live, :tax_category_id
|
||||
|
||||
# Infinity is not a valid JSON object, but Rails encodes it anyway
|
||||
node( :taxon_ids ) { |p| p.taxons.map(&:id).join(",") }
|
||||
node( :on_hand ) { |p| p.on_hand.nil? ? 0 : p.on_hand.to_f.finite? ? p.on_hand : t(:on_demand) }
|
||||
node( :price ) { |p| p.price.nil? ? '0.0' : p.price }
|
||||
|
||||
node( :available_on ) { |p| p.available_on.blank? ? "" : p.available_on.strftime("%F %T") }
|
||||
node( :permalink_live, &:permalink )
|
||||
node( :producer_id, &:supplier_id )
|
||||
node( :category_id, &:primary_taxon_id )
|
||||
node( :supplier ) do |p|
|
||||
partial 'api/enterprises/bulk_show', object: p.supplier
|
||||
end
|
||||
node( :variants ) do |p|
|
||||
partial 'spree/api/variants/bulk_index', object: p.variants.reorder('spree_variants.id ASC')
|
||||
end
|
||||
node( :master ) do |p|
|
||||
partial 'spree/api/variants/bulk_show', object: p.master
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
collection @variants
|
||||
extends "spree/api/variants/bulk_show"
|
||||
@@ -1,7 +0,0 @@
|
||||
object @variant
|
||||
|
||||
attributes :id, :options_text, :unit_value, :unit_description, :on_demand, :display_as, :display_name
|
||||
|
||||
# Infinity is not a valid JSON object, but Rails encodes it anyway
|
||||
node( :on_hand ) { |v| v.on_hand.nil? ? 0 : ( v.on_hand.to_f.finite? ? v.on_hand : t(:on_demand) ) }
|
||||
node( :price ) { |v| v.price.nil? ? 0.to_f : v.price }
|
||||
@@ -1,4 +0,0 @@
|
||||
= address.address1
|
||||
= ", #{address.address2}" unless address.address2.blank?
|
||||
%br/
|
||||
= [address.city, address.state_text, address.zipcode, address.country.name].compact.join ', '
|
||||
11
app/views/spree/shared/_error_messages.html.haml
Normal file
11
app/views/spree/shared/_error_messages.html.haml
Normal file
@@ -0,0 +1,11 @@
|
||||
- if target && target.errors.any?
|
||||
#errorExplanation.errorExplanation
|
||||
%h2
|
||||
= Spree.t(:errors_prohibited_this_record_from_being_saved, count: target.errors.count)
|
||||
\:
|
||||
%p
|
||||
= Spree.t(:there_were_problems_with_the_following_fields)
|
||||
\:
|
||||
%ul
|
||||
- target.errors.full_messages.each do |msg|
|
||||
%li= msg
|
||||
@@ -1,15 +0,0 @@
|
||||
- filters = @taxon ? @taxon.applicable_filters : []
|
||||
- unless filters.empty?
|
||||
%nav#filters
|
||||
- params[:search] ||= {}
|
||||
- filters.each do |filter|
|
||||
- labels = filter[:labels] || filter[:conds].map {|m,c| [m,m]}
|
||||
- next if labels.empty?
|
||||
|
||||
%h6.filter_name= "Shop by #{filter[:name]}"
|
||||
|
||||
%ul.filter_choices
|
||||
- labels.each do |nm,val|
|
||||
%li.nowrap
|
||||
- active = params[:search][filter[:scope]] && params[:search][filter[:scope]].include?(val.to_s)
|
||||
= link_to nm, "?search[#{filter[:scope].to_s}][]=#{CGI.escape(val)}"
|
||||
@@ -1,23 +0,0 @@
|
||||
- paginated_products = @searcher.retrieve_products if params.key?(:keywords)
|
||||
- paginated_products ||= products
|
||||
|
||||
- if products.empty?
|
||||
= t(:no_products_found)
|
||||
- elsif params.key?(:keywords)
|
||||
%h6.search-results-title= t(:search_results, :keywords => h(params[:keywords]))
|
||||
|
||||
- if products.any?
|
||||
%ul#products.inline.product-listing{"data-hook" => ""}
|
||||
- reset_cycle('default')
|
||||
- products.each do |product|
|
||||
- if Spree::Config[:show_zero_stock_products] || product.has_stock?
|
||||
%li{:class => "columns three #{cycle("alpha", "secondary", "", "omega secondary")}", "data-hook" => "products_list_item", :id => "product_#{product.id}", :itemscope => "", :itemtype => "http://schema.org/Product"}
|
||||
.product-image
|
||||
= link_to small_image(product, :itemprop => "image"), product, :itemprop => 'url'
|
||||
= link_to truncate(product.name, :length => 50), product, :class => 'info', :itemprop => "name", :title => product.name
|
||||
%span.price.selling{:itemprop => "price"}= spree_number_to_currency(product.price)
|
||||
|
||||
- if paginated_products.respond_to?(:num_pages)
|
||||
- params.delete(:search)
|
||||
- params.delete(:taxon)
|
||||
= paginate paginated_products
|
||||
@@ -113,6 +113,7 @@ module Openfoodnetwork
|
||||
)
|
||||
|
||||
config.paths["config/routes"] = %w(
|
||||
config/routes/api.rb
|
||||
config/routes.rb
|
||||
config/routes/admin.rb
|
||||
config/routes/spree.rb
|
||||
|
||||
5
config/initializers/kaminari.rb
Normal file
5
config/initializers/kaminari.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
# Sets a maximum number of returned records when Kaminari pagination is used on a query but no
|
||||
# per_page value has been passed to the #per method.
|
||||
Kaminari.configure do |config|
|
||||
config.max_per_page = 100
|
||||
end
|
||||
@@ -244,6 +244,7 @@ ca:
|
||||
reset_password_token: Reinicia el token de contrasenya
|
||||
expired: ha caducat, si us plau, sol·liciteu-ne un de nou
|
||||
back_to_payments_list: "Torna a la llista de pagaments"
|
||||
maestro_or_solo_cards: "Targetes Maestro/Solo"
|
||||
actions:
|
||||
create_and_add_another: "Crea i afegeix-ne una altra"
|
||||
create: "Crear"
|
||||
@@ -452,6 +453,8 @@ ca:
|
||||
encoding_error: "Comproveu la configuració de l'idioma del vostre fitxer d'origen i assegureu-vos que es desa amb la codificació UTF-8"
|
||||
unexpected_error: "La importació de productes ha detectat un error inesperat mentre obria el fitxer: %{error_message}"
|
||||
index:
|
||||
notice: "Avís"
|
||||
beta_notice: "Aquesta funcionalitat continua en fase beta: podeu experimentar alguns errors mentre l'utilitzeu. No dubteu en contactar amb nosaltres."
|
||||
select_file: Selecciona un full de càlcul per pujar-lo
|
||||
spreadsheet: Full de càlcul
|
||||
choose_import_type: Selecciona el tipus d'importació
|
||||
|
||||
@@ -14,7 +14,9 @@ en_US:
|
||||
spree/product:
|
||||
primary_taxon: "Product Category"
|
||||
supplier: "Supplier"
|
||||
shipping_category_id: "Shipping Category"
|
||||
variant_unit: "Variant Unit"
|
||||
variant_unit_name: "Variant Unit Name"
|
||||
order_cycle:
|
||||
orders_close_at: Close Date
|
||||
errors:
|
||||
@@ -75,6 +77,8 @@ en_US:
|
||||
user_confirmations:
|
||||
spree_user:
|
||||
send_instructions: "You will receive an email with instructions about how to confirm your account in a few minutes."
|
||||
confirmation_sent: "Email confirmation sent"
|
||||
confirmation_not_sent: "Error sending confirmation email"
|
||||
user_registrations:
|
||||
spree_user:
|
||||
signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please open the link to activate your account."
|
||||
@@ -85,9 +89,12 @@ en_US:
|
||||
Were you a guest last time? Perhaps you need to create an account or reset your password.
|
||||
unconfirmed: "You have to confirm your account before continuing."
|
||||
already_registered: "This email address is already registered. Please log in to continue, or go back and use another email address."
|
||||
success:
|
||||
logged_in_succesfully: "Logged in successfully"
|
||||
user_passwords:
|
||||
spree_user:
|
||||
updated_not_active: "Your password has been reset, but your email has not been confirmed yet."
|
||||
updated: "Your password was changed successfully. You are now signed in."
|
||||
send_instructions: "You will receive an email with instructions about how to confirm your account in a few minutes."
|
||||
models:
|
||||
order_cycle:
|
||||
@@ -237,6 +244,7 @@ en_US:
|
||||
reset_password_token: Reset password token
|
||||
expired: has expired, please request a new one
|
||||
back_to_payments_list: "Back to Payments List"
|
||||
maestro_or_solo_cards: "Debit cards"
|
||||
actions:
|
||||
create_and_add_another: "Create and Add Another"
|
||||
create: "Create"
|
||||
@@ -321,6 +329,7 @@ en_US:
|
||||
invoice_settings:
|
||||
edit:
|
||||
title: "Invoice Settings"
|
||||
enable_invoices?: "Enable Invoices?"
|
||||
invoice_style2?: "Use the alternative invoice model that includes total tax breakdown per rate and tax rate info per item (not yet suitable for countries displaying prices excluding tax)"
|
||||
enable_receipt_printing?: "Show options for printing receipts using thermal printers in order dropdown?"
|
||||
stripe_connect_settings:
|
||||
@@ -389,6 +398,7 @@ en_US:
|
||||
calculator: "Calculator"
|
||||
calculator_values: "Calculator Values"
|
||||
search: "Search"
|
||||
name_placeholder: "e.g. packing fee"
|
||||
enterprise_groups:
|
||||
index:
|
||||
new_button: New Enterprise Group
|
||||
@@ -443,6 +453,8 @@ en_US:
|
||||
encoding_error: "Please check the language setting of your source file and ensure it is saved with UTF-8 encoding"
|
||||
unexpected_error: "Product Import encountered an unexpected error when opening the file: %{error_message}"
|
||||
index:
|
||||
notice: "Notice"
|
||||
beta_notice: "This feature is still in beta: you may experience some errors while using it. Please don't hesitate to contact support."
|
||||
select_file: Select a spreadsheet to upload
|
||||
spreadsheet: Spreadsheet
|
||||
choose_import_type: Select import type
|
||||
@@ -647,6 +659,8 @@ en_US:
|
||||
permalink_tip: "This permalink is used to create the url to your shop: %{link}your-shop-name/shop"
|
||||
link_to_front: Link to shop front
|
||||
link_to_front_tip: A direct link to your shopfront on the Open Food Network.
|
||||
ofn_uid: OFN UID
|
||||
ofn_uid_tip: The unique id used to identify the enterprise on Open Food Network.
|
||||
shipping_methods:
|
||||
name: Name
|
||||
applies: Applies?
|
||||
@@ -828,6 +842,7 @@ en_US:
|
||||
add_distributor: 'Add distributor'
|
||||
advanced_settings:
|
||||
title: Advanced Settings
|
||||
choose_product_tip: You can restrict products incoming and outgoing to only %{inventory}'s inventory.
|
||||
preferred_product_selection_from_coordinator_inventory_only_here: Coordinator's Inventory Only
|
||||
preferred_product_selection_from_coordinator_inventory_only_all: All Available Products
|
||||
save_reload: Save and Reload Page
|
||||
@@ -963,6 +978,8 @@ en_US:
|
||||
pause_subscription: Pause Subscription
|
||||
unpause_subscription: Unpause Subscription
|
||||
cancel_subscription: Cancel Subscription
|
||||
filters:
|
||||
query_placeholder: "Search by email..."
|
||||
setup_explanation:
|
||||
just_a_few_more_steps: 'Just a few more steps before you can begin:'
|
||||
enable_subscriptions: "Enable subscriptions for at least one of your shops"
|
||||
@@ -999,6 +1016,8 @@ en_US:
|
||||
charges_not_allowed: Charges are not allowed by this customer
|
||||
no_default_card: Customer has no cards available to charge
|
||||
card_ok: Customer has a card available to charge
|
||||
begins_at_placeholder: "Select a Date"
|
||||
ends_at_placeholder: "Optional"
|
||||
loading_flash:
|
||||
loading: LOADING SUBSCRIPTIONS
|
||||
review:
|
||||
@@ -1012,6 +1031,7 @@ en_US:
|
||||
saved: "SAVED"
|
||||
product_already_in_order: This product has already been added to the order. Please edit the quantity directly.
|
||||
stock:
|
||||
insufficient_stock: "Insufficient stock available"
|
||||
out_of_stock: "Out of Stock"
|
||||
orders:
|
||||
number: Number
|
||||
@@ -1020,6 +1040,7 @@ en_US:
|
||||
cancel_failure_msg: "Sorry, cancellation failed!"
|
||||
confirm_pause_msg: "Are you sure you want to pause this subscription?"
|
||||
pause_failure_msg: "Sorry, pausing failed!"
|
||||
confirm_unpause_msg: "If you have an open Order Cycle in this subscription's schedule, an order will be created for this customer. Are you sure you want to unpause this subscription?"
|
||||
unpause_failure_msg: "Sorry, unpausing failed!"
|
||||
confirm_cancel_open_orders_msg: "Some orders for this subscription are currently open. The customer has already been notified that the order will be placed. Would you like to cancel these order(s) or keep them?"
|
||||
resume_canceled_orders_msg: "Some orders for this subscription can be resumed right now. You can resume them from the orders dropdown."
|
||||
@@ -1057,8 +1078,12 @@ en_US:
|
||||
show_on_map: "Show all on the map"
|
||||
shared:
|
||||
menu:
|
||||
cart:
|
||||
cart: "Cart"
|
||||
signed_in:
|
||||
profile: "Profile"
|
||||
mobile_menu:
|
||||
cart: "Cart"
|
||||
joyride:
|
||||
checkout: "Checkout now"
|
||||
already_ordered_products: "Already ordered in this order cycle"
|
||||
@@ -1095,7 +1120,11 @@ en_US:
|
||||
shop:
|
||||
messages:
|
||||
login: "login"
|
||||
signup: "signup"
|
||||
contact: "contact"
|
||||
require_customer_login: "Only approved customers can access this shop."
|
||||
require_login_html: "If you're already an approved customer, %{login} or %{signup} to proceed. Want to start shopping here? Please %{contact} %{enterprise} and ask about joining."
|
||||
require_customer_html: "If you'd like to start shopping here, please %{contact} %{enterprise} to ask about joining."
|
||||
card_could_not_be_updated: Card could not be updated
|
||||
card_could_not_be_saved: card could not be saved
|
||||
spree_gateway_error_flash_for_checkout: "There was a problem with your payment information: %{error}"
|
||||
@@ -1685,6 +1714,7 @@ en_US:
|
||||
introduction:
|
||||
registration_greeting: "Hi there!"
|
||||
registration_intro: "You can now create a profile for your Producer or Hub"
|
||||
registration_checklist: "What do I need?"
|
||||
registration_time: "5-10 minutes"
|
||||
registration_enterprise_address: "Address"
|
||||
registration_contact_details: "Primary contact details"
|
||||
@@ -1939,6 +1969,7 @@ en_US:
|
||||
spree_admin_supplier: Supplier
|
||||
spree_admin_product_category: Product Category
|
||||
spree_admin_variant_unit_name: Variant unit name
|
||||
unit_name: "Unit name"
|
||||
change_package: "Change Package"
|
||||
spree_admin_single_enterprise_hint: "Hint: To allow people to find you, turn on your visibility under"
|
||||
spree_admin_eg_pickup_from_school: "eg. 'Pick up at Community Center'"
|
||||
@@ -2174,6 +2205,7 @@ en_US:
|
||||
payment_methods: "Payment Methods"
|
||||
payment_method_fee: "Transaction fee"
|
||||
payment_processing_failed: "Payment could not be processed, please check the details you entered"
|
||||
payment_method_not_supported: "That payment method is unsupported. Please choose another one."
|
||||
payment_updated: "Payment Updated"
|
||||
inventory_settings: "Inventory Settings"
|
||||
tag_rules: "Tag Rules"
|
||||
@@ -2364,8 +2396,18 @@ en_US:
|
||||
severity: Severity
|
||||
description: Description
|
||||
resolve: Resolve
|
||||
tag_rules:
|
||||
shipping_method_tagged_top: "Shipping methods tagged"
|
||||
shipping_method_tagged_bottom: "are:"
|
||||
payment_method_tagged_top: "Payment methods tagged"
|
||||
payment_method_tagged_bottom: "are:"
|
||||
order_cycle_tagged_top: "Order Cycles tagged"
|
||||
order_cycle_tagged_bottom: "are:"
|
||||
inventory_tagged_top: "Inventory variants tagged"
|
||||
inventory_tagged_bottom: "are:"
|
||||
new_tag_rule_dialog:
|
||||
select_rule_type: "Select a rule type:"
|
||||
add_rule: "Add Rule"
|
||||
enterprise_fees:
|
||||
inherit_from_product: "Inherit From Product"
|
||||
orders:
|
||||
@@ -2622,6 +2664,7 @@ en_US:
|
||||
fill_in_customer_info: "Please fill in customer info"
|
||||
new_payment: "New Payment"
|
||||
capture: "Capture"
|
||||
void: "Void"
|
||||
configurations: "Configurations"
|
||||
general_settings: "General Settings"
|
||||
site_name: "Site Name"
|
||||
@@ -2723,7 +2766,16 @@ en_US:
|
||||
no_results: "No results"
|
||||
create: "Create"
|
||||
loading: "Loading"
|
||||
flat_percent: "Flat Percent"
|
||||
per_kg: "Per Kg"
|
||||
amount: "Amount"
|
||||
currency: "Currency"
|
||||
first_item: "First Item Cost"
|
||||
additional_item: "Additional Item Cost"
|
||||
max_items: "Max Items"
|
||||
minimal_amount: "Minimal Amount"
|
||||
normal_amount: "Normal Amount"
|
||||
discount_amount: "Discount Amount"
|
||||
email: Email
|
||||
account_updated: "Account updated!"
|
||||
my_account: "My account"
|
||||
@@ -2733,6 +2785,7 @@ en_US:
|
||||
inventory: Inventory
|
||||
zipcode: Zipcode
|
||||
weight: Weight (per lb)
|
||||
error_user_destroy_with_orders: "Users with completed orders may not be deleted"
|
||||
actions:
|
||||
update: "Update"
|
||||
errors:
|
||||
@@ -2871,6 +2924,7 @@ en_US:
|
||||
error_saving_payment: Error saving payment
|
||||
submitting_payment: Submitting payment...
|
||||
products:
|
||||
image_upload_error: "The product image was not recognised. Please upload an image in PNG or JPG format."
|
||||
new:
|
||||
title: 'New Product'
|
||||
unit_name_placeholder: 'eg. bunches'
|
||||
|
||||
@@ -244,6 +244,7 @@ en_ZA:
|
||||
reset_password_token: Reset password
|
||||
expired: has expired, please request a new one
|
||||
back_to_payments_list: "Back to Payments List"
|
||||
maestro_or_solo_cards: "Maestro/Solo cards"
|
||||
actions:
|
||||
create_and_add_another: "Create and Add Another"
|
||||
create: "Create"
|
||||
@@ -452,6 +453,8 @@ en_ZA:
|
||||
encoding_error: "Please check the language setting of your source file and ensure it is saved with UTF-8 encoding"
|
||||
unexpected_error: "Product Import encountered an unexpected error whilst opening the file: %{error_message}"
|
||||
index:
|
||||
notice: "Notice"
|
||||
beta_notice: "This feature is still in beta: you may experience some errors while using it. Please don't hesitate to contact support."
|
||||
select_file: Select a spreadsheet to upload
|
||||
spreadsheet: Spreadsheet
|
||||
choose_import_type: Select import type
|
||||
@@ -1509,7 +1512,7 @@ en_ZA:
|
||||
components_filters_clearfilters: "Clear all filters"
|
||||
groups_title: Groups
|
||||
groups_headline: Groups / regions
|
||||
groups_text: "Every producer is unique. Every business has something different to offer. Our groups are collectives of producers, hubs and distributors who share something in common like location, farmers market or philosophy. This makes your shopping experience easier. So explore our groups and have the curating done for you."
|
||||
groups_text: "OFN groups are collectives of producers, hubs and distributors who share something in common such as location or philosophy. Groups also enable consumers to browse producers that are registered with various industry bodies e.g. organic / biodynamic certification. Specifically in South Africa, groups also enable non-profit organisations to highlight emerging farmers or enterprises that they may be assisting. In this way we can help these producers to enjoy the benefits of the wider OFN network."
|
||||
groups_search: "Search name or keyword"
|
||||
groups_no_groups: "No groups found"
|
||||
groups_about: "About Us"
|
||||
|
||||
@@ -244,6 +244,7 @@ es:
|
||||
reset_password_token: token de restablecimiento de contraseña
|
||||
expired: ha expirado, por favor solicite una nueva
|
||||
back_to_payments_list: "Volver a la lista de pagos"
|
||||
maestro_or_solo_cards: "Tarjetas Maestro/Solo"
|
||||
actions:
|
||||
create_and_add_another: "Crear y agregar otro"
|
||||
create: "Crear"
|
||||
@@ -452,6 +453,8 @@ es:
|
||||
encoding_error: "Verifique la configuración de idioma de su archivo fuente y asegúrese de que esté guardado con la codificación UTF-8"
|
||||
unexpected_error: "La importación de productos encontró un error inesperado al abrir el archivo: %{error_message}"
|
||||
index:
|
||||
notice: "aviso"
|
||||
beta_notice: "Esta funcionalidad aún está en versión beta: puede experimentar algunos errores mientras la usa. Por favor no dude en ponerse en contacto con nosotros."
|
||||
select_file: Selecciona una hoja de cálculo para subir
|
||||
spreadsheet: Hoja de cálculo
|
||||
choose_import_type: Seleccionar tipo de importación
|
||||
|
||||
@@ -88,38 +88,6 @@ Openfoodnetwork::Application.routes.draw do
|
||||
get '/:id/shop', to: 'enterprises#shop', as: 'enterprise_shop'
|
||||
get "/enterprises/:permalink", to: redirect("/") # Legacy enterprise URL
|
||||
|
||||
namespace :api do
|
||||
resources :enterprises do
|
||||
post :update_image, on: :member
|
||||
get :managed, on: :collection
|
||||
get :accessible, on: :collection
|
||||
|
||||
resource :logo, only: [:destroy]
|
||||
resource :promo_image, only: [:destroy]
|
||||
|
||||
member do
|
||||
get :shopfront
|
||||
end
|
||||
end
|
||||
|
||||
resources :order_cycles do
|
||||
get :managed, on: :collection
|
||||
get :accessible, on: :collection
|
||||
end
|
||||
|
||||
resources :orders, only: [:index]
|
||||
|
||||
resource :status do
|
||||
get :job_queue
|
||||
end
|
||||
|
||||
resources :customers, only: [:index, :update]
|
||||
|
||||
resources :enterprise_fees, only: [:destroy]
|
||||
|
||||
post '/product_images/:product_id', to: 'product_images#update_product_image'
|
||||
end
|
||||
|
||||
get 'sitemap.xml', to: 'sitemap#index', defaults: { format: 'xml' }
|
||||
|
||||
# Mount engine routes
|
||||
|
||||
48
config/routes/api.rb
Normal file
48
config/routes/api.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
Openfoodnetwork::Application.routes.draw do
|
||||
namespace :api do
|
||||
resources :products do
|
||||
collection do
|
||||
get :bulk_products
|
||||
get :overridable
|
||||
end
|
||||
delete :soft_delete
|
||||
post :clone
|
||||
|
||||
resources :variants do
|
||||
delete :soft_delete
|
||||
end
|
||||
end
|
||||
|
||||
resources :variants, :only => [:index]
|
||||
|
||||
resources :enterprises do
|
||||
post :update_image, on: :member
|
||||
get :managed, on: :collection
|
||||
get :accessible, on: :collection
|
||||
|
||||
resource :logo, only: [:destroy]
|
||||
resource :promo_image, only: [:destroy]
|
||||
|
||||
member do
|
||||
get :shopfront
|
||||
end
|
||||
end
|
||||
|
||||
resources :order_cycles do
|
||||
get :managed, on: :collection
|
||||
get :accessible, on: :collection
|
||||
end
|
||||
|
||||
resources :orders, only: [:index]
|
||||
|
||||
resource :status do
|
||||
get :job_queue
|
||||
end
|
||||
|
||||
resources :customers, only: [:index, :update]
|
||||
|
||||
resources :enterprise_fees, only: [:destroy]
|
||||
|
||||
post '/product_images/:product_id', to: 'product_images#update_product_image'
|
||||
end
|
||||
end
|
||||
@@ -62,22 +62,27 @@ Spree::Core::Engine.routes.prepend do
|
||||
get :authorise_api, on: :collection
|
||||
end
|
||||
|
||||
resources :products do
|
||||
collection do
|
||||
get :managed
|
||||
get :bulk_products
|
||||
get :overridable
|
||||
end
|
||||
delete :soft_delete
|
||||
post :clone
|
||||
resources :orders do
|
||||
get :managed, on: :collection
|
||||
|
||||
resources :variants do
|
||||
delete :soft_delete
|
||||
resources :shipments, :only => [:create, :update] do
|
||||
member do
|
||||
put :ready
|
||||
put :ship
|
||||
put :add
|
||||
put :remove
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
resources :orders do
|
||||
get :managed, on: :collection
|
||||
resources :taxons, :only => [:index]
|
||||
|
||||
resources :taxonomies do
|
||||
resources :taxons do
|
||||
member do
|
||||
get :jstree
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -105,6 +110,13 @@ Spree::Core::Engine.routes.prepend do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
resources :users do
|
||||
member do
|
||||
put :generate_api_key
|
||||
put :clear_api_key
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
resources :orders do
|
||||
|
||||
@@ -12,7 +12,7 @@ job_type :run_file, "cd :path; :environment_variable=:environment bundle exec sc
|
||||
job_type :enqueue_job, "cd :path; :environment_variable=:environment bundle exec script/enqueue :task :priority :output"
|
||||
|
||||
|
||||
every 1.hour do
|
||||
every 1.day, at: '01:00am' do
|
||||
rake 'ofn:cache:check_products_integrity'
|
||||
end
|
||||
|
||||
|
||||
17
db/migrate/20190701002454_convert_string_fields_to_text.rb
Normal file
17
db/migrate/20190701002454_convert_string_fields_to_text.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
class ConvertStringFieldsToText < ActiveRecord::Migration
|
||||
def up
|
||||
change_column :enterprises, :description, :text
|
||||
change_column :enterprises, :pickup_times, :text
|
||||
change_column :exchanges, :pickup_time, :text
|
||||
change_column :exchanges, :pickup_instructions, :text
|
||||
change_column :exchanges, :receival_instructions, :text
|
||||
end
|
||||
|
||||
def down
|
||||
change_column :enterprises, :description, :string
|
||||
change_column :enterprises, :pickup_times, :string
|
||||
change_column :exchanges, :pickup_time, :string
|
||||
change_column :exchanges, :pickup_instructions, :string
|
||||
change_column :exchanges, :receival_instructions, :string
|
||||
end
|
||||
end
|
||||
12
db/schema.rb
12
db/schema.rb
@@ -11,7 +11,7 @@
|
||||
#
|
||||
# It's strongly recommended to check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(:version => 20190506194625) do
|
||||
ActiveRecord::Schema.define(:version => 20190701002454) do
|
||||
|
||||
create_table "adjustment_metadata", :force => true do |t|
|
||||
t.integer "adjustment_id"
|
||||
@@ -175,7 +175,7 @@ ActiveRecord::Schema.define(:version => 20190506194625) do
|
||||
|
||||
create_table "enterprises", :force => true do |t|
|
||||
t.string "name"
|
||||
t.string "description"
|
||||
t.text "description"
|
||||
t.text "long_description"
|
||||
t.boolean "is_primary_producer"
|
||||
t.string "contact_name"
|
||||
@@ -185,7 +185,7 @@ ActiveRecord::Schema.define(:version => 20190506194625) do
|
||||
t.string "abn"
|
||||
t.string "acn"
|
||||
t.integer "address_id"
|
||||
t.string "pickup_times"
|
||||
t.text "pickup_times"
|
||||
t.string "next_collection_at"
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
@@ -247,12 +247,12 @@ ActiveRecord::Schema.define(:version => 20190506194625) do
|
||||
t.integer "order_cycle_id"
|
||||
t.integer "sender_id"
|
||||
t.integer "receiver_id"
|
||||
t.string "pickup_time"
|
||||
t.string "pickup_instructions"
|
||||
t.text "pickup_time"
|
||||
t.text "pickup_instructions"
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
t.boolean "incoming", :default => false, :null => false
|
||||
t.string "receival_instructions"
|
||||
t.text "receival_instructions"
|
||||
end
|
||||
|
||||
add_index "exchanges", ["order_cycle_id"], :name => "index_exchanges_on_order_cycle_id"
|
||||
|
||||
@@ -54,14 +54,16 @@ module OpenFoodNetwork
|
||||
end
|
||||
|
||||
def all_variants_for_shop
|
||||
# We use the in_stock? method here instead of the in_stock scope because we need to
|
||||
# look up the stock as overridden by VariantOverrides, and the scope method is not affected
|
||||
# by them.
|
||||
scoper = OpenFoodNetwork::ScopeVariantToHub.new(@distributor)
|
||||
Spree::Variant.
|
||||
for_distribution(@order_cycle, @distributor).
|
||||
each { |v| scoper.scope(v) }.
|
||||
select(&:in_stock?)
|
||||
@all_variants_for_shop ||= begin
|
||||
# We use the in_stock? method here instead of the in_stock scope
|
||||
# because we need to look up the stock as overridden by
|
||||
# VariantOverrides, and the scope method is not affected by them.
|
||||
scoper = OpenFoodNetwork::ScopeVariantToHub.new(@distributor)
|
||||
Spree::Variant.
|
||||
for_distribution(@order_cycle, @distributor).
|
||||
each { |v| scoper.scope(v) }.
|
||||
select(&:in_stock?)
|
||||
end
|
||||
end
|
||||
|
||||
def variants_for_shop_by_id
|
||||
|
||||
33
lib/spree/api/controller_setup.rb
Normal file
33
lib/spree/api/controller_setup.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
require 'spree/api/responders'
|
||||
|
||||
module Spree
|
||||
module Api
|
||||
module ControllerSetup
|
||||
def self.included(klass)
|
||||
klass.class_eval do
|
||||
include AbstractController::Rendering
|
||||
include AbstractController::ViewPaths
|
||||
include AbstractController::Callbacks
|
||||
include AbstractController::Helpers
|
||||
|
||||
include ActiveSupport::Rescuable
|
||||
|
||||
include ActionController::Rendering
|
||||
include ActionController::ImplicitRender
|
||||
include ActionController::Rescue
|
||||
include ActionController::MimeResponds
|
||||
include ActionController::Head
|
||||
|
||||
include CanCan::ControllerAdditions
|
||||
include Spree::Core::ControllerHelpers::Auth
|
||||
|
||||
prepend_view_path Rails.root + "app/views"
|
||||
append_view_path File.expand_path("../../../app/views", File.dirname(__FILE__))
|
||||
|
||||
self.responder = Spree::Api::Responders::AppResponder
|
||||
respond_to :json
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
require 'spree/api/testing_support/helpers'
|
||||
|
||||
Spree::Api::TestingSupport::Helpers.class_eval do
|
||||
def current_api_user
|
||||
@current_api_user ||= Spree::LegacyUser.new(email: "spree@example.com", enterprises: [])
|
||||
end
|
||||
end
|
||||
@@ -3,7 +3,6 @@ require 'spec_helper'
|
||||
module Api
|
||||
describe CustomersController, type: :controller do
|
||||
include AuthenticationWorkflow
|
||||
include OpenFoodNetwork::ApiHelper
|
||||
render_views
|
||||
|
||||
let(:user) { create(:user) }
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
require 'spec_helper'
|
||||
require 'spree/api/testing_support/helpers'
|
||||
|
||||
module Api
|
||||
describe OrdersController, type: :controller do
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
require 'spec_helper'
|
||||
require 'spree/api/testing_support/helpers'
|
||||
|
||||
module Api
|
||||
describe ProductImagesController, type: :controller do
|
||||
|
||||
287
spec/controllers/api/products_controller_spec.rb
Normal file
287
spec/controllers/api/products_controller_spec.rb
Normal file
@@ -0,0 +1,287 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe Api::ProductsController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:supplier) { create(:supplier_enterprise) }
|
||||
let(:supplier2) { create(:supplier_enterprise) }
|
||||
let!(:product) { create(:product, supplier: supplier) }
|
||||
let!(:inactive_product) { create(:product, available_on: Time.zone.now.tomorrow, name: "inactive") }
|
||||
let(:product_other_supplier) { create(:product, supplier: supplier2) }
|
||||
let(:product_with_image) { create(:product_with_image, supplier: supplier) }
|
||||
let(:attributes) { ["id", "name", "supplier", "price", "on_hand", "available_on", "permalink_live"] }
|
||||
let(:all_attributes) { ["id", "name", "price", "available_on", "variants"] }
|
||||
let(:variants_attributes) { ["id", "options_text", "unit_value", "unit_description", "unit_to_display", "on_demand", "display_as", "display_name", "name_to_display", "sku", "on_hand", "price"] }
|
||||
|
||||
let(:current_api_user) { build(:user) }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:spree_current_user) { current_api_user }
|
||||
end
|
||||
|
||||
context "as a normal user" do
|
||||
before do
|
||||
allow(current_api_user)
|
||||
.to receive(:has_spree_role?).with("admin").and_return(false)
|
||||
end
|
||||
|
||||
it "gets a single product" do
|
||||
product.master.images.create!(attachment: image("thinking-cat.jpg"))
|
||||
product.variants.create!(unit_value: "1", unit_description: "thing")
|
||||
product.variants.first.images.create!(attachment: image("thinking-cat.jpg"))
|
||||
product.set_property("spree", "rocks")
|
||||
api_get :show, id: product.to_param
|
||||
|
||||
expect(all_attributes.all?{ |attr| json_response.keys.include? attr }).to eq(true)
|
||||
expect(variants_attributes.all?{ |attr| json_response['variants'].first.keys.include? attr }).to eq(true)
|
||||
end
|
||||
|
||||
context "finds a product by permalink first then by id" do
|
||||
let!(:other_product) { create(:product, permalink: "these-are-not-the-droids-you-are-looking-for") }
|
||||
|
||||
before do
|
||||
product.update_attribute(:permalink, "#{other_product.id}-and-1-ways")
|
||||
end
|
||||
|
||||
specify do
|
||||
api_get :show, id: product.to_param
|
||||
|
||||
expect(json_response["permalink_live"]).to match(/and-1-ways/)
|
||||
product.destroy
|
||||
|
||||
api_get :show, id: other_product.id
|
||||
expect(json_response["permalink_live"]).to match(/droids/)
|
||||
end
|
||||
end
|
||||
|
||||
it "cannot see inactive products" do
|
||||
api_get :show, id: inactive_product.to_param
|
||||
|
||||
expect(json_response["error"]).to eq("The resource you were looking for could not be found.")
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
it "returns a 404 error when it cannot find a product" do
|
||||
api_get :show, id: "non-existant"
|
||||
|
||||
expect(json_response["error"]).to eq("The resource you were looking for could not be found.")
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
include_examples "modifying product actions are restricted"
|
||||
end
|
||||
|
||||
context "as an enterprise user" do
|
||||
let(:current_api_user) { supplier_enterprise_user(supplier) }
|
||||
|
||||
it "soft deletes my products" do
|
||||
spree_delete :soft_delete, product_id: product.to_param, format: :json
|
||||
|
||||
expect(response.status).to eq(204)
|
||||
expect { product.reload }.not_to raise_error
|
||||
expect(product.deleted_at).not_to be_nil
|
||||
end
|
||||
|
||||
it "is denied access to soft deleting another enterprises' product" do
|
||||
spree_delete :soft_delete, product_id: product_other_supplier.to_param, format: :json
|
||||
|
||||
assert_unauthorized!
|
||||
expect { product_other_supplier.reload }.not_to raise_error
|
||||
expect(product_other_supplier.deleted_at).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "as an administrator" do
|
||||
before do
|
||||
allow(current_api_user)
|
||||
.to receive(:has_spree_role?).with("admin").and_return(true)
|
||||
end
|
||||
|
||||
it "soft deletes a product" do
|
||||
spree_delete :soft_delete, product_id: product.to_param, format: :json
|
||||
|
||||
expect(response.status).to eq(204)
|
||||
expect { product.reload }.not_to raise_error
|
||||
expect(product.deleted_at).not_to be_nil
|
||||
end
|
||||
|
||||
it "can create a new product" do
|
||||
api_post :create, product: { name: "The Other Product",
|
||||
price: 19.99,
|
||||
shipping_category_id: create(:shipping_category).id,
|
||||
supplier_id: supplier.id,
|
||||
primary_taxon_id: FactoryBot.create(:taxon).id,
|
||||
variant_unit: "items",
|
||||
variant_unit_name: "things",
|
||||
unit_description: "things" }
|
||||
|
||||
expect(all_attributes.all?{ |attr| json_response.keys.include? attr }).to eq(true)
|
||||
expect(response.status).to eq(201)
|
||||
end
|
||||
|
||||
it "cannot create a new product with invalid attributes" do
|
||||
api_post :create, product: {}
|
||||
|
||||
expect(response.status).to eq(422)
|
||||
expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.")
|
||||
errors = json_response["errors"]
|
||||
expect(errors.keys).to match_array(["name", "price", "primary_taxon", "shipping_category_id", "supplier", "variant_unit"])
|
||||
end
|
||||
|
||||
it "can update a product" do
|
||||
api_put :update, id: product.to_param, product: { name: "New and Improved Product!" }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
it "cannot update a product with an invalid attribute" do
|
||||
api_put :update, id: product.to_param, product: { name: "" }
|
||||
|
||||
expect(response.status).to eq(422)
|
||||
expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.")
|
||||
expect(json_response["errors"]["name"]).to eq(["can't be blank"])
|
||||
end
|
||||
|
||||
it "can delete a product" do
|
||||
expect(product.deleted_at).to be_nil
|
||||
api_delete :destroy, id: product.to_param
|
||||
|
||||
expect(response.status).to eq(204)
|
||||
expect(product.reload.deleted_at).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#clone' do
|
||||
context 'as a normal user' do
|
||||
before do
|
||||
allow(current_api_user)
|
||||
.to receive(:has_spree_role?).with("admin").and_return(false)
|
||||
end
|
||||
|
||||
it 'denies access' do
|
||||
spree_post :clone, product_id: product.id, format: :json
|
||||
|
||||
assert_unauthorized!
|
||||
end
|
||||
end
|
||||
|
||||
context 'as an enterprise user' do
|
||||
let(:current_api_user) { supplier_enterprise_user(supplier) }
|
||||
|
||||
it 'responds with a successful response' do
|
||||
spree_post :clone, product_id: product.id, format: :json
|
||||
|
||||
expect(response.status).to eq(201)
|
||||
end
|
||||
|
||||
it 'clones the product' do
|
||||
spree_post :clone, product_id: product.id, format: :json
|
||||
|
||||
expect(json_response['name']).to eq("COPY OF #{product.name}")
|
||||
end
|
||||
|
||||
it 'clones a product with image' do
|
||||
spree_post :clone, product_id: product_with_image.id, format: :json
|
||||
|
||||
expect(response.status).to eq(201)
|
||||
expect(json_response['name']).to eq("COPY OF #{product_with_image.name}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'as an administrator' do
|
||||
before do
|
||||
allow(current_api_user)
|
||||
.to receive(:has_spree_role?).with("admin").and_return(true)
|
||||
end
|
||||
|
||||
it 'responds with a successful response' do
|
||||
spree_post :clone, product_id: product.id, format: :json
|
||||
|
||||
expect(response.status).to eq(201)
|
||||
end
|
||||
|
||||
it 'clones the product' do
|
||||
spree_post :clone, product_id: product.id, format: :json
|
||||
|
||||
expect(json_response['name']).to eq("COPY OF #{product.name}")
|
||||
end
|
||||
|
||||
it 'clones a product with image' do
|
||||
spree_post :clone, product_id: product_with_image.id, format: :json
|
||||
|
||||
expect(response.status).to eq(201)
|
||||
expect(json_response['name']).to eq("COPY OF #{product_with_image.name}")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#bulk_products' do
|
||||
context "as an enterprise user" do
|
||||
let!(:taxon) { create(:taxon) }
|
||||
let!(:product2) { create(:product, supplier: supplier, primary_taxon: taxon) }
|
||||
let!(:product3) { create(:product, supplier: supplier2, primary_taxon: taxon) }
|
||||
let!(:product4) { create(:product, supplier: supplier2) }
|
||||
let(:current_api_user) { supplier_enterprise_user(supplier) }
|
||||
|
||||
before { current_api_user.enterprise_roles.create(enterprise: supplier2) }
|
||||
|
||||
it "returns a list of products" do
|
||||
api_get :bulk_products, { page: 1, per_page: 15 }, format: :json
|
||||
expect(returned_product_ids).to eq [product4.id, product3.id, product2.id, inactive_product.id, product.id]
|
||||
end
|
||||
|
||||
it "returns pagination data" do
|
||||
api_get :bulk_products, { page: 1, per_page: 15 }, format: :json
|
||||
expect(json_response['pagination']).to eq "results" => 5, "pages" => 1, "page" => 1, "per_page" => 15
|
||||
end
|
||||
|
||||
it "uses defaults when page and per_page are not supplied" do
|
||||
api_get :bulk_products, format: :json
|
||||
expect(json_response['pagination']).to eq "results" => 5, "pages" => 1, "page" => 1, "per_page" => 15
|
||||
end
|
||||
|
||||
it "returns paginated products by page" do
|
||||
api_get :bulk_products, { page: 1, per_page: 2 }, format: :json
|
||||
expect(returned_product_ids).to eq [product4.id, product3.id]
|
||||
|
||||
api_get :bulk_products, { page: 2, per_page: 2 }, format: :json
|
||||
expect(returned_product_ids).to eq [product2.id, inactive_product.id]
|
||||
end
|
||||
|
||||
it "filters results by supplier" do
|
||||
api_get :bulk_products, { page: 1, per_page: 15, q: {supplier_id_eq: supplier.id} }, format: :json
|
||||
expect(returned_product_ids).to eq [product2.id, inactive_product.id, product.id]
|
||||
end
|
||||
|
||||
it "filters results by product category" do
|
||||
api_get :bulk_products, { page: 1, per_page: 15, q: {primary_taxon_id_eq: taxon.id} }, format: :json
|
||||
expect(returned_product_ids).to eq [product3.id, product2.id]
|
||||
end
|
||||
|
||||
it "filters results by import_date" do
|
||||
product.variants.first.import_date = 1.day.ago
|
||||
product2.variants.first.import_date = 2.days.ago
|
||||
product3.variants.first.import_date = 1.day.ago
|
||||
|
||||
product.save
|
||||
product2.save
|
||||
product3.save
|
||||
|
||||
api_get :bulk_products, { page: 1, per_page: 15, import_date: 1.day.ago.to_date.to_s }, format: :json
|
||||
expect(returned_product_ids).to eq [product3.id, product.id]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def supplier_enterprise_user(enterprise)
|
||||
user = create(:user)
|
||||
user.enterprise_roles.create(enterprise: enterprise)
|
||||
user
|
||||
end
|
||||
|
||||
def returned_product_ids
|
||||
json_response['products'].map{ |obj| obj['id'] }
|
||||
end
|
||||
end
|
||||
197
spec/controllers/api/variants_controller_spec.rb
Normal file
197
spec/controllers/api/variants_controller_spec.rb
Normal file
@@ -0,0 +1,197 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe Api::VariantsController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:supplier) { FactoryBot.create(:supplier_enterprise) }
|
||||
let!(:variant1) { FactoryBot.create(:variant) }
|
||||
let!(:variant2) { FactoryBot.create(:variant) }
|
||||
let!(:variant3) { FactoryBot.create(:variant) }
|
||||
let(:attributes) { [:id, :options_text, :price, :on_hand, :unit_value, :unit_description, :on_demand, :display_as, :display_name] }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:spree_current_user) { current_api_user }
|
||||
end
|
||||
|
||||
context "as a normal user" do
|
||||
sign_in_as_user!
|
||||
|
||||
let!(:product) { create(:product) }
|
||||
let!(:variant) do
|
||||
variant = product.master
|
||||
variant.option_values << create(:option_value)
|
||||
variant
|
||||
end
|
||||
|
||||
it "retrieves a list of variants with appropriate attributes" do
|
||||
spree_get :index, format: :json
|
||||
|
||||
keys = json_response.first.keys.map(&:to_sym)
|
||||
expect(attributes.all?{ |attr| keys.include? attr }).to eq(true)
|
||||
end
|
||||
|
||||
it "is denied access when trying to delete a variant" do
|
||||
product = create(:product)
|
||||
variant = product.master
|
||||
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
|
||||
|
||||
assert_unauthorized!
|
||||
expect { variant.reload }.not_to raise_error
|
||||
expect(variant.deleted_at).to be_nil
|
||||
end
|
||||
|
||||
it 'can query the results through a parameter' do
|
||||
expected_result = create(:variant, sku: 'FOOBAR')
|
||||
api_get :index, q: { sku_cont: 'FOO' }
|
||||
|
||||
expect(json_response.size).to eq(1)
|
||||
expect(json_response.first['sku']).to eq expected_result.sku
|
||||
end
|
||||
|
||||
# Regression test for spree#2141
|
||||
context "a deleted variant" do
|
||||
before do
|
||||
variant.update_column(:deleted_at, Time.zone.now)
|
||||
end
|
||||
|
||||
it "is not returned in the results" do
|
||||
api_get :index
|
||||
expect(json_response.count).to eq(10) # there are 11 variants
|
||||
end
|
||||
|
||||
it "is not returned even when show_deleted is passed" do
|
||||
api_get :index, show_deleted: true
|
||||
expect(json_response.count).to eq(10) # there are 11 variants
|
||||
end
|
||||
end
|
||||
|
||||
it "can see a single variant" do
|
||||
api_get :show, id: variant.to_param
|
||||
|
||||
keys = json_response.keys.map(&:to_sym)
|
||||
expect((attributes).all?{ |attr| keys.include? attr }).to eq(true)
|
||||
end
|
||||
|
||||
it "cannot create a new variant if not an admin" do
|
||||
api_post :create, variant: { sku: "12345" }
|
||||
|
||||
assert_unauthorized!
|
||||
end
|
||||
|
||||
it "cannot update a variant" do
|
||||
api_put :update, id: variant.to_param, variant: { sku: "12345" }
|
||||
|
||||
assert_unauthorized!
|
||||
end
|
||||
|
||||
it "cannot delete a variant" do
|
||||
api_delete :destroy, id: variant.to_param
|
||||
|
||||
assert_unauthorized!
|
||||
expect { variant.reload }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context "as an enterprise user" do
|
||||
sign_in_as_enterprise_user! [:supplier]
|
||||
let(:supplier_other) { create(:supplier_enterprise) }
|
||||
let(:product) { create(:product, supplier: supplier) }
|
||||
let(:variant) { product.master }
|
||||
let(:product_other) { create(:product, supplier: supplier_other) }
|
||||
let(:variant_other) { product_other.master }
|
||||
|
||||
it "soft deletes a variant" do
|
||||
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
|
||||
|
||||
expect(response.status).to eq(204)
|
||||
expect { variant.reload }.not_to raise_error
|
||||
expect(variant.deleted_at).to be_present
|
||||
end
|
||||
|
||||
it "is denied access to soft deleting another enterprises' variant" do
|
||||
spree_delete :soft_delete, variant_id: variant_other.to_param, product_id: product_other.to_param, format: :json
|
||||
|
||||
assert_unauthorized!
|
||||
expect { variant.reload }.not_to raise_error
|
||||
expect(variant.deleted_at).to be_nil
|
||||
end
|
||||
|
||||
context 'when the variant is not the master' do
|
||||
before { variant.update_attribute(:is_master, false) }
|
||||
|
||||
it 'refreshes the cache' do
|
||||
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).with(variant)
|
||||
spree_delete :soft_delete, variant_id: variant.id, product_id: variant.product.permalink, format: :json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "as an administrator" do
|
||||
sign_in_as_admin!
|
||||
|
||||
let(:product) { create(:product) }
|
||||
let(:variant) { product.master }
|
||||
let(:resource_scoping) { { product_id: variant.product.to_param } }
|
||||
|
||||
it "soft deletes a variant" do
|
||||
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
|
||||
|
||||
expect(response.status).to eq(204)
|
||||
expect { variant.reload }.not_to raise_error
|
||||
expect(variant.deleted_at).not_to be_nil
|
||||
end
|
||||
|
||||
it "doesn't delete the only variant of the product" do
|
||||
product = create(:product)
|
||||
variant = product.variants.first
|
||||
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
|
||||
|
||||
expect(variant.reload).to_not be_deleted
|
||||
expect(assigns(:variant).errors[:product]).to include "must have at least one variant"
|
||||
end
|
||||
|
||||
context 'when the variant is not the master' do
|
||||
before { variant.update_attribute(:is_master, false) }
|
||||
|
||||
it 'refreshes the cache' do
|
||||
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).with(variant)
|
||||
spree_delete :soft_delete, variant_id: variant.id, product_id: variant.product.permalink, format: :json
|
||||
end
|
||||
end
|
||||
|
||||
context "deleted variants" do
|
||||
before do
|
||||
variant.update_column(:deleted_at, Time.zone.now)
|
||||
end
|
||||
|
||||
it "are visible by admin" do
|
||||
api_get :index, show_deleted: 1
|
||||
|
||||
expect(json_response.count).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
it "can create a new variant" do
|
||||
original_number_of_variants = variant.product.variants.count
|
||||
api_post :create, variant: { sku: "12345", unit_value: "weight", unit_description: "L" }
|
||||
|
||||
expect(attributes.all?{ |attr| json_response.include? attr.to_s }).to eq(true)
|
||||
expect(response.status).to eq(201)
|
||||
expect(json_response["sku"]).to eq("12345")
|
||||
expect(variant.product.variants.count).to eq(original_number_of_variants + 1)
|
||||
end
|
||||
|
||||
it "can update a variant" do
|
||||
api_put :update, id: variant.to_param, variant: { sku: "12345" }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
it "can delete a variant" do
|
||||
api_delete :destroy, id: variant.to_param
|
||||
|
||||
expect(response.status).to eq(204)
|
||||
expect { Spree::Variant.find(variant.id) }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -150,15 +150,16 @@ describe Spree::Admin::ProductsController, type: :controller do
|
||||
|
||||
describe "when user uploads an image in an unsupported format" do
|
||||
it "does not throw an exception" do
|
||||
product_image = ActionDispatch::Http::UploadedFile.new({
|
||||
:filename => 'unsupported_image_format.exr',
|
||||
:content_type => 'application/octet-stream',
|
||||
:tempfile => Tempfile.new('unsupported_image_format.exr')
|
||||
})
|
||||
product_image = ActionDispatch::Http::UploadedFile.new(
|
||||
filename: 'unsupported_image_format.exr',
|
||||
content_type: 'application/octet-stream',
|
||||
tempfile: Tempfile.new('unsupported_image_format.exr')
|
||||
)
|
||||
product_attrs_with_image = product_attrs.merge(
|
||||
images_attributes: {
|
||||
'0' => { attachment: product_image }
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
expect do
|
||||
spree_put :create, product: product_attrs_with_image
|
||||
|
||||
64
spec/controllers/spree/api/base_controller_spec.rb
Normal file
64
spec/controllers/spree/api/base_controller_spec.rb
Normal file
@@ -0,0 +1,64 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe Spree::Api::BaseController do
|
||||
render_views
|
||||
controller(Spree::Api::BaseController) do
|
||||
def index
|
||||
render text: { "products" => [] }.to_json
|
||||
end
|
||||
|
||||
def spree_current_user; end
|
||||
end
|
||||
|
||||
context "signed in as a user using an authentication extension" do
|
||||
before do
|
||||
allow(controller).to receive_messages try_spree_current_user:
|
||||
double(email: "spree@example.com")
|
||||
end
|
||||
|
||||
it "can make a request" do
|
||||
api_get :index
|
||||
expect(json_response).to eq( "products" => [] )
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
|
||||
context "cannot make a request to the API" do
|
||||
it "without an API key" do
|
||||
api_get :index
|
||||
expect(json_response).to eq( "error" => "You must specify an API key." )
|
||||
expect(response.status).to eq(401)
|
||||
end
|
||||
|
||||
it "with an invalid API key" do
|
||||
request.env["X-Spree-Token"] = "fake_key"
|
||||
get :index, {}
|
||||
expect(json_response).to eq( "error" => "Invalid API key (fake_key) specified." )
|
||||
expect(response.status).to eq(401)
|
||||
end
|
||||
|
||||
it "using an invalid token param" do
|
||||
get :index, token: "fake_key"
|
||||
expect(json_response).to eq( "error" => "Invalid API key (fake_key) specified." )
|
||||
end
|
||||
end
|
||||
|
||||
it 'handles exceptions' do
|
||||
expect(subject).to receive(:authenticate_user).and_return(true)
|
||||
expect(subject).to receive(:index).and_raise(Exception.new("no joy"))
|
||||
get :index, token: "fake_key"
|
||||
expect(json_response).to eq( "exception" => "no joy" )
|
||||
end
|
||||
|
||||
it "maps symantec keys to nested_attributes keys" do
|
||||
klass = double(nested_attributes_options: { line_items: {},
|
||||
bill_address: {} })
|
||||
attributes = { 'line_items' => { id: 1 },
|
||||
'bill_address' => { id: 2 },
|
||||
'name' => 'test order' }
|
||||
|
||||
mapped = subject.map_nested_attributes_keys(klass, attributes)
|
||||
expect(mapped.key?('line_items_attributes')).to be_truthy
|
||||
expect(mapped.key?('name')).to be_truthy
|
||||
end
|
||||
end
|
||||
@@ -1,35 +0,0 @@
|
||||
require 'spec_helper'
|
||||
|
||||
module Spree
|
||||
describe Spree::Api::LineItemsController, type: :controller do
|
||||
render_views
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:spree_current_user) { current_api_user }
|
||||
end
|
||||
|
||||
# test that when a line item is updated, an order's fees are updated too
|
||||
context "as an admin user" do
|
||||
sign_in_as_admin!
|
||||
|
||||
let(:order) { FactoryBot.create(:order, state: 'complete', completed_at: Time.zone.now) }
|
||||
let(:line_item) { FactoryBot.create(:line_item_with_shipment, order: order, final_weight_volume: 500) }
|
||||
|
||||
context "as a line item is updated" do
|
||||
before { allow(controller).to receive(:order) { order } }
|
||||
|
||||
it "update distribution charge on the order" do
|
||||
line_item_params = {
|
||||
order_id: order.number,
|
||||
id: line_item.id,
|
||||
line_item: { id: line_item.id, final_weight_volume: 520 },
|
||||
format: :json
|
||||
}
|
||||
|
||||
expect(order).to receive(:update_distribution_charge!)
|
||||
spree_post :update, line_item_params
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,180 +0,0 @@
|
||||
require 'spec_helper'
|
||||
|
||||
module Spree
|
||||
describe Spree::Api::ProductsController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:supplier) { create(:supplier_enterprise) }
|
||||
let(:supplier2) { create(:supplier_enterprise) }
|
||||
let!(:product1) { create(:product, supplier: supplier) }
|
||||
let(:product_other_supplier) { create(:product, supplier: supplier2) }
|
||||
let(:product_with_image) { create(:product_with_image, supplier: supplier) }
|
||||
let(:attributes) { [:id, :name, :supplier, :price, :on_hand, :available_on, :permalink_live] }
|
||||
|
||||
let(:current_api_user) { build(:user) }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:spree_current_user) { current_api_user }
|
||||
end
|
||||
|
||||
context "as a normal user" do
|
||||
before do
|
||||
allow(current_api_user)
|
||||
.to receive(:has_spree_role?).with("admin").and_return(false)
|
||||
end
|
||||
|
||||
it "should deny me access to managed products" do
|
||||
spree_get :managed, template: 'bulk_index', format: :json
|
||||
assert_unauthorized!
|
||||
end
|
||||
end
|
||||
|
||||
context "as an enterprise user" do
|
||||
let(:current_api_user) do
|
||||
user = create(:user)
|
||||
user.enterprise_roles.create(enterprise: supplier)
|
||||
user
|
||||
end
|
||||
|
||||
it "retrieves a list of managed products" do
|
||||
spree_get :managed, template: 'bulk_index', format: :json
|
||||
keys = json_response.first.keys.map(&:to_sym)
|
||||
expect(attributes.all?{ |attr| keys.include? attr }).to eq(true)
|
||||
end
|
||||
|
||||
it "soft deletes my products" do
|
||||
spree_delete :soft_delete, product_id: product1.to_param, format: :json
|
||||
expect(response.status).to eq(204)
|
||||
expect { product1.reload }.not_to raise_error
|
||||
expect(product1.deleted_at).not_to be_nil
|
||||
end
|
||||
|
||||
it "is denied access to soft deleting another enterprises' product" do
|
||||
spree_delete :soft_delete, product_id: product_other_supplier.to_param, format: :json
|
||||
assert_unauthorized!
|
||||
expect { product_other_supplier.reload }.not_to raise_error
|
||||
expect(product_other_supplier.deleted_at).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "as an administrator" do
|
||||
before do
|
||||
allow(current_api_user)
|
||||
.to receive(:has_spree_role?).with("admin").and_return(true)
|
||||
end
|
||||
|
||||
it "retrieves a list of managed products" do
|
||||
spree_get :managed, template: 'bulk_index', format: :json
|
||||
keys = json_response.first.keys.map(&:to_sym)
|
||||
expect(attributes.all?{ |attr| keys.include? attr }).to eq(true)
|
||||
end
|
||||
|
||||
it "retrieves a list of products with appropriate attributes" do
|
||||
spree_get :index, template: 'bulk_index', format: :json
|
||||
keys = json_response.first.keys.map(&:to_sym)
|
||||
expect(attributes.all?{ |attr| keys.include? attr }).to eq(true)
|
||||
end
|
||||
|
||||
it "sorts products in ascending id order" do
|
||||
FactoryBot.create(:product, supplier: supplier)
|
||||
FactoryBot.create(:product, supplier: supplier)
|
||||
|
||||
spree_get :index, template: 'bulk_index', format: :json
|
||||
|
||||
ids = json_response.map{ |product| product['id'] }
|
||||
expect(ids[0]).to be < ids[1]
|
||||
expect(ids[1]).to be < ids[2]
|
||||
end
|
||||
|
||||
it "formats available_on to 'yyyy-mm-dd hh:mm'" do
|
||||
spree_get :index, template: 'bulk_index', format: :json
|
||||
expect(json_response.map{ |product| product['available_on'] }.all?{ |a| a.match("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$") }).to eq(true)
|
||||
end
|
||||
|
||||
it "returns permalink as permalink_live" do
|
||||
spree_get :index, template: 'bulk_index', format: :json
|
||||
expect(json_response.detect{ |product| product['id'] == product1.id }['permalink_live']).to eq(product1.permalink)
|
||||
end
|
||||
|
||||
it "should allow available_on to be nil" do
|
||||
spree_get :index, template: 'bulk_index', format: :json
|
||||
expect(json_response.size).to eq(1)
|
||||
|
||||
product5 = FactoryBot.create(:product)
|
||||
product5.available_on = nil
|
||||
product5.save!
|
||||
|
||||
spree_get :index, template: 'bulk_index', format: :json
|
||||
expect(json_response.size).to eq(2)
|
||||
end
|
||||
|
||||
it "soft deletes a product" do
|
||||
spree_delete :soft_delete, product_id: product1.to_param, format: :json
|
||||
expect(response.status).to eq(204)
|
||||
expect { product1.reload }.not_to raise_error
|
||||
expect(product1.deleted_at).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#clone' do
|
||||
context 'as a normal user' do
|
||||
before do
|
||||
allow(current_api_user)
|
||||
.to receive(:has_spree_role?).with("admin").and_return(false)
|
||||
end
|
||||
|
||||
it 'denies access' do
|
||||
spree_post :clone, product_id: product1.id, format: :json
|
||||
assert_unauthorized!
|
||||
end
|
||||
end
|
||||
|
||||
context 'as an enterprise user' do
|
||||
let(:current_api_user) do
|
||||
user = create(:user)
|
||||
user.enterprise_roles.create(enterprise: supplier)
|
||||
user
|
||||
end
|
||||
|
||||
it 'responds with a successful response' do
|
||||
spree_post :clone, product_id: product1.id, format: :json
|
||||
expect(response.status).to eq(201)
|
||||
end
|
||||
|
||||
it 'clones the product' do
|
||||
spree_post :clone, product_id: product1.id, format: :json
|
||||
expect(json_response['name']).to eq("COPY OF #{product1.name}")
|
||||
end
|
||||
|
||||
it 'clones a product with image' do
|
||||
spree_post :clone, product_id: product_with_image.id, format: :json
|
||||
expect(response.status).to eq(201)
|
||||
expect(json_response['name']).to eq("COPY OF #{product_with_image.name}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'as an administrator' do
|
||||
before do
|
||||
allow(current_api_user)
|
||||
.to receive(:has_spree_role?).with("admin").and_return(true)
|
||||
end
|
||||
|
||||
it 'responds with a successful response' do
|
||||
spree_post :clone, product_id: product1.id, format: :json
|
||||
expect(response.status).to eq(201)
|
||||
end
|
||||
|
||||
it 'clones the product' do
|
||||
spree_post :clone, product_id: product1.id, format: :json
|
||||
expect(json_response['name']).to eq("COPY OF #{product1.name}")
|
||||
end
|
||||
|
||||
it 'clones a product with image' do
|
||||
spree_post :clone, product_id: product_with_image.id, format: :json
|
||||
expect(response.status).to eq(201)
|
||||
expect(json_response['name']).to eq("COPY OF #{product_with_image.name}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,12 +5,27 @@ describe Spree::Api::ShipmentsController, type: :controller do
|
||||
|
||||
let!(:shipment) { create(:shipment) }
|
||||
let!(:attributes) { [:id, :tracking, :number, :cost, :shipped_at, :stock_location_name, :order_id, :shipping_rates, :shipping_method, :inventory_units] }
|
||||
let!(:resource_scoping) { { order_id: shipment.order.to_param, id: shipment.to_param } }
|
||||
let(:current_api_user) { build(:user) }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:spree_current_user) { current_api_user }
|
||||
end
|
||||
|
||||
context "as a non-admin" do
|
||||
it "cannot make a shipment ready" do
|
||||
api_put :ready
|
||||
assert_unauthorized!
|
||||
end
|
||||
|
||||
it "cannot make a shipment shipped" do
|
||||
api_put :ship
|
||||
assert_unauthorized!
|
||||
end
|
||||
end
|
||||
|
||||
context "as an admin" do
|
||||
let(:current_api_user) { build(:admin_user) }
|
||||
let!(:order) { shipment.order }
|
||||
let(:order_ship_address) { create(:address) }
|
||||
let!(:stock_location) { create(:stock_location_with_items) }
|
||||
@@ -30,8 +45,6 @@ describe Spree::Api::ShipmentsController, type: :controller do
|
||||
shipment.shipping_method.distributors << variant.product.supplier
|
||||
end
|
||||
|
||||
sign_in_as_admin!
|
||||
|
||||
context '#create' do
|
||||
it 'creates a shipment if order does not have a shipment' do
|
||||
order.shipment.destroy
|
||||
@@ -77,6 +90,62 @@ describe Spree::Api::ShipmentsController, type: :controller do
|
||||
end
|
||||
end
|
||||
|
||||
it "can make a shipment ready" do
|
||||
allow_any_instance_of(Spree::Order).to receive_messages(paid?: true, complete?: true)
|
||||
api_put :ready
|
||||
|
||||
expect(attributes.all?{ |attr| json_response.key? attr.to_s }).to be_truthy
|
||||
expect(json_response["state"]).to eq("ready")
|
||||
expect(shipment.reload.state).to eq("ready")
|
||||
end
|
||||
|
||||
it "cannot make a shipment ready if the order is unpaid" do
|
||||
allow_any_instance_of(Spree::Order).to receive_messages(paid?: false)
|
||||
api_put :ready
|
||||
|
||||
expect(json_response["error"]).to eq("Cannot ready shipment.")
|
||||
expect(response.status).to eq(422)
|
||||
end
|
||||
|
||||
context 'for completed shipments' do
|
||||
let(:order) { create :completed_order_with_totals }
|
||||
let!(:resource_scoping) { { order_id: order.to_param, id: order.shipments.first.to_param } }
|
||||
|
||||
it 'adds a variant to a shipment' do
|
||||
api_put :add, variant_id: variant.to_param, quantity: 2
|
||||
expect(response.status).to eq(200)
|
||||
expect(json_response['inventory_units'].select { |h| h['variant_id'] == variant.id }.size).to eq(2)
|
||||
end
|
||||
|
||||
it 'removes a variant from a shipment' do
|
||||
order.contents.add(variant, 2)
|
||||
|
||||
api_put :remove, variant_id: variant.to_param, quantity: 1
|
||||
expect(response.status).to eq(200)
|
||||
expect(json_response['inventory_units'].select { |h| h['variant_id'] == variant.id }.size).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "can transition a shipment from ready to ship" do
|
||||
before do
|
||||
allow_any_instance_of(Spree::Order).to receive_messages(paid?: true, complete?: true)
|
||||
# For the shipment notification email
|
||||
Spree::Config[:mails_from] = "spree@example.com"
|
||||
|
||||
shipment.update!(shipment.order)
|
||||
expect(shipment.state).to eq("ready")
|
||||
allow_any_instance_of(Spree::ShippingRate).to receive_messages(cost: 5)
|
||||
end
|
||||
|
||||
it "can transition a shipment from ready to ship" do
|
||||
shipment.reload
|
||||
api_put :ship, order_id: shipment.order.to_param, id: shipment.to_param, shipment: { tracking: "123123" }
|
||||
|
||||
expect(attributes.all?{ |attr| json_response.key? attr.to_s }).to be_truthy
|
||||
expect(json_response["state"]).to eq("shipped")
|
||||
end
|
||||
end
|
||||
|
||||
context 'for a completed order with shipment' do
|
||||
let(:order) { create :completed_order_with_totals }
|
||||
|
||||
|
||||
135
spec/controllers/spree/api/taxons_controller_spec.rb
Normal file
135
spec/controllers/spree/api/taxons_controller_spec.rb
Normal file
@@ -0,0 +1,135 @@
|
||||
require 'spec_helper'
|
||||
|
||||
module Spree
|
||||
describe Api::TaxonsController do
|
||||
render_views
|
||||
|
||||
let(:taxonomy) { create(:taxonomy) }
|
||||
let(:taxon) { create(:taxon, name: "Ruby", taxonomy: taxonomy) }
|
||||
let(:taxon2) { create(:taxon, name: "Rails", taxonomy: taxonomy) }
|
||||
let(:attributes) {
|
||||
["id", "name", "pretty_name", "permalink", "position", "parent_id", "taxonomy_id"]
|
||||
}
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:spree_current_user) { current_api_user }
|
||||
|
||||
taxon2.children << create(:taxon, name: "3.2.2", taxonomy: taxonomy)
|
||||
taxon.children << taxon2
|
||||
taxonomy.root.children << taxon
|
||||
end
|
||||
|
||||
context "as a normal user" do
|
||||
let(:current_api_user) { build(:user) }
|
||||
|
||||
it "gets all taxons for a taxonomy" do
|
||||
api_get :index, taxonomy_id: taxonomy.id
|
||||
|
||||
expect(json_response.first['name']).to eq taxon.name
|
||||
children = json_response.first['taxons']
|
||||
expect(children.count).to eq 1
|
||||
expect(children.first['name']).to eq taxon2.name
|
||||
expect(children.first['taxons'].count).to eq 1
|
||||
end
|
||||
|
||||
it "gets all taxons" do
|
||||
api_get :index
|
||||
|
||||
expect(json_response.first['name']).to eq taxonomy.root.name
|
||||
children = json_response.first['taxons']
|
||||
expect(children.count).to eq 1
|
||||
expect(children.first['name']).to eq taxon.name
|
||||
expect(children.first['taxons'].count).to eq 1
|
||||
end
|
||||
|
||||
it "can search for a single taxon" do
|
||||
api_get :index, q: { name_cont: "Ruby" }
|
||||
|
||||
expect(json_response.count).to eq(1)
|
||||
expect(json_response.first['name']).to eq "Ruby"
|
||||
end
|
||||
|
||||
it "gets a single taxon" do
|
||||
api_get :show, id: taxon.id, taxonomy_id: taxonomy.id
|
||||
|
||||
expect(json_response['name']).to eq taxon.name
|
||||
expect(json_response['taxons'].count).to eq 1
|
||||
end
|
||||
|
||||
it "gets all taxons in JSTree form" do
|
||||
api_get :jstree, taxonomy_id: taxonomy.id, id: taxon.id
|
||||
|
||||
response = json_response.first
|
||||
response["data"].should eq(taxon2.name)
|
||||
response["attr"].should eq("name" => taxon2.name, "id" => taxon2.id)
|
||||
response["state"].should eq("closed")
|
||||
end
|
||||
|
||||
it "can learn how to create a new taxon" do
|
||||
api_get :new, taxonomy_id: taxonomy.id
|
||||
expect(json_response["attributes"]).to eq(attributes.map(&:to_s))
|
||||
required_attributes = json_response["required_attributes"]
|
||||
expect(required_attributes).to include("name")
|
||||
end
|
||||
|
||||
it "cannot create a new taxon if not an admin" do
|
||||
api_post :create, taxonomy_id: taxonomy.id, taxon: { name: "Location" }
|
||||
assert_unauthorized!
|
||||
end
|
||||
|
||||
it "cannot update a taxon" do
|
||||
api_put :update, taxonomy_id: taxonomy.id,
|
||||
id: taxon.id,
|
||||
taxon: { name: "I hacked your store!" }
|
||||
assert_unauthorized!
|
||||
end
|
||||
|
||||
it "cannot delete a taxon" do
|
||||
api_delete :destroy, taxonomy_id: taxonomy.id, id: taxon.id
|
||||
assert_unauthorized!
|
||||
end
|
||||
end
|
||||
|
||||
context "as an admin" do
|
||||
let(:current_api_user) { build(:admin_user) }
|
||||
|
||||
it "can create" do
|
||||
api_post :create, taxonomy_id: taxonomy.id, taxon: { name: "Colors" }
|
||||
|
||||
expect(attributes.all? { |a| json_response.include? a }).to be true
|
||||
expect(response.status).to eq(201)
|
||||
|
||||
expect(taxonomy.reload.root.children.count).to eq 2
|
||||
|
||||
expect(Spree::Taxon.last.parent_id).to eq taxonomy.root.id
|
||||
expect(Spree::Taxon.last.taxonomy_id).to eq taxonomy.id
|
||||
end
|
||||
|
||||
it "cannot create a new taxon with invalid attributes" do
|
||||
api_post :create, taxonomy_id: taxonomy.id, taxon: {}
|
||||
expect(response.status).to eq(422)
|
||||
expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.")
|
||||
errors = json_response["errors"]
|
||||
|
||||
expect(taxonomy.reload.root.children.count).to eq 1
|
||||
end
|
||||
|
||||
it "cannot create a new taxon with invalid taxonomy_id" do
|
||||
api_post :create, taxonomy_id: 1000, taxon: { name: "Colors" }
|
||||
expect(response.status).to eq(422)
|
||||
expect(json_response["error"]).to eq("Invalid resource. Please fix errors and try again.")
|
||||
|
||||
errors = json_response["errors"]
|
||||
expect(errors["taxonomy_id"]).not_to be_nil
|
||||
expect(errors["taxonomy_id"].first).to eq "Invalid taxonomy id."
|
||||
|
||||
expect(taxonomy.reload.root.children.count).to eq 1
|
||||
end
|
||||
|
||||
it "can destroy" do
|
||||
api_delete :destroy, taxonomy_id: taxonomy.id, id: taxon2.id
|
||||
expect(response.status).to eq(204)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,102 +0,0 @@
|
||||
require 'spec_helper'
|
||||
|
||||
module Spree
|
||||
describe Spree::Api::VariantsController, type: :controller do
|
||||
render_views
|
||||
|
||||
let(:supplier) { FactoryBot.create(:supplier_enterprise) }
|
||||
let!(:variant1) { FactoryBot.create(:variant) }
|
||||
let!(:variant2) { FactoryBot.create(:variant) }
|
||||
let!(:variant3) { FactoryBot.create(:variant) }
|
||||
let(:attributes) { [:id, :options_text, :price, :on_hand, :unit_value, :unit_description, :on_demand, :display_as, :display_name] }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:spree_current_user) { current_api_user }
|
||||
end
|
||||
|
||||
context "as a normal user" do
|
||||
sign_in_as_user!
|
||||
|
||||
it "retrieves a list of variants with appropriate attributes" do
|
||||
spree_get :index, template: 'bulk_index', format: :json
|
||||
keys = json_response.first.keys.map(&:to_sym)
|
||||
expect(attributes.all?{ |attr| keys.include? attr }).to eq(true)
|
||||
end
|
||||
|
||||
it "is denied access when trying to delete a variant" do
|
||||
product = create(:product)
|
||||
variant = product.master
|
||||
|
||||
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
|
||||
assert_unauthorized!
|
||||
expect { variant.reload }.not_to raise_error
|
||||
expect(variant.deleted_at).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "as an enterprise user" do
|
||||
sign_in_as_enterprise_user! [:supplier]
|
||||
let(:supplier_other) { create(:supplier_enterprise) }
|
||||
let(:product) { create(:product, supplier: supplier) }
|
||||
let(:variant) { product.master }
|
||||
let(:product_other) { create(:product, supplier: supplier_other) }
|
||||
let(:variant_other) { product_other.master }
|
||||
|
||||
it "soft deletes a variant" do
|
||||
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
|
||||
expect(response.status).to eq(204)
|
||||
expect { variant.reload }.not_to raise_error
|
||||
expect(variant.deleted_at).to be_present
|
||||
end
|
||||
|
||||
it "is denied access to soft deleting another enterprises' variant" do
|
||||
spree_delete :soft_delete, variant_id: variant_other.to_param, product_id: product_other.to_param, format: :json
|
||||
assert_unauthorized!
|
||||
expect { variant.reload }.not_to raise_error
|
||||
expect(variant.deleted_at).to be_nil
|
||||
end
|
||||
|
||||
context 'when the variant is not the master' do
|
||||
before { variant.update_attribute(:is_master, false) }
|
||||
|
||||
it 'refreshes the cache' do
|
||||
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).with(variant)
|
||||
spree_delete :soft_delete, variant_id: variant.id, product_id: variant.product.permalink, format: :json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "as an administrator" do
|
||||
sign_in_as_admin!
|
||||
|
||||
let(:product) { create(:product) }
|
||||
let(:variant) { product.master }
|
||||
|
||||
it "soft deletes a variant" do
|
||||
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
|
||||
expect(response.status).to eq(204)
|
||||
expect { variant.reload }.not_to raise_error
|
||||
expect(variant.deleted_at).not_to be_nil
|
||||
end
|
||||
|
||||
it "doesn't delete the only variant of the product" do
|
||||
product = create(:product)
|
||||
variant = product.variants.first
|
||||
|
||||
spree_delete :soft_delete, variant_id: variant.to_param, product_id: product.to_param, format: :json
|
||||
|
||||
expect(variant.reload).to_not be_deleted
|
||||
expect(assigns(:variant).errors[:product]).to include "must have at least one variant"
|
||||
end
|
||||
|
||||
context 'when the variant is not the master' do
|
||||
before { variant.update_attribute(:is_master, false) }
|
||||
|
||||
it 'refreshes the cache' do
|
||||
expect(OpenFoodNetwork::ProductsCache).to receive(:variant_destroyed).with(variant)
|
||||
spree_delete :soft_delete, variant_id: variant.id, product_id: variant.product.permalink, format: :json
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,4 @@
|
||||
require 'spec_helper'
|
||||
require 'spree/api/testing_support/helpers'
|
||||
require 'support/request/authentication_workflow'
|
||||
|
||||
describe Spree::CheckoutController, type: :controller do
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
require 'spec_helper'
|
||||
require 'spree/api/testing_support/helpers'
|
||||
|
||||
describe Spree::UsersController, type: :controller do
|
||||
include AuthenticationWorkflow
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
require 'spec_helper'
|
||||
require 'spree/api/testing_support/helpers'
|
||||
|
||||
describe UserPasswordsController, type: :controller do
|
||||
include OpenFoodNetwork::EmailHelper
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
require 'spec_helper'
|
||||
require 'spree/api/testing_support/helpers'
|
||||
|
||||
describe UserRegistrationsController, type: :controller do
|
||||
include OpenFoodNetwork::EmailHelper
|
||||
|
||||
@@ -41,6 +41,6 @@ FactoryBot.modify do
|
||||
factory :shipping_method, parent: :base_shipping_method do
|
||||
distributors { [Enterprise.is_distributor.first || FactoryBot.create(:distributor_enterprise)] }
|
||||
display_on ''
|
||||
zones { |a| [] }
|
||||
zones { [] }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -437,6 +437,7 @@ feature '
|
||||
visit spree.admin_products_path
|
||||
|
||||
select2_select s1.name, from: "producer_filter"
|
||||
apply_filters
|
||||
|
||||
expect(page).to have_no_field "product_name", with: p2.name
|
||||
fill_in "product_name", with: "new product1"
|
||||
@@ -609,6 +610,7 @@ feature '
|
||||
|
||||
# Set a filter
|
||||
select2_select s1.name, from: "producer_filter"
|
||||
apply_filters
|
||||
|
||||
# Products are hidden when filtered out
|
||||
expect(page).to have_field "product_name", with: p1.name
|
||||
@@ -616,6 +618,7 @@ feature '
|
||||
|
||||
# Clearing filters
|
||||
click_button "Clear Filters"
|
||||
apply_filters
|
||||
|
||||
# All products are shown again
|
||||
expect(page).to have_field "product_name", with: p1.name
|
||||
@@ -789,4 +792,8 @@ feature '
|
||||
expect(page).to have_selector "div.reveal-modal"
|
||||
end
|
||||
end
|
||||
|
||||
def apply_filters
|
||||
page.find('.button.icon-search').click
|
||||
end
|
||||
end
|
||||
|
||||
34
spec/features/admin/inventory_spec.rb
Normal file
34
spec/features/admin/inventory_spec.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
require "spec_helper"
|
||||
|
||||
feature "Managing inventory", js: true do
|
||||
include AdminHelper
|
||||
include AuthenticationWorkflow
|
||||
include WebHelper
|
||||
|
||||
it "shows more than 100 products" do
|
||||
supplier = create(:supplier_enterprise, sells: "own")
|
||||
inventory_items = (1..101).map do
|
||||
product = create(:simple_product, supplier: supplier)
|
||||
InventoryItem.create!(
|
||||
enterprise: supplier,
|
||||
variant: product.variants.first,
|
||||
visible: true
|
||||
)
|
||||
end
|
||||
first_variant = inventory_items.first.variant
|
||||
last_variant = inventory_items.last.variant
|
||||
first_variant.product.update_attributes!(name: "A First Product")
|
||||
last_variant.product.update_attributes!(name: "Z Last Product")
|
||||
quick_login_as supplier.users.first
|
||||
visit admin_inventory_path
|
||||
|
||||
expect(page).to have_text first_variant.name
|
||||
expect(page).to have_selector "tr.product", count: 10
|
||||
expect(page).to have_button "Show more"
|
||||
expect(page).to have_button "Show all (91 More)"
|
||||
|
||||
click_button "Show all (91 More)"
|
||||
expect(page).to have_selector "tr.product", count: 101
|
||||
expect(page).to have_text last_variant.name
|
||||
end
|
||||
end
|
||||
@@ -170,6 +170,7 @@ feature "Product Import", js: true do
|
||||
expect(page).to have_selector 'div#s2id_import_date_filter'
|
||||
import_time = carrots.import_date.to_date.to_formatted_s(:long)
|
||||
select2_select import_time, from: "import_date_filter"
|
||||
page.find('.button.icon-search').click
|
||||
|
||||
expect(page).to have_field "product_name", with: carrots.name
|
||||
expect(page).to have_field "product_name", with: potatoes.name
|
||||
|
||||
@@ -27,7 +27,7 @@ feature "Packing Reports", js: true do
|
||||
select oc.name, from: "q_order_cycle_id_in"
|
||||
|
||||
find('#q_completed_at_gt').click
|
||||
select_date(Time.zone.today - 1.days)
|
||||
select_date(Time.zone.today - 1.day)
|
||||
|
||||
find('#q_completed_at_lt').click
|
||||
select_date(Time.zone.today)
|
||||
|
||||
@@ -299,13 +299,6 @@ describe "AdminProductEditCtrl", ->
|
||||
$scope.$digest()
|
||||
expect($scope.resetProducts).toHaveBeenCalled()
|
||||
|
||||
it "sets the loading property to true before fetching products and unsets it when loading is complete", ->
|
||||
$scope.fetchProducts()
|
||||
expect($scope.loading).toEqual true
|
||||
$scope.$digest()
|
||||
expect($scope.loading).toEqual false
|
||||
|
||||
|
||||
describe "resetting products", ->
|
||||
beforeEach ->
|
||||
spyOn DirtyProducts, "clear"
|
||||
|
||||
@@ -8,35 +8,6 @@ describe "BulkProducts service", ->
|
||||
BulkProducts = _BulkProducts_
|
||||
$httpBackend = _$httpBackend_
|
||||
|
||||
describe "fetching products", ->
|
||||
beforeEach ->
|
||||
spyOn BulkProducts, 'addProducts'
|
||||
|
||||
it "makes a standard call to dataFetcher when no filters exist", ->
|
||||
$httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond "list of products"
|
||||
BulkProducts.fetch [], ->
|
||||
$httpBackend.flush()
|
||||
|
||||
it "makes more calls to dataFetcher if more pages exist", ->
|
||||
$httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;").respond { products: [], pages: 2 }
|
||||
$httpBackend.expectGET("/api/products/bulk_products?page=2;per_page=20;").respond { products: ["list of products"] }
|
||||
BulkProducts.fetch [], ->
|
||||
$httpBackend.flush()
|
||||
|
||||
it "applies filters when they are supplied", ->
|
||||
filter =
|
||||
property:
|
||||
name: "Name"
|
||||
db_column: "name"
|
||||
predicate:
|
||||
name: "Equals"
|
||||
predicate: "eq"
|
||||
value: "Product1"
|
||||
$httpBackend.expectGET("/api/products/bulk_products?page=1;per_page=20;q[name_eq]=Product1;").respond "list of products"
|
||||
BulkProducts.fetch [filter], ->
|
||||
$httpBackend.flush()
|
||||
|
||||
|
||||
describe "cloning products", ->
|
||||
it "clones products using a http post request to /api/products/(id)/clone", ->
|
||||
BulkProducts.products = [
|
||||
|
||||
@@ -114,7 +114,6 @@ describe Enterprise do
|
||||
subject { FactoryBot.create(:distributor_enterprise) }
|
||||
it { is_expected.to validate_presence_of(:name) }
|
||||
it { is_expected.to validate_uniqueness_of(:permalink) }
|
||||
it { is_expected.to ensure_length_of(:description).is_at_most(255) }
|
||||
|
||||
it "requires an owner" do
|
||||
expect{
|
||||
|
||||
@@ -250,7 +250,7 @@ describe ProductImport::ProductImporter do
|
||||
describe "updating an exiting variant" do
|
||||
let(:csv_data) {
|
||||
CSV.generate do |csv|
|
||||
csv << ["name", "producer", "description" ,"category", "on_hand", "price", "units", "unit_type", "display_name", "shipping_category"]
|
||||
csv << ["name", "producer", "description", "category", "on_hand", "price", "units", "unit_type", "display_name", "shipping_category"]
|
||||
csv << ["Hypothetical Cake", "Another Enterprise", "New Description", "Cake", "5", "5.50", "500", "g", "Preexisting Banana", shipping_category.name]
|
||||
end
|
||||
}
|
||||
@@ -530,7 +530,6 @@ describe ProductImport::ProductImporter do
|
||||
}
|
||||
let(:importer) { import_data csv_data, import_into: 'inventories' }
|
||||
|
||||
|
||||
it "updates inventory item correctly" do
|
||||
importer.save_entries
|
||||
|
||||
@@ -548,8 +547,8 @@ describe ProductImport::ProductImporter do
|
||||
let!(:inventory) { InventoryItem.create(variant_id: product4.variants.first.id, enterprise_id: enterprise2.id, visible: false) }
|
||||
let(:csv_data) {
|
||||
CSV.generate do |csv|
|
||||
csv << ["name", "distributor", "producer", "on_hand", "price", "units", "variant_unit_name"]
|
||||
csv << ["Cabbage", "Another Enterprise", "User Enterprise", "900", "", "1", "Whole"]
|
||||
csv << ["name", "distributor", "producer", "on_hand", "price", "units", "variant_unit_name"]
|
||||
csv << ["Cabbage", "Another Enterprise", "User Enterprise", "900", "", "1", "Whole"]
|
||||
end
|
||||
}
|
||||
let(:importer) { import_data csv_data, import_into: 'inventories' }
|
||||
|
||||
@@ -385,7 +385,7 @@ describe Spree::Order do
|
||||
let(:distributor) { create(:distributor_enterprise) }
|
||||
|
||||
let(:order_cycle) do
|
||||
create(:order_cycle).tap do |record|
|
||||
create(:order_cycle).tap do
|
||||
create(:exchange, variants: [v1], incoming: true)
|
||||
create(:exchange, variants: [v1], incoming: false, receiver: distributor)
|
||||
end
|
||||
|
||||
@@ -390,6 +390,17 @@ module Spree
|
||||
expect(stockable_products).to_not include p3
|
||||
end
|
||||
end
|
||||
|
||||
describe "imported_on" do
|
||||
let!(:v1) { create(:variant, import_date: 1.day.ago) }
|
||||
let!(:v2) { create(:variant, import_date: 2.days.ago) }
|
||||
let!(:v3) { create(:variant, import_date: 1.day.ago) }
|
||||
|
||||
it "returns products imported on given day" do
|
||||
imported_products = Spree::Product.imported_on(1.day.ago.to_date)
|
||||
expect(imported_products).to include v1.product, v3.product
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "properties" do
|
||||
|
||||
@@ -3,11 +3,27 @@ require 'spec_helper'
|
||||
describe "checking out an order with a Stripe Connect payment method", type: :request do
|
||||
include ShopWorkflow
|
||||
include AuthenticationWorkflow
|
||||
include OpenFoodNetwork::ApiHelper
|
||||
|
||||
let!(:order_cycle) { create(:simple_order_cycle) }
|
||||
let!(:enterprise) { create(:distributor_enterprise) }
|
||||
let!(:exchange) { create(:exchange, order_cycle: order_cycle, sender: order_cycle.coordinator, receiver: enterprise, incoming: false, pickup_time: "Monday") }
|
||||
let!(:shipping_method) { create(:shipping_method, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 0), distributors: [enterprise]) }
|
||||
let!(:exchange) do
|
||||
create(
|
||||
:exchange,
|
||||
order_cycle: order_cycle,
|
||||
sender: order_cycle.coordinator,
|
||||
receiver: enterprise,
|
||||
incoming: false,
|
||||
pickup_time: "Monday"
|
||||
)
|
||||
end
|
||||
let!(:shipping_method) do
|
||||
create(
|
||||
:shipping_method,
|
||||
calculator: Spree::Calculator::FlatRate.new(preferred_amount: 0),
|
||||
distributors: [enterprise]
|
||||
)
|
||||
end
|
||||
let!(:payment_method) { create(:stripe_payment_method, distributors: [enterprise]) }
|
||||
let!(:stripe_account) { create(:stripe_account, enterprise: enterprise) }
|
||||
let!(:line_item) { create(:line_item, price: 12.34) }
|
||||
@@ -17,19 +33,48 @@ describe "checking out an order with a Stripe Connect payment method", type: :re
|
||||
let(:new_token) { "newtoken123" }
|
||||
let(:card_id) { "card_XyZ456" }
|
||||
let(:customer_id) { "cus_A123" }
|
||||
let(:payments_attributes) do
|
||||
{
|
||||
payment_method_id: payment_method.id,
|
||||
source_attributes: {
|
||||
gateway_payment_profile_id: token,
|
||||
cc_type: "visa",
|
||||
last_digits: "4242",
|
||||
month: 10,
|
||||
year: 2025,
|
||||
first_name: 'Jill',
|
||||
last_name: 'Jeffreys'
|
||||
}
|
||||
}
|
||||
end
|
||||
let(:allowed_address_attributes) do
|
||||
[
|
||||
"firstname",
|
||||
"lastname",
|
||||
"address1",
|
||||
"address2",
|
||||
"phone",
|
||||
"city",
|
||||
"zipcode",
|
||||
"state_id",
|
||||
"country_id"
|
||||
]
|
||||
end
|
||||
let(:params) do
|
||||
{ format: :json, order: {
|
||||
shipping_method_id: shipping_method.id,
|
||||
payments_attributes: [{ payment_method_id: payment_method.id, source_attributes: { gateway_payment_profile_id: token, cc_type: "visa", last_digits: "4242", month: 10, year: 2025, first_name: 'Jill', last_name: 'Jeffreys' } }],
|
||||
bill_address_attributes: address.attributes.slice("firstname", "lastname", "address1", "address2", "phone", "city", "zipcode", "state_id", "country_id"),
|
||||
ship_address_attributes: address.attributes.slice("firstname", "lastname", "address1", "address2", "phone", "city", "zipcode", "state_id", "country_id")
|
||||
} }
|
||||
{
|
||||
format: :json, order: {
|
||||
shipping_method_id: shipping_method.id,
|
||||
payments_attributes: [payments_attributes],
|
||||
bill_address_attributes: address.attributes.slice(*allowed_address_attributes),
|
||||
ship_address_attributes: address.attributes.slice(*allowed_address_attributes)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
order_cycle_distributed_variants = double(:order_cycle_distributed_variants)
|
||||
allow(OrderCycleDistributedVariants).to receive(:new).and_return(order_cycle_distributed_variants)
|
||||
allow(order_cycle_distributed_variants).to receive(:distributes_order_variants?).and_return(true)
|
||||
allow(OrderCycleDistributedVariants).to receive(:new) { order_cycle_distributed_variants }
|
||||
allow(order_cycle_distributed_variants).to receive(:distributes_order_variants?) { true }
|
||||
|
||||
allow(Stripe).to receive(:api_key) { "sk_test_12345" }
|
||||
order.update_attributes(distributor_id: enterprise.id, order_cycle_id: order_cycle.id)
|
||||
@@ -38,9 +83,22 @@ describe "checking out an order with a Stripe Connect payment method", type: :re
|
||||
end
|
||||
|
||||
context "when a new card is submitted" do
|
||||
let(:store_response_mock) { { status: 200, body: JSON.generate(id: customer_id, default_card: card_id, sources: { data: [{ id: "1" }] }) } }
|
||||
let(:token_response_mock) { { status: 200, body: JSON.generate(id: new_token) } }
|
||||
let(:charge_response_mock) { { status: 200, body: JSON.generate(id: "ch_1234", object: "charge", amount: 2000) } }
|
||||
let(:store_response_mock) do
|
||||
{
|
||||
status: 200,
|
||||
body: JSON.generate(
|
||||
id: customer_id,
|
||||
default_card: card_id,
|
||||
sources: { data: [{ id: "1" }] }
|
||||
)
|
||||
}
|
||||
end
|
||||
let(:token_response_mock) do
|
||||
{ status: 200, body: JSON.generate(id: new_token) }
|
||||
end
|
||||
let(:charge_response_mock) do
|
||||
{ status: 200, body: JSON.generate(id: "ch_1234", object: "charge", amount: 2000) }
|
||||
end
|
||||
|
||||
context "and the user doesn't request that the card is saved for later" do
|
||||
before do
|
||||
@@ -53,10 +111,12 @@ describe "checking out an order with a Stripe Connect payment method", type: :re
|
||||
context "and the charge request is successful" do
|
||||
it "should process the payment without storing card details" do
|
||||
put update_checkout_path, params
|
||||
json_response = JSON.parse(response.body)
|
||||
|
||||
expect(json_response["path"]).to eq spree.order_path(order)
|
||||
expect(order.payments.completed.count).to be 1
|
||||
|
||||
card = order.payments.completed.first.source
|
||||
|
||||
expect(card.gateway_customer_profile_id).to eq nil
|
||||
expect(card.gateway_payment_profile_id).to eq token
|
||||
expect(card.cc_type).to eq "visa"
|
||||
@@ -67,12 +127,15 @@ describe "checking out an order with a Stripe Connect payment method", type: :re
|
||||
end
|
||||
|
||||
context "when the charge request returns an error message" do
|
||||
let(:charge_response_mock) { { status: 402, body: JSON.generate(error: { message: "charge-failure" }) } }
|
||||
let(:charge_response_mock) do
|
||||
{ status: 402, body: JSON.generate(error: { message: "charge-failure" }) }
|
||||
end
|
||||
|
||||
it "should not process the payment" do
|
||||
put update_checkout_path, params
|
||||
|
||||
expect(response.status).to be 400
|
||||
json_response = JSON.parse(response.body)
|
||||
|
||||
expect(json_response["flash"]["error"]).to eq "charge-failure"
|
||||
expect(order.payments.completed.count).to be 0
|
||||
end
|
||||
@@ -81,7 +144,8 @@ describe "checking out an order with a Stripe Connect payment method", type: :re
|
||||
|
||||
context "and the customer requests that the card is saved for later" do
|
||||
before do
|
||||
params[:order][:payments_attributes][0][:source_attributes][:save_requested_by_customer] = '1'
|
||||
source_attributes = params[:order][:payments_attributes][0][:source_attributes]
|
||||
source_attributes[:save_requested_by_customer] = '1'
|
||||
|
||||
# Saves the card against the user
|
||||
stub_request(:post, "https://api.stripe.com/v1/customers")
|
||||
@@ -95,16 +159,21 @@ describe "checking out an order with a Stripe Connect payment method", type: :re
|
||||
|
||||
# Charges the card
|
||||
stub_request(:post, "https://api.stripe.com/v1/charges")
|
||||
.with(basic_auth: ["sk_test_12345", ""], body: /#{token}.*#{order.number}/).to_return(charge_response_mock)
|
||||
.with(
|
||||
basic_auth: ["sk_test_12345", ""],
|
||||
body: /#{token}.*#{order.number}/
|
||||
).to_return(charge_response_mock)
|
||||
end
|
||||
|
||||
context "and the store, token and charge requests are successful" do
|
||||
it "should process the payment, and stores the card/customer details" do
|
||||
put update_checkout_path, params
|
||||
json_response = JSON.parse(response.body)
|
||||
|
||||
expect(json_response["path"]).to eq spree.order_path(order)
|
||||
expect(order.payments.completed.count).to be 1
|
||||
|
||||
card = order.payments.completed.first.source
|
||||
|
||||
expect(card.gateway_customer_profile_id).to eq customer_id
|
||||
expect(card.gateway_payment_profile_id).to eq card_id
|
||||
expect(card.cc_type).to eq "visa"
|
||||
@@ -115,37 +184,47 @@ describe "checking out an order with a Stripe Connect payment method", type: :re
|
||||
end
|
||||
|
||||
context "when the store request returns an error message" do
|
||||
let(:store_response_mock) { { status: 402, body: JSON.generate(error: { message: "store-failure" }) } }
|
||||
let(:store_response_mock) do
|
||||
{ status: 402, body: JSON.generate(error: { message: "store-failure" }) }
|
||||
end
|
||||
|
||||
it "should not process the payment" do
|
||||
put update_checkout_path, params
|
||||
|
||||
expect(response.status).to be 400
|
||||
json_response = JSON.parse(response.body)
|
||||
expect(json_response["flash"]["error"]).to eq I18n.t(:spree_gateway_error_flash_for_checkout, error: 'store-failure')
|
||||
|
||||
expect(json_response["flash"]["error"])
|
||||
.to eq(I18n.t(:spree_gateway_error_flash_for_checkout, error: 'store-failure'))
|
||||
expect(order.payments.completed.count).to be 0
|
||||
end
|
||||
end
|
||||
|
||||
context "when the charge request returns an error message" do
|
||||
let(:charge_response_mock) { { status: 402, body: JSON.generate(error: { message: "charge-failure" }) } }
|
||||
let(:charge_response_mock) do
|
||||
{ status: 402, body: JSON.generate(error: { message: "charge-failure" }) }
|
||||
end
|
||||
|
||||
it "should not process the payment" do
|
||||
put update_checkout_path, params
|
||||
|
||||
expect(response.status).to be 400
|
||||
json_response = JSON.parse(response.body)
|
||||
|
||||
expect(json_response["flash"]["error"]).to eq "charge-failure"
|
||||
expect(order.payments.completed.count).to be 0
|
||||
end
|
||||
end
|
||||
|
||||
context "when the token request returns an error message" do
|
||||
let(:token_response_mock) { { status: 402, body: JSON.generate(error: { message: "token-failure" }) } }
|
||||
let(:token_response_mock) do
|
||||
{ status: 402, body: JSON.generate(error: { message: "token-failure" }) }
|
||||
end
|
||||
|
||||
# Note, no requests have been stubbed
|
||||
it "should not process the payment" do
|
||||
put update_checkout_path, params
|
||||
|
||||
expect(response.status).to be 400
|
||||
json_response = JSON.parse(response.body)
|
||||
|
||||
expect(json_response["flash"]["error"]).to eq "token-failure"
|
||||
expect(order.payments.completed.count).to be 0
|
||||
end
|
||||
@@ -169,7 +248,9 @@ describe "checking out an order with a Stripe Connect payment method", type: :re
|
||||
end
|
||||
|
||||
let(:token_response_mock) { { status: 200, body: JSON.generate(id: new_token) } }
|
||||
let(:charge_response_mock) { { status: 200, body: JSON.generate(id: "ch_1234", object: "charge", amount: 2000) } }
|
||||
let(:charge_response_mock) do
|
||||
{ status: 200, body: JSON.generate(id: "ch_1234", object: "charge", amount: 2000) }
|
||||
end
|
||||
|
||||
before do
|
||||
params[:order][:existing_card_id] = credit_card.id
|
||||
@@ -189,10 +270,12 @@ describe "checking out an order with a Stripe Connect payment method", type: :re
|
||||
context "and the charge and token requests are accepted" do
|
||||
it "should process the payment, and keep the profile ids and other card details" do
|
||||
put update_checkout_path, params
|
||||
json_response = JSON.parse(response.body)
|
||||
|
||||
expect(json_response["path"]).to eq spree.order_path(order)
|
||||
expect(order.payments.completed.count).to be 1
|
||||
|
||||
card = order.payments.completed.first.source
|
||||
|
||||
expect(card.gateway_customer_profile_id).to eq customer_id
|
||||
expect(card.gateway_payment_profile_id).to eq card_id
|
||||
expect(card.cc_type).to eq "master"
|
||||
@@ -203,24 +286,30 @@ describe "checking out an order with a Stripe Connect payment method", type: :re
|
||||
end
|
||||
|
||||
context "when the charge request returns an error message" do
|
||||
let(:charge_response_mock) { { status: 402, body: JSON.generate(error: { message: "charge-failure" }) } }
|
||||
let(:charge_response_mock) do
|
||||
{ status: 402, body: JSON.generate(error: { message: "charge-failure" }) }
|
||||
end
|
||||
|
||||
it "should not process the payment" do
|
||||
put update_checkout_path, params
|
||||
|
||||
expect(response.status).to be 400
|
||||
json_response = JSON.parse(response.body)
|
||||
|
||||
expect(json_response["flash"]["error"]).to eq "charge-failure"
|
||||
expect(order.payments.completed.count).to be 0
|
||||
end
|
||||
end
|
||||
|
||||
context "when the token request returns an error message" do
|
||||
let(:token_response_mock) { { status: 402, body: JSON.generate(error: { message: "token-error" }) } }
|
||||
let(:token_response_mock) do
|
||||
{ status: 402, body: JSON.generate(error: { message: "token-error" }) }
|
||||
end
|
||||
|
||||
it "should not process the payment" do
|
||||
put update_checkout_path, params
|
||||
|
||||
expect(response.status).to be 400
|
||||
json_response = JSON.parse(response.body)
|
||||
|
||||
expect(json_response["flash"]["error"]).to eq "token-error"
|
||||
expect(order.payments.completed.count).to be 0
|
||||
end
|
||||
|
||||
@@ -39,10 +39,9 @@ Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
|
||||
require 'spree/testing_support/controller_requests'
|
||||
require 'spree/testing_support/capybara_ext'
|
||||
require 'spree/api/testing_support/setup'
|
||||
require 'spree/api/testing_support/helpers'
|
||||
require 'spree/api/testing_support/helpers_decorator'
|
||||
require 'spree/testing_support/authorization_helpers'
|
||||
require 'spree/testing_support/preferences'
|
||||
require 'support/api_helper'
|
||||
|
||||
# Capybara config
|
||||
require 'selenium-webdriver'
|
||||
@@ -127,8 +126,6 @@ RSpec.configure do |config|
|
||||
spree_config.shipping_instructions = true
|
||||
spree_config.auto_capture = true
|
||||
end
|
||||
|
||||
Spree::Api::Config[:requires_authentication] = true
|
||||
end
|
||||
|
||||
# Helpers
|
||||
@@ -140,7 +137,7 @@ RSpec.configure do |config|
|
||||
config.include Spree::TestingSupport::Preferences
|
||||
config.include Devise::TestHelpers, type: :controller
|
||||
config.extend Spree::Api::TestingSupport::Setup, type: :controller
|
||||
config.include Spree::Api::TestingSupport::Helpers, type: :controller
|
||||
config.include OpenFoodNetwork::ApiHelper, type: :controller
|
||||
config.include OpenFoodNetwork::ControllerHelper, type: :controller
|
||||
config.include Features::DatepickerHelper, type: :feature
|
||||
config.include OpenFoodNetwork::FeatureToggleHelper
|
||||
|
||||
@@ -11,5 +11,18 @@ module OpenFoodNetwork
|
||||
json_response
|
||||
end
|
||||
end
|
||||
|
||||
def current_api_user
|
||||
@current_api_user ||= Spree::LegacyUser.new(email: "spree@example.com", enterprises: [])
|
||||
end
|
||||
|
||||
def assert_unauthorized!
|
||||
expect(json_response).to eq("error" => "You are not authorized to perform that action.")
|
||||
expect(response.status).to eq 401
|
||||
end
|
||||
|
||||
def image(filename)
|
||||
File.open(Rails.root + "spec/support/fixtures" + filename)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
34
spec/support/controller_hacks.rb
Normal file
34
spec/support/controller_hacks.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
require 'active_support/all'
|
||||
|
||||
module ControllerHacks
|
||||
def api_get(action, params = {}, session = nil, flash = nil)
|
||||
api_process(action, params, session, flash, "GET")
|
||||
end
|
||||
|
||||
def api_post(action, params = {}, session = nil, flash = nil)
|
||||
api_process(action, params, session, flash, "POST")
|
||||
end
|
||||
|
||||
def api_put(action, params = {}, session = nil, flash = nil)
|
||||
api_process(action, params, session, flash, "PUT")
|
||||
end
|
||||
|
||||
def api_delete(action, params = {}, session = nil, flash = nil)
|
||||
api_process(action, params, session, flash, "DELETE")
|
||||
end
|
||||
|
||||
def api_process(action, params = {}, session = nil, flash = nil, method = "get")
|
||||
scoping = respond_to?(:resource_scoping) ? resource_scoping : {}
|
||||
process(action,
|
||||
params.
|
||||
merge(scoping).
|
||||
reverse_merge!(use_route: :spree, format: :json),
|
||||
session,
|
||||
flash,
|
||||
method)
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.include ControllerHacks, type: :controller
|
||||
end
|
||||
BIN
spec/support/fixtures/thinking-cat.jpg
Normal file
BIN
spec/support/fixtures/thinking-cat.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -1,5 +1,5 @@
|
||||
# From: https://robots.thoughtbot.com/better-tests-through-internationalization
|
||||
|
||||
I18n.exception_handler = lambda do |exception, locale, key, options|
|
||||
I18n.exception_handler = lambda do |_exception, _locale, key, _options|
|
||||
raise "missing translation: #{key}"
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user