Compare commits

...

114 Commits

Author SHA1 Message Date
Matt-Yorkley
fc9f61ecf8 Revert PR #4204
Temporarily reverting these changes for a quick release
2019-09-05 13:23:59 +01:00
Luis Ramos
07d4528276 Merge pull request #4174 from openfoodfoundation/dependabot/bundler/delayed_job_active_record-4.1.4
Bump delayed_job_active_record from 4.1.3 to 4.1.4
2019-09-02 22:24:04 +01:00
Luis Ramos
4ace780431 Merge pull request #4187 from openfoodfoundation/dependabot/bundler/knapsack-1.18.0
Bump knapsack from 1.17.2 to 1.18.0
2019-09-02 22:23:15 +01:00
dependabot-preview[bot]
51df8de64f Bump knapsack from 1.17.2 to 1.18.0
Bumps [knapsack](https://github.com/ArturT/knapsack) from 1.17.2 to 1.18.0.
- [Release notes](https://github.com/ArturT/knapsack/releases)
- [Changelog](https://github.com/ArturT/knapsack/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ArturT/knapsack/compare/v1.17.2...v1.18.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-02 16:16:32 +00:00
Matt-Yorkley
d4a5829858 Merge pull request #4081 from Matt-Yorkley/products_pagination
Bulk Edit Products pagination
2019-09-02 17:14:14 +01:00
Matt-Yorkley
ff5fe66994 Fix route after big API refactor merge resolution 2019-09-02 16:29:03 +01:00
Matt-Yorkley
37e50a68e4 Fix timezone date parsing from Angular form fields 2019-09-02 16:29:00 +01:00
Matt-Yorkley
a72c662b97 Update per_page dropdown UX 2019-09-02 14:35:00 +01:00
Matt-Yorkley
ff2db0c5f8 Update spec routes, namespacing, and test content
Resolving new issues after a big merge
2019-09-02 14:35:00 +01:00
Matt-Yorkley
e9c60a33b9 Hide search in dropdown for per_page options 2019-09-02 14:35:00 +01:00
Matt-Yorkley
8e059d3c69 Define a limit on paginated queries with no supplied value for per_page 2019-09-02 14:35:00 +01:00
Matt-Yorkley
806ba94a2e Clarify dependency on kaminari gem 2019-09-02 14:35:00 +01:00
Matt-Yorkley
4bec583bff Refactor import_date_scope 2019-09-02 14:35:00 +01:00
Matt-Yorkley
90256f9c28 Add defaults and pages tests 2019-09-02 14:35:00 +01:00
Matt-Yorkley
eb284c1742 Use constants for defaults 2019-09-02 14:35:00 +01:00
Matt-Yorkley
b614e17f48 Add test coverage for #bulk_products endpoint and test all filtering functions 2019-09-02 14:34:54 +01:00
Matt-Yorkley
5259eaae5f Merge pull request #4204 from Matt-Yorkley/cartastrophe
Cartastrophe averted :)
2019-09-02 12:26:08 +01:00
Matt-Yorkley
b0ad0fccfa Add some defaults to avoid returning zero values when not supplied in query 2019-09-02 12:00:32 +01:00
Matt-Yorkley
2a83ad8689 Improve UX and consistency in orders pagination and page changing 2019-09-02 11:59:13 +01:00
Matt-Yorkley
c127110192 Make import_date query modification conditional 2019-09-02 11:59:13 +01:00
Matt-Yorkley
0470725112 Refactor pagination data hash 2019-09-02 11:57:32 +01:00
Matt-Yorkley
0623bab084 Don't respond to a successful update by querying 500 arbitrary products 2019-09-02 11:56:00 +01:00
Matt-Yorkley
4a0df684c7 Adjust specs 2019-09-02 11:56:00 +01:00
Matt-Yorkley
7dccb5ba90 Changing per_page should also reset the query 2019-09-02 11:56:00 +01:00
Matt-Yorkley
5a4be24df0 Add "filter results" button 2019-09-02 11:56:00 +01:00
Matt-Yorkley
5cb5967977 Fix cleared filters submitting "0" as value in queries 2019-09-02 11:56:00 +01:00
Matt-Yorkley
aeb8d30dae Fix server-side import_date filtering 2019-09-02 11:56:00 +01:00
Matt-Yorkley
1822fd97a6 Tidy up filters 2019-09-02 11:54:28 +01:00
Matt-Yorkley
4ff3e9fe10 Update Angular loading conditionals 2019-09-02 11:54:28 +01:00
Matt-Yorkley
a63994440d Add pagination to UI 2019-09-02 11:54:28 +01:00
Matt-Yorkley
f6d0de1454 Improve pagination data in bulk products 2019-09-02 11:54:28 +01:00
Matt-Yorkley
9b0e27a9d1 Add new ProductResource 2019-09-02 11:46:42 +01:00
Matt-Yorkley
415d88f302 Fix indentation in bulk product controller 2019-09-02 11:46:42 +01:00
Pau Pérez Fabregat
f9c98ea9a1 Merge pull request #4199 from openfoodfoundation/transifex
Transifex
2019-09-02 12:33:12 +02:00
Pau Pérez Fabregat
369a5a8a2f Merge pull request #4101 from luisramos0/remove_variants_rabl
Convert spree/api/products and spree/api/variants views from rabl to AMS
2019-09-02 12:14:26 +02:00
Matt-Yorkley
62341c6381 Unit test access to associated objects after soft-delete 2019-08-31 10:06:47 +01:00
Luis Ramos
fa1becb791 Merge pull request #4063 from luisramos0/dead_code
Remove dead code under views/spree/shared
2019-08-30 22:39:01 +01:00
Matt-Yorkley
50a1704994 Make prices soft-deletable 2019-08-30 20:11:32 +01:00
Matt-Yorkley
302538c370 Add failing spec for cart issue 2019-08-30 15:32:45 +01:00
Maikel
0f80b6ce12 Merge pull request #4197 from kristinalim/fix/4195-fix_invalid_date_in_firefox
4195 Specify API date format when converting date to string in JS
2019-08-30 08:36:17 +10:00
Transifex-Openfoodnetwork
1df8fc903e Updating translations for config/locales/en_ZA.yml 2019-08-30 04:22:54 +10:00
Transifex-Openfoodnetwork
9a2dcb89af Updating translations for config/locales/en_ZA.yml 2019-08-30 04:22:46 +10:00
Transifex-Openfoodnetwork
1661591f6c Updating translations for config/locales/ca.yml 2019-08-29 23:12:46 +10:00
Transifex-Openfoodnetwork
6dde720039 Updating translations for config/locales/es.yml 2019-08-29 23:10:30 +10:00
Transifex-Openfoodnetwork
a54b725d6d Updating translations for config/locales/ca.yml 2019-08-29 23:09:39 +10:00
Luis Ramos
265e76e8ca Merge pull request #4074 from HugsDaniel/defacepocalypse
[Defacepocalypse] De-deface product properties index
2019-08-28 22:18:18 +01:00
Kristina Lim
1516069888 Specify API date format when converting date to string in JS 2019-08-29 02:44:14 +08:00
Luis Ramos
cd263b761c Merge pull request #4055 from luisramos0/remove_spree_api
Remove dependency to spree_api - step 1 - controllers and routes
2019-08-28 15:34:08 +01:00
Maikel
c952ad16ad Merge pull request #4163 from luisramos0/swagger
Add swagger.yaml to codebase
2019-08-28 14:46:51 +10:00
Maikel
ca09c58f26 Merge pull request #3985 from jonleighton/string-to-text
Convert several fields from string to text
2019-08-28 11:53:49 +10:00
Hugo Daniel
7d21d88dc9 Force hide the select2 close cross 2019-08-22 10:32:07 +02:00
dependabot-preview[bot]
31b62d6296 Bump delayed_job_active_record from 4.1.3 to 4.1.4
Bumps [delayed_job_active_record](https://github.com/collectiveidea/delayed_job_active_record) from 4.1.3 to 4.1.4.
- [Release notes](https://github.com/collectiveidea/delayed_job_active_record/releases)
- [Commits](https://github.com/collectiveidea/delayed_job_active_record/compare/v4.1.3...v4.1.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-08-20 19:14:59 +00:00
Kevin Christianson
2dfcedad56 Add swagger.yaml 2019-08-18 18:26:44 +01:00
luisramos0
b9ddb39edc Re-add taxons jstree action to make taxonomies config page work again 2019-08-14 16:31:34 +01:00
luisramos0
4aa6c673ff Adapt api products and variants controllers to new namespace outside of Spree 2019-08-01 18:34:19 +01:00
luisramos0
aa3c1aa0fe Remove Spree module declaration from these files as they were moved out of the spree namespace 2019-08-01 14:30:11 +01:00
luisramos0
31bac9641f Move api products and variants routes and ctrl out of spree namespace 2019-08-01 14:28:55 +01:00
luisramos0
b7f7038934 Remove api/enterprises rabl template, it was only used as a member in the now removed rabl variants/products templates 2019-07-31 14:36:36 +01:00
luisramos0
6c054e6078 Add bulk_products and overridable to skip_authorization_check so these endpoints work with AMS 2019-07-31 12:18:27 +01:00
luisramos0
18974c68e1 Remove orphan price check from price model
This is a quick fix. This check is breaking product deletion in some situations and orphan Prices are not really a problem in the DB
2019-07-31 11:24:55 +01:00
luisramos0
78ab852141 Make spree/api/products_controller work with AMS 2019-07-31 11:23:43 +01:00
luisramos0
4497173213 Adapt spree/api/products_controller_spec to AMS serializer 2019-07-31 11:23:18 +01:00
luisramos0
4d74d246e8 Remove spree/api/products_controller index and new actions, not used 2019-07-31 10:32:45 +01:00
luisramos0
cc51537e93 Convert spree/api/products_controller from rabl to ams 2019-07-31 09:50:34 +01:00
luisramos0
07aececdcf Remove unused route api/products managed 2019-07-31 09:50:31 +01:00
luisramos0
c3fbf9cdf9 Remove unused pagination from index and respective specs, fix spec for search by sku by adding sku to the serializer and adapt a few specs to pass with AMS attrivbutes, 2019-07-31 09:40:19 +01:00
luisramos0
180598c603 Convert spree/api/variants_controller to AMS by changing base_controller, using render json instad of respond with, deleting rabl templates and adapting specs
Delete unused pagination spec
2019-07-31 09:40:19 +01:00
luisramos0
69a5527e24 Update/regenarate .rubocop_todo.yml 2019-07-31 09:36:48 +01:00
luisramos0
e4a6b3880f Fix some more simple rubocop issues 2019-07-31 09:36:48 +01:00
luisramos0
96ce4deb45 Transpec spec/support/api_helper.rb 2019-07-31 09:36:48 +01:00
luisramos0
a3c179bd3f Fix some more simple rubocop issues 2019-07-31 09:36:48 +01:00
luisramos0
a57504ba1f Bring api_helper.image from spree_api to support spree/api/products_controller_spec 2019-07-31 09:36:48 +01:00
luisramos0
25451eed6b Bring api spec helpers from spree_api into ofn/api_helper 2019-07-31 09:36:48 +01:00
luisramos0
50765563f8 Bring spree/api_helpers from spree_api 2019-07-31 09:35:46 +01:00
luisramos0
2ae75ce13e Add ControllerSetup from spree_api as it is used in spree/api/base_controller 2019-07-31 09:35:46 +01:00
luisramos0
18aa16650d Remove dependency to Spree::ApiConfiguration, overall requires_authentication? is true, exceptions will be endpoint specific 2019-07-31 09:35:46 +01:00
luisramos0
314ed50e0f Fix a rubocop issue in spree/api/products_controller 2019-07-31 09:34:20 +01:00
luisramos0
7346a49982 Move routes in ofn api namespace to separate routes file 2019-07-31 09:34:20 +01:00
luisramos0
5182286218 Add necessary spree api routes related to api keys for users and bring respective implementations from spree_api 2019-07-31 09:34:20 +01:00
luisramos0
a267848394 Remove unused api routes from views/spree/admin/shared/routes view 2019-07-31 09:32:33 +01:00
luisramos0
104bd31f9b Add necessary spree api routes: taxons, variants and shipments 2019-07-31 09:32:33 +01:00
luisramos0
8bc9985edb Transpec and fix rubocop issues in spree/api/variants_controller_spec 2019-07-31 09:32:33 +01:00
luisramos0
6dfc927730 Make spree/api/variant_controllers_spec pass 2019-07-31 09:32:33 +01:00
luisramos0
3771e26eba Bring tests from spree/api/variants_controller_spec from spree_api 2019-07-31 09:32:33 +01:00
luisramos0
fd21d35aee Transpec and fix rubocop issues in spree/api/shipments_controller_spec 2019-07-31 09:32:33 +01:00
luisramos0
1417b924d2 Bring and adapt tests from spree/api/shipments_controller_spec and mix them with exiting tests in OFN 2019-07-31 09:32:33 +01:00
luisramos0
2912c1b87d Transpec and fix rubocop issues in spree/api/product_controller_spec 2019-07-31 09:32:33 +01:00
luisramos0
e746a0db7d Bring tests from spree/api/products_controller_spec and add them to existing ones on the ofn side
Adapt these tests to have a green build
2019-07-31 09:32:33 +01:00
luisramos0
84a2886003 Improve auth code in spree/api/taxons_controller_spec 2019-07-31 09:32:33 +01:00
luisramos0
c668677b8a Bring spree/api/taxons_controller_spec from spree_api, adapt it, transpec it and fix rubocop issues 2019-07-31 09:32:33 +01:00
luisramos0
2490cbfccb Transpec and fix rubocop issues in spree/api/base_controller_spec 2019-07-31 09:32:33 +01:00
luisramos0
20a46a791c Bring and adapt spree/api/base_controller_spec from spree_api 2019-07-31 09:32:33 +01:00
luisramos0
0e4fe08ac4 Fix logical problem in spree/api/base_controller and in spree/checkout_controller
See this stack overflow post for more info: https://stackoverflow.com/questions/39629976/ruby-return-vs-and-return
2019-07-31 09:32:33 +01:00
luisramos0
cf0f716534 Fix easy rubocop issues in spree/api/taxons_controller 2019-07-31 09:32:33 +01:00
luisramos0
b70cfa5968 Bring spree/api/taxons controller from spree_api as it is needed in OFN admin 2019-07-31 09:32:33 +01:00
luisramos0
f77beb50ff Fix class scope in spree/api/products_controller, should not use Spree namespace here
Also, add missing dependency to spree/admin/products_controller_decorator
2019-07-31 09:32:33 +01:00
luisramos0
a941280982 Fix easy rubocop issues in spree/api/base_controller 2019-07-31 09:32:33 +01:00
luisramos0
9d40ee49e6 Bring spree/api/base_controller from spree_api 2019-07-31 09:32:33 +01:00
luisramos0
6abbdecb97 Fix the easy rubocop issues in the new spree api controllers 2019-07-31 09:32:33 +01:00
luisramos0
660ce92c27 Merge spree api controllers and its decorators 2019-07-31 09:32:33 +01:00
luisramos0
c5bcef6ae4 Delete unused spree/api/line_items_controller_decorator.rb 2019-07-31 09:32:33 +01:00
luisramos0
d26a0b6b73 Bring from spree_api the api controllers that are overriden in OFN so that we can merge the original and the override afterwards 2019-07-31 09:32:33 +01:00
Hugo Daniel
c464b21d76 Remove data-hooks 2019-07-25 14:27:53 +02:00
Hugo Daniel
c83d249147 Impor missing partials from spree to ofn and convert to haml 2019-07-25 14:23:24 +02:00
Hugo Daniel
2d872c25bf Use Haml javascript tag to make autocomplete work 2019-07-25 11:20:51 +02:00
Hugo Daniel
0a88738faa Replace old ruby syntax with new 2019-07-24 14:37:00 +02:00
Hugo Daniel
4d6af57f79 De-deface product_properties/index 2019-07-24 13:17:45 +02:00
Hugo Daniel
110fd3ecdf Convert erb to haml 2019-07-24 13:12:58 +02:00
Hugo Daniel
1cb065f829 Import product_properties/index.html.erb from spree_backend to ofn 2019-07-24 13:09:04 +02:00
Hugo Daniel
1cfa499b0e De-deface _product_propery_fields 2019-07-24 13:01:50 +02:00
Hugo Daniel
3fc0d4a666 Convert _product_properties_fields from ERB to Haml 2019-07-24 12:40:24 +02:00
Hugo Daniel
de6c96d138 Import product_properties/_product_properties_fields.html.erb from Spree to OFN 2019-07-24 12:38:29 +02:00
luisramos0
11974689ef Remove dead code under views/spree/shared 2019-07-23 16:42:00 +01:00
Jon Leighton
4398ea12b8 Convert several fields from string to text
See discussion here:
https://github.com/openfoodfoundation/openfoodnetwork/pull/3751#issuecomment-503416955

Fixes #3192.

I have also done a pass over the schema to identify other fields which
would benefit from being a text rather than a string. However, I ignored
all `spree_*` tables because I didn’t want to mess up the ‘default’
Spree schema.
2019-07-09 13:11:30 +10:00
95 changed files with 3446 additions and 1049 deletions

View File

@@ -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'

View File

@@ -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'

View File

@@ -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)
@@ -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)

View File

@@ -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) ->

View File

@@ -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

View File

@@ -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'
})

View File

@@ -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) =>

View File

@@ -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)

View File

@@ -3,7 +3,7 @@
.select2-container {
.select2-choice {
.select2-search-choice-close {
display: none;
display: none !important;
}
.select2-arrow {
width: 22px;

View File

@@ -0,0 +1,143 @@
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,
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

View 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

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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])

View 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

View File

@@ -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? }

View File

@@ -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

View File

@@ -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 })
}

View File

@@ -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?

View File

@@ -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' }

View File

@@ -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')

View File

@@ -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

View File

@@ -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

View File

@@ -1,3 +0,0 @@
object @enterprise
attributes :id, :name

View File

@@ -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} }}

View File

@@ -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

View 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();
}
});

View File

@@ -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'

View File

@@ -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}" }

View File

@@ -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 &nbsp;
.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 &nbsp;
.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)

View File

@@ -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')
'

View File

@@ -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'

View File

@@ -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()'}

View File

@@ -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'

View 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

View File

@@ -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>

View File

@@ -1,2 +0,0 @@
collection @products.order('id ASC')
extends "spree/api/products/bulk_show"

View File

@@ -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

View File

@@ -1,2 +0,0 @@
collection @variants
extends "spree/api/variants/bulk_show"

View File

@@ -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 }

View File

@@ -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 ', '

View 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

View File

@@ -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)}"

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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ó

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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

View 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

View File

@@ -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"

View 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

View File

@@ -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

View File

@@ -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) }

View File

@@ -1,5 +1,4 @@
require 'spec_helper'
require 'spree/api/testing_support/helpers'
module Api
describe OrdersController, type: :controller do

View File

@@ -1,5 +1,4 @@
require 'spec_helper'
require 'spree/api/testing_support/helpers'
module Api
describe ProductImagesController, type: :controller do

View 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

View 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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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 }

View 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

View File

@@ -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

View File

@@ -1,5 +1,4 @@
require 'spec_helper'
require 'spree/api/testing_support/helpers'
require 'support/request/authentication_workflow'
describe Spree::CheckoutController, type: :controller do

View File

@@ -1,5 +1,4 @@
require 'spec_helper'
require 'spree/api/testing_support/helpers'
describe Spree::UsersController, type: :controller do
include AuthenticationWorkflow

View File

@@ -1,5 +1,4 @@
require 'spec_helper'
require 'spree/api/testing_support/helpers'
describe UserPasswordsController, type: :controller do
include OpenFoodNetwork::EmailHelper

View File

@@ -1,5 +1,4 @@
require 'spec_helper'
require 'spree/api/testing_support/helpers'
describe UserRegistrationsController, type: :controller do
include OpenFoodNetwork::EmailHelper

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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"

View File

@@ -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 = [

View File

@@ -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{

View File

@@ -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' }

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -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

View File

@@ -8,5 +8,22 @@ module OpenFoodNetwork
ensure
Spree::Config.products_require_tax_category = original_value
end
shared_examples "modifying product actions are restricted" do
it "cannot create a new product if not an admin" do
api_post :create, product: { name: "Brand new product!" }
assert_unauthorized!
end
it "cannot update a product" do
api_put :update, id: product.to_param, product: { name: "I hacked your store!" }
assert_unauthorized!
end
it "cannot delete a product" do
api_delete :destroy, id: product.to_param
assert_unauthorized!
end
end
end
end

1234
swagger.yaml Normal file

File diff suppressed because it is too large Load Diff