Compare commits

..

1 Commits

Author SHA1 Message Date
filipefurtad0
f30c07c2aa Update all locales with the latest Transifex translations 2024-03-26 14:43:00 +00:00
2137 changed files with 72499 additions and 177484 deletions

56
.codeclimate.yml Normal file
View File

@@ -0,0 +1,56 @@
version: "2"
plugins:
rubocop:
enabled: true
channel: "rubocop-1-12"
config:
file: ".rubocop.yml"
scss-lint:
enabled: true
checks:
ImportantRule:
enabled: false
VendorPrefix:
enabled: false
LeadingZero:
enabled: false
PropertySortOrder:
enabled: false
StringQuotes:
enabled: false
DeclarationOrder:
enabled: false
NestingDepth:
enabled: false
duplication:
enabled: true
exclude_patterns:
- "db/**"
- "config/initializers/active_record_postgresql_referential_integrity_patch.rb"
checks:
argument-count:
enabled: false
complex-logic:
enabled: false
file-lines:
enabled: false
method-complexity:
enabled: false
method-count:
enabled: false
method-lines:
enabled: false
nested-control-flow:
enabled: false
return-statements:
enabled: false
similar-code:
enabled: false
identical-code:
enabled: false
exclude_patterns:
- "spec/**/*"
- "vendor/**/*"
- "app/assets/javascripts/shared/*"
- "app/assets/javascripts/jquery-migrate-1.0.0.js"

21
.env
View File

@@ -10,10 +10,10 @@ TIMEZONE="Melbourne"
DEFAULT_COUNTRY_CODE="AU"
# Locale for translation.
LOCALE="en_AU"
LOCALE="en"
# For multilingual - ENV doesn't have array so pass it as string with commas
AVAILABLE_LOCALES="en_AU,es"
AVAILABLE_LOCALES="en,es"
# Spree zone.
CHECKOUT_ZONE="Australia"
@@ -42,6 +42,14 @@ SMTP_PASSWORD="f00d"
# Javascript error reporting via Bugsnag.
# BUGSNAG_JS_KEY=""
# SingleSignOn login for Discourse
#
# DISCOURSE_SSO_SECRET should be a random string. It must be the same as provided to your Discourse instance.
# DISCOURSE_SSO_SECRET=""
#
# DISCOURSE_URL must be the URL of your Discourse instance.
# DISCOURSE_URL="https://noticeboard.openfoodnetwork.org.au"
# see="https://developers.google.com/maps/documentation/javascript/get-api-key
# GOOGLE_MAPS_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# see https://developers.google.com/maps/documentation/javascript/localization#Region
@@ -61,12 +69,3 @@ SMTP_PASSWORD="f00d"
# NEW_RELIC_AGENT_ENABLED=true
# NEW_RELIC_APP_NAME="Open Food Network"
# NEW_RELIC_LICENSE_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Database encryption configuration, required for VINE connected app
# Generate with bin/rails db:encryption:init
# ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# VINE API settings
# VINE_API_URL="https://vine-staging.openfoodnetwork.org.au/api/v1"

View File

@@ -5,28 +5,17 @@
#
# cp .env.development .env.local
# Locale for translation. Using a locale other than `en` tests the
# successful fallback to `en`. To see up-to-date text used in production,
# set another locale in a local env file.
LOCALE="en_TST"
VERBOSE_QUERY_LOGS=true
SECRET_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
OFN_REDIS_URL="redis://localhost:6379/1"
OFN_REDIS_JOBS_URL="redis://localhost:6379/2"
OFN_REDIS_CABLE_URL="redis://localhost:6379/0"
SITE_URL="localhost:3000"
SITE_URL="0.0.0.0:3000"
# Deactivate rack-timeout in development.
# https://github.com/zombocom/rack-timeout#configuring
RACK_TIMEOUT_SERVICE_TIMEOUT="0"
RACK_TIMEOUT_WAIT_TIMEOUT="0"
RACK_TIMEOUT_WAIT_OVERTIME="0"
# Database encryption configuration, required for VINE connected app
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY="dev_primary_key"
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY="dev_determinnistic_key"
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT="dev_derivation_salt"

View File

@@ -1,18 +1,6 @@
# ENV vars for the test environment
# Override locally with `.env.test.local`
# Test env specific variables
#
# Adjust this to your computer. When you start test-driven development, you may
# want to reduce this value to avoid waiting for a test that you expect to fail.
CAPYBARA_MAX_WAIT_TIME="10"
# General app specific variables
# Locale for translation. Using a locale other than `en` tests the
# successful fallback to `en`.
LOCALE="en_TST"
OFN_REDIS_JOBS_URL="redis://localhost:6379/2"
SECRET_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@@ -20,17 +8,10 @@ STRIPE_INSTANCE_SECRET_KEY="bogus_key"
STRIPE_CUSTOMER="bogus_customer"
STRIPE_ACCOUNT="bogus_account"
STRIPE_CLIENT_ID="bogus_client_id"
STRIPE_PUBLIC_TEST_API_KEY="bogus_stripe_publishable_key"
SITE_URL="test.host"
# OIDC Settings for DFC authentication
# Find secrets in BitWarden.
# To get a refresh token: log into the OIDC provider, connect your OFN user to it at /admin/oidc_settings, then copy the token from the database:
# ./bin/rails runner 'puts "OPENID_REFRESH_TOKEN=\"#{OidcAccount.last.refresh_token}\""'
OPENID_APP_ID="test-provider"
OPENID_APP_SECRET="dummy-openid-app-secret-token"
OPENID_APP_SECRET="12345"
OPENID_REFRESH_TOKEN="dummy-refresh-token"
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY="test_primary_key"
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY="test_deterministic_key"
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT="test_derivation_salt"

View File

@@ -7,13 +7,11 @@ assignees: ''
---
## 1. Drafting on Friday
## 1. Preparation on Thursday
- [ ] Merge pull requests in the [Ready To Go] column
- [ ] Include translations: `script/release/update_locales`
- You need the [Transifex Client] installed on your local dev environement to run the script.
- [ ] Increment version number: `git push upstream HEAD:refs/tags/vX.Y.Z`
Check for [minor or major breaking changes]
- Major: if server changes are required (eg. provision with ofn-install)
- Minor: larger change that is irreversible (eg. migration deleting data)
- Patch: all others. Shortcut: `script/release/tag`
@@ -27,9 +25,8 @@ assignees: ''
- [ ] Move this issue to Test Ready.
- [ ] Notify `@testers` in [#testing].
- [ ] Test build: [Deploy to Staging] with release tag.
- [ ] Notify a deployer to deploy it
## 3. Deployment at beginning of week
## 3. Finish on Tuesday
- [ ] Publish and notify [#global-community] (this is automatically posted with a plugin)
- [ ] Deploy the new release to all managed instances.
@@ -42,19 +39,17 @@ assignees: ''
</details>
- [ ] Notify [#instance-managers]:
> @instance_managers The new release has been deployed.
- [ ] [Create issue] for next release and confirm with next release drafter in [#delivery-circle].
- [ ] [Create issue] for next release and confirm with next release manager in [#core-devs].
The full process is described at https://github.com/openfoodfoundation/openfoodnetwork/wiki/Releasing.
[Ready To Go]: https://github.com/orgs/openfoodfoundation/projects/8?filterQuery=status%3A%22Ready+to+go+%F0%9F%9A%80%22
[Transifex pull request]: https://github.com/openfoodfoundation/openfoodnetwork/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aopen+head%3Atransifex
[Draft new release]: https://github.com/openfoodfoundation/openfoodnetwork/releases/new?title=v+Code+Name&body=Congrats%0A%0ADescription%0A%0A
[Draft new release]: https://github.com/openfoodfoundation/openfoodnetwork/releases/new?tag=v&title=v+Code+Name&body=Congrats%0A%0ADescription%0A%0A
[releases]: https://github.com/openfoodfoundation/openfoodnetwork/releases
[#instance-managers]: https://app.slack.com/client/T02G54U79/CG7NJ966B
[#testing]: https://openfoodnetwork.slack.com/app_redirect?channel=C02TZ6X00
[Deploy to Staging]: https://github.com/openfoodfoundation/openfoodnetwork/actions/workflows/stage.yml
[#global-community]: https://app.slack.com/client/T02G54U79/C59ADD8F2
[Create issue]: https://github.com/openfoodfoundation/openfoodnetwork/issues/new?assignees=&labels=&projects=&template=release.md&title=Release
[#delivery-circle]: https://openfoodnetwork.slack.com/archives/C01T75H6G0Z
[Transifex Client]: https://developers.transifex.com/docs/cli
[minor or major breaking changes]: https://github.com/openfoodfoundation/openfoodnetwork/pulls?q=label%3A%22breaking+change%22%2C%22major+breaking+change%22
[#core-devs]: https://openfoodnetwork.slack.com/archives/GK2T38QPJ

View File

@@ -1,4 +1,4 @@
## What? Why?
#### What? Why?
- Closes # <!-- Insert issue number here. -->
@@ -7,7 +7,7 @@
## What should we test?
#### What should we test?
<!-- List which features should be tested and how.
This can be similar to the Steps to Reproduce in the issue.
Also think of other parts of the app which could be affected
@@ -16,7 +16,7 @@
- Visit ... page.
-
## Release notes
#### Release notes
<!-- Please select one for your PR and delete the other. -->
@@ -33,12 +33,12 @@ Changelog Category (reviewers may add a label for the release notes):
The title of the pull request will be included in the release notes.
## Dependencies
#### Dependencies
<!-- Does this PR depend on another one?
Add the link or remove this section. -->
## Documentation updates
#### Documentation updates
<!-- Are there any wiki pages that need updating after merging this PR?
List them here or remove this section. -->

View File

@@ -1,37 +1,11 @@
# Dependabot configuration
#
# The `directory` and `schedule.interval` options are mandatory.
# Most of the configuration here is not used for security updates though.
version: 2
multi-ecosystem-groups:
turbo_power:
schedule:
interval: "daily"
updates:
- package-ecosystem: "bundler"
directory: "/"
patterns: ["turbo_power"]
multi-ecosystem-group: "turbo_power"
# Only specific requirements are specified in Gemfile, so don't touch it.
versioning-strategy: lockfile-only
- package-ecosystem: "npm"
directory: "/"
patterns: ["turbo_power"]
multi-ecosystem-group: "turbo_power"
# Only specific requirements are specified in package.json, so don't touch it.
versioning-strategy: lockfile-only
- package-ecosystem: "bundler"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 10
# Only specific requirements are specified in Gemfile, so don't touch it.
versioning-strategy: lockfile-only
@@ -39,6 +13,5 @@ updates:
directory: "/"
schedule:
interval: "daily"
# Only specific requirements are specified in package.json, so don't touch it.
versioning-strategy: lockfile-only
# All versions are specified in package.json, so please update them.
versioning-strategy: increase

6
.github/release.yml vendored
View File

@@ -14,12 +14,6 @@ changelog:
- technical changes only
- user facing changes
# These will require a minor or major version increment
- title: "Significant changes 🚀"
labels:
- breaking change
- major breaking change
# Posted in advance for #instance-managers
- title: "User-facing changes 👀"
labels:

View File

@@ -1,15 +0,0 @@
{
"pull_request": {
"number": 13545,
"title": "Bump test from 7.0.4 to 7.0.8",
"user": {
"login": "dependabot[bot]"
}
},
"repository": {
"owner": {
"login": "openfoodfoundation"
},
"name": "openfoodnetwork"
}
}

View File

@@ -1,14 +0,0 @@
name: Auto Author Assign
on:
pull_request_target:
types: [opened, reopened]
permissions:
pull-requests: write
jobs:
assign-author:
runs-on: ubuntu-latest
steps:
- uses: toshimaru/auto-author-assign@v2.1.0

51
.github/workflows/brakeman-analysis.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
# This workflow integrates Brakeman with GitHub's Code Scanning feature
# Brakeman is a static analysis security vulnerability scanner for Ruby on Rails applications
name: Brakeman Scan
# This section configures the trigger for the workflow. Feel free to customize depending on your convention
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
permissions:
contents: read
jobs:
brakeman-scan:
permissions:
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
name: Brakeman Scan
runs-on: ubuntu-latest
steps:
# Checkout the repository to the GitHub Actions runner
- name: Checkout
uses: actions/checkout@v3
# Customize the ruby version depending on your needs
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '2.7'
- name: Setup Brakeman
env:
BRAKEMAN_VERSION: '5.4.0'
run: |
gem install brakeman --version $BRAKEMAN_VERSION
# Execute Brakeman CLI and generate a SARIF output with the security issues identified during the analysis
- name: Scan
continue-on-error: true
run: |
git show --no-patch # the commit being tested (which is often a merge due to actions/checkout@v3)
brakeman -f sarif -o output.sarif.json .
# Upload the SARIF file generated in the previous step
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: output.sarif.json

View File

@@ -3,7 +3,7 @@ name: Build
on:
workflow_dispatch:
push:
branches-ignore:
branches-ignore:
- 'dependabot/**'
pull_request:
@@ -17,7 +17,7 @@ permissions:
contents: read
jobs:
controllers_and_models:
controllers:
runs-on: ubuntu-22.04
services:
postgres:
@@ -38,16 +38,16 @@ jobs:
# [n] - where the n is a number of parallel jobs you want to run your tests on.
# Use a higher number if you have slow tests to split them between more parallel jobs.
# Remember to update the value of the `ci_node_index` below to (0..n-1).
ci_node_total: [4]
ci_node_total: [8]
# Indexes for parallel jobs (starting from zero).
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
ci_node_index: [0, 1, 2, 3]
ci_node_index: [0, 1, 2, 3, 4, 5, 6, 7]
steps:
- uses: actions/checkout@v3
- name: Setup redis
uses: supercharge/redis-github-action@1.4.0
with:
with:
redis-version: 6
- name: Set up Ruby
@@ -55,18 +55,17 @@ jobs:
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
# JS is required in order for webpacker to compile, in order to render templates containing image urls
- uses: actions/setup-node@v3
with:
node-version-file: .node-version
cache: yarn
- name: Install JS dependencies
run: yarn install --frozen-lockfile
- name: Set up database
run: |
bin/rails db:create db:schema:load
bundle exec rake db:create
bundle exec rake db:schema:load
- name: Run tests
env:
@@ -81,21 +80,12 @@ jobs:
# RSpec split test files by test examples feature - it's optional
# https://knapsackpro.com/faq/question/how-to-split-slow-rspec-test-files-by-test-examples-by-individual-it
#KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/controllers/**/*_spec.rb,spec/models/**/*_spec.rb}"
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/controllers/**/*_spec.rb}"
run: |
git show --no-patch # the commit being tested (which is often a merge due to actions/checkout@v3)
bin/rails assets:precompile knapsack_pro:rspec
bundle exec rake knapsack_pro:rspec
- name: Save SimpleCov file
uses: actions/upload-artifact@v4
with:
name: simplecov-chunk-controllers-${{ matrix.ci_node_index }}
path: coverage/*.*
retention-days: 2 # doesn't need to be long, because it's the combined results that matter
if-no-files-found: ignore
include-hidden-files: true
system:
models:
runs-on: ubuntu-22.04
services:
postgres:
@@ -116,16 +106,16 @@ jobs:
# [n] - where the n is a number of parallel jobs you want to run your tests on.
# Use a higher number if you have slow tests to split them between more parallel jobs.
# Remember to update the value of the `ci_node_index` below to (0..n-1).
ci_node_total: [19]
ci_node_total: [5]
# Indexes for parallel jobs (starting from zero).
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
ci_node_index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
ci_node_index: [0, 1, 2, 3, 4]
steps:
- uses: actions/checkout@v3
- name: Setup redis
uses: supercharge/redis-github-action@1.4.0
with:
with:
redis-version: 6
- name: Set up Ruby
@@ -136,14 +126,83 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version-file: .node-version
cache: yarn
- name: Install JS dependencies
run: yarn install --frozen-lockfile
- name: Set up database
run: |
bin/rails db:create db:schema:load
bundle exec rake db:create
bundle exec rake db:schema:load
- name: Run tests
env:
KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC: 09476e2ce491c12083df62768667c674
KNAPSACK_PRO_CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
KNAPSACK_PRO_CI_NODE_INDEX: ${{ matrix.ci_node_index }}
KNAPSACK_PRO_LOG_LEVEL: info
# if you use Knapsack Pro Queue Mode you must set below env variable
# to be able to retry CI build and run previously recorded tests
# https://github.com/KnapsackPro/knapsack_pro-ruby#knapsack_pro_fixed_queue_split-remember-queue-split-on-retry-ci-node
# KNAPSACK_PRO_FIXED_QUEUE_SPLIT: false
# RSpec split test files by test examples feature - it's optional
# https://knapsackpro.com/faq/question/how-to-split-slow-rspec-test-files-by-test-examples-by-individual-it
#KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/models/**/*_spec.rb}"
run: |
bundle exec rake knapsack_pro:rspec
system_admin:
runs-on: ubuntu-22.04
services:
postgres:
image: postgres:10
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
POSTGRES_DB: open_food_network_test
POSTGRES_USER: ofn
POSTGRES_PASSWORD: f00d
strategy:
fail-fast: false
matrix:
# [n] - where the n is a number of parallel jobs you want to run your tests on.
# Use a higher number if you have slow tests to split them between more parallel jobs.
# Remember to update the value of the `ci_node_index` below to (0..n-1).
ci_node_total: [13]
# Indexes for parallel jobs (starting from zero).
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
ci_node_index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
steps:
- uses: actions/checkout@v3
- name: Setup redis
uses: supercharge/redis-github-action@1.4.0
with:
redis-version: 6
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- uses: actions/setup-node@v3
with:
node-version-file: .node-version
- name: Install JS dependencies
run: yarn install --frozen-lockfile
- name: Set up database
run: |
bundle exec rake db:create
bundle exec rake db:schema:load
- name: Run tests
@@ -159,25 +218,94 @@ jobs:
# RSpec split test files by test examples feature - it's optional
# https://knapsackpro.com/faq/question/how-to-split-slow-rspec-test-files-by-test-examples-by-individual-it
#KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/system/admin/**/*_spec.rb,spec/system/consumer/**/*_spec.rb}"
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/system/admin/**/*_spec.rb}"
run: |
bin/rails assets:precompile knapsack_pro:queue:rspec
- name: Save SimpleCov file
uses: actions/upload-artifact@v4
with:
name: simplecov-chunk-system-${{ matrix.ci_node_index }}
path: coverage/*.*
retention-days: 2 # doesn't need to be long, because it's the combined results that matter
if-no-files-found: ignore
include-hidden-files: true
bundle exec rake knapsack_pro:queue:rspec
- name: Archive failed tests screenshots
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: failed-system_${{ matrix.ci_node_index }}-tests-screenshots
name: failed-tests-screenshots
path: tmp/capybara/screenshots/*.png
retention-days: 7
if-no-files-found: ignore
system_consumer:
runs-on: ubuntu-22.04
services:
postgres:
image: postgres:10
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
POSTGRES_DB: open_food_network_test
POSTGRES_USER: ofn
POSTGRES_PASSWORD: f00d
strategy:
fail-fast: false
matrix:
# [n] - where the n is a number of parallel jobs you want to run your tests on.
# Use a higher number if you have slow tests to split them between more parallel jobs.
# Remember to update the value of the `ci_node_index` below to (0..n-1).
ci_node_total: [12]
# Indexes for parallel jobs (starting from zero).
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
ci_node_index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
steps:
- uses: actions/checkout@v3
- name: Setup redis
uses: supercharge/redis-github-action@1.4.0
with:
redis-version: 6
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- uses: actions/setup-node@v3
with:
node-version-file: .node-version
- name: Install JS dependencies
run: yarn install --frozen-lockfile
- name: Set up database
run: |
bundle exec rake db:create
bundle exec rake db:schema:load
- name: Run tests
env:
KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC: e52bd4390c853e6c5bdfe4d0334586c1
KNAPSACK_PRO_CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
KNAPSACK_PRO_CI_NODE_INDEX: ${{ matrix.ci_node_index }}
KNAPSACK_PRO_LOG_LEVEL: info
# if you use Knapsack Pro Queue Mode you must set below env variable
# to be able to retry CI build and run previously recorded tests
# https://github.com/KnapsackPro/knapsack_pro-ruby#knapsack_pro_fixed_queue_split-remember-queue-split-on-retry-ci-node
KNAPSACK_PRO_FIXED_QUEUE_SPLIT: true
# RSpec split test files by test examples feature - it's optional
# https://knapsackpro.com/faq/question/how-to-split-slow-rspec-test-files-by-test-examples-by-individual-it
#KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/system/consumer/**/*_spec.rb}"
run: |
bundle exec rake knapsack_pro:queue:rspec
- name: Archive failed tests screenshots
if: failure()
uses: actions/upload-artifact@v3
with:
name: failed-tests-screenshots
path: tmp/capybara/screenshots/*.png
retention-days: 7
if-no-files-found: ignore
@@ -212,7 +340,7 @@ jobs:
- name: Setup redis
uses: supercharge/redis-github-action@1.4.0
with:
with:
redis-version: 6
- name: Set up Ruby
@@ -220,18 +348,17 @@ jobs:
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
# JS is required in order for webpacker to compile, in order to render templates linking to mail.css
- uses: actions/setup-node@v3
with:
node-version-file: .node-version
cache: yarn
- name: Install JS dependencies
run: yarn install --frozen-lockfile
- name: Set up database
run: |
bin/rails db:create db:schema:load
bundle exec rake db:create
bundle exec rake db:schema:load
- name: Run tests
@@ -250,16 +377,16 @@ jobs:
KNAPSACK_PRO_TEST_FILE_PATTERN: "{spec/lib/**/*_spec.rb,spec/migrations/**/*_spec.rb,spec/serializers/**/*_spec.rb,engines/**/*_spec.rb}"
run: |
bin/rails assets:precompile knapsack_pro:rspec
bundle exec rake knapsack_pro:rspec
- name: Save SimpleCov file
uses: actions/upload-artifact@v4
- name: Archive failed tests screenshots
if: failure()
uses: actions/upload-artifact@v3
with:
name: simplecov-chunk-engines-${{ matrix.ci_node_index }}
path: coverage/*.*
retention-days: 2 # doesn't need to be long, because it's the combined results that matter
name: failed-tests-screenshots
path: tmp/capybara/screenshots/*.png
retention-days: 7
if-no-files-found: ignore
include-hidden-files: true
test_the_rest:
runs-on: ubuntu-22.04
@@ -282,16 +409,16 @@ jobs:
# [n] - where the n is a number of parallel jobs you want to run your tests on.
# Use a higher number if you have slow tests to split them between more parallel jobs.
# Remember to update the value of the `ci_node_index` below to (0..n-1).
ci_node_total: [3]
ci_node_total: [5]
# Indexes for parallel jobs (starting from zero).
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
ci_node_index: [0, 1, 2]
ci_node_index: [0, 1, 2, 3, 4]
steps:
- uses: actions/checkout@v3
- name: Setup redis
uses: supercharge/redis-github-action@1.4.0
with:
with:
redis-version: 6
- name: Set up Ruby
@@ -299,20 +426,20 @@ jobs:
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
# JS is required in order for webpacker to compile, in order to render templates linking to mail.css
- uses: actions/setup-node@v3
with:
node-version-file: .node-version
cache: yarn
- name: Install JS dependencies
run: yarn install --frozen-lockfile
- name: Set up database
run: |
bin/rails db:create db:schema:load
bundle exec rake db:create
bundle exec rake db:schema:load
- name: Run tests
env:
KNAPSACK_PRO_TEST_SUITE_TOKEN_RSPEC: e3b8800198d2d89b70c7edbdd85f8fd8
KNAPSACK_PRO_CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
@@ -326,17 +453,10 @@ jobs:
# https://knapsackpro.com/faq/question/how-to-split-slow-rspec-test-files-by-test-examples-by-individual-it
#KNAPSACK_PRO_RSPEC_SPLIT_BY_TEST_EXAMPLES: true
KNAPSACK_PRO_TEST_FILE_EXCLUDE_PATTERN: "{engines/**/*_spec.rb,spec/models/**/*_spec.rb,spec/controllers/**/*_spec.rb,spec/serializers/**/*_spec.rb,spec/lib/**/*_spec.rb,spec/migrations/**/*_spec.rb,spec/system/**/*_spec.rb}"
run: |
bin/rails assets:precompile knapsack_pro:rspec
- name: Save SimpleCov file
uses: actions/upload-artifact@v4
with:
name: simplecov-chunk-the-rest-${{ matrix.ci_node_index }}
path: coverage/*.*
retention-days: 2 # doesn't need to be long, because it's the combined results that matter
if-no-files-found: ignore
include-hidden-files: true
bundle exec rake knapsack_pro:rspec
non_knapsack_jest_karma:
runs-on: ubuntu-22.04
@@ -356,7 +476,11 @@ jobs:
steps:
- uses: actions/checkout@v3
# Rails is required for the Karma rake script
- name: Setup redis
uses: supercharge/redis-github-action@1.4.0
with:
redis-version: 6
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
@@ -365,51 +489,16 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version-file: .node-version
cache: yarn
- name: Install JS dependencies
run: yarn install --frozen-lockfile
- name: Set up database
run: |
bundle exec rake db:create
bundle exec rake db:schema:load
- name: Run JS tests
run: bin/rake karma:run
run: bundle exec rake karma:run
- name: Run jest tests
run: yarn jest
collate_simplecov_results:
runs-on: ubuntu-22.04
needs:
- controllers_and_models
- engines
- system
- test_the_rest
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- name: Download individual results from individual runners
uses: actions/download-artifact@v4
with:
pattern: simplecov-chunk-*
path: tmp/simplecov
- name: collate results from each of the workers
run: bundle exec rake 'simplecov:collate_results[tmp/simplecov]'
- name: Upload collated results
uses: actions/upload-artifact@v4
with:
name: combined-simplecov-report
path: coverage/**/*.*
retention-days: 7
if-no-files-found: ignore
include-hidden-files: true
- name: Compare SimpleCov results with Undercover
run: |
git fetch --no-tags origin ${{ github.event.pull_request.base.ref }}:master
bundle exec undercover
if: ${{ github.ref != 'refs/heads/master' }} # Does not run on master, as we can't fetch master in the master branch

View File

@@ -1,30 +1,47 @@
name: Linters
on: [pull_request]
on: [push, pull_request]
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
lint:
name: reviewdog
rubocop:
name: runner / rubocop
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Check out code
uses: actions/checkout@v1
- uses: ruby/setup-ruby@v1
- run: git show --no-patch # the commit being tested (which is often a merge due to actions/checkout@v3)
- name: rubocop
uses: reviewdog/action-rubocop@v2
with:
rubocop_version: gemfile
rubocop_extensions: rubocop-rails:gemfile rubocop-rspec:gemfile
reporter: github-pr-check
level: error
fail_on_error: true
prettier:
name: runner / prettier
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version-file: .node-version
- name: Install JS dependencies
run: yarn install --frozen-lockfile
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- run: git show --no-patch # the commit being tested (which is often a merge due to actions/checkout@v3)
- uses: reviewdog/action-setup@v1
- name: prettier
uses: EPMatt/reviewdog-action-prettier@v1
with:
reviewdog_version: v0.21.0
- run: ./script/reviewdog.sh
env:
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.github_token }}
github_token: ${{ secrets.github_token }}
reporter: github-pr-check
level: error
fail_on_error: true

51
.github/workflows/mapi.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: 'Mayhem for API'
on: workflow_dispatch
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
test:
permissions:
contents: read # to fetch code (actions/checkout)
security-events: write # to upload SARIF results (github/codeql-action/upload-sarif)
if: ${{ github.repository_owner == 'openfoodfoundation' }}
runs-on: ubuntu-latest
strategy:
fail-fast: true
steps:
- uses: actions/checkout@v3
- run: docker/build
- run: docker-compose up --detach
- run: until curl -f -s http://localhost:3000; do echo "waiting for api server"; sleep 1; done
- run: docker-compose exec -T db psql postgresql://ofn:f00d@localhost:5432/open_food_network_dev --command="update spree_users set spree_api_key='testing' where login='ofn@example.com'"
# equivalent to Flipper.enable(:api_v1)
- run: docker-compose exec -T db psql postgresql://ofn:f00d@localhost:5432/open_food_network_dev --command="insert into flipper_features (key, created_at, updated_at) values ('api_v1', localtimestamp, localtimestamp)"
- run: docker-compose exec -T db psql postgresql://ofn:f00d@localhost:5432/open_food_network_dev --command="insert into flipper_gates (feature_key, key, value, created_at, updated_at) values ('api_v1', 'boolean', 'true', localtimestamp, localtimestamp)"
# Run Mayhem for API
- name: Run Mayhem for API
uses: ForAllSecure/mapi-action@v1
continue-on-error: true
with:
mapi-token: ${{ secrets.MAPI_TOKEN }}
api-url: http://localhost:3000
api-spec: swagger/v1.yaml
target: openfoodfoundation/openfoodnetwork
duration: 1min
sarif-report: mapi.sarif
html-report: mapi.html
run-args: |
--header-auth
X-Api-Token: testing
# Archive HTML report
- name: Archive Mayhem for API report
uses: actions/upload-artifact@v3
with:
name: mapi-report
path: mapi.html
# Upload SARIF file (only available on public repos or github enterprise)
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: mapi.sarif

View File

@@ -1,151 +0,0 @@
name: Auto-move Dependabot PRs to Code Review
permissions:
contents: read
pull-requests: read
on:
pull_request_target:
types: [opened]
jobs:
move-pr-to-code-review:
runs-on: ubuntu-latest
if: github.event.pull_request.user.login == 'dependabot[bot]' || startsWith(github.event.pull_request.title, 'Bump')
steps:
- name: Generate GitHub App Token
id: app-token
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.DEPENDABOT_PR_APP_ID }}
private_key: ${{ secrets.DEPENDABOT_PR_APP_PRIVATE_KEY }}
installation_retrieval_mode: id
installation_retrieval_payload: ${{ secrets.DEPENDABOT_PR_APP_INSTALLATION_ID }}
- name: Move PR to Code Review in Project v2
uses: actions/github-script@v7
with:
github-token: ${{ steps.app-token.outputs.token }}
script: |
const projectNumber = 8; // for "OFN Delivery board"
const org = "openfoodfoundation";
const repo = context.repo.repo;
const prNumber = context.payload.pull_request.number;
const statusFieldName = "Status";
const statusValue = "Code review 🔎";
// ---- Helper: Get PR Node ID ----
async function getPrNodeId(owner, repo, number) {
const res = await github.graphql(`
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
id
number
title
}
}
}
`, { owner, repo, number });
return res.repository.pullRequest.id;
}
console.log("🚀 Starting ProjectV2 automation...");
// ---- Step 1: Get Project and Fields ----
const projectRes = await github.graphql(`
query($org: String!, $number: Int!) {
organization(login: $org) {
projectV2(number: $number) {
id
title
fields(first: 50) {
nodes {
... on ProjectV2Field {
id
name
}
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
}
}
}
}
}
`, { org, number: projectNumber });
const project = projectRes.organization.projectV2;
if (!project) throw new Error(`❌ Project #${projectNumber} not found`);
console.log(`✅ Found project: ${project.title} (${project.id})`);
const statusField = project.fields.nodes.find(f => f.name === statusFieldName);
if (!statusField) throw new Error(`❌ Field '${statusFieldName}' not found`);
const option = statusField.options.find(o => o.name === statusValue);
if (!option) throw new Error(`❌ Option '${statusValue}' not found in '${statusFieldName}'`);
console.log(`✅ Found field '${statusFieldName}' and option '${statusValue}'`);
// ---- Step 2: Get PR Node ID ----
const prNodeId = await getPrNodeId(org, repo, prNumber);
console.log(`✅ PR #${prNumber} node ID: ${prNodeId}`);
// ---- Step 3: Check if PR is already in Project ----
const itemRes = await github.graphql(`
query($prId: ID!) {
node(id: $prId) {
... on PullRequest {
projectItems(first: 50) {
nodes {
id
project { id title }
}
}
}
}
}
`, { prId: prNodeId });
let projectItem = itemRes.node.projectItems.nodes.find(i => i.project.id === project.id);
if (!projectItem) {
console.log(" PR not yet in project, adding...");
const addRes = await github.graphql(`
mutation($projectId: ID!, $contentId: ID!) {
addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
item { id }
}
}
`, { projectId: project.id, contentId: prNodeId });
projectItem = addRes.addProjectV2ItemById.item;
console.log(`✅ Added PR to project: ${projectItem.id}`);
} else {
console.log(` PR already in project: ${projectItem.id}`);
}
// ---- Step 4: Update Status ----
await github.graphql(`
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $projectId,
itemId: $itemId,
fieldId: $fieldId,
value: { singleSelectOptionId: $optionId }
}) {
projectV2Item { id }
}
}
`, {
projectId: project.id,
itemId: projectItem.id,
fieldId: statusField.id,
optionId: option.id,
});
console.log(`🎉 Moved PR #${prNumber} → '${statusValue}'`);

1
.gitignore vendored
View File

@@ -59,4 +59,3 @@ yarn-debug.log*
/config/credentials.yml.enc
/config/master.key
.secrets

View File

@@ -2,12 +2,19 @@
# frameworks such as Jekyll/Middleman
skip_frontmatter: false
inherits_from: .haml-lint_todo.yml
linters:
AltText:
enabled: false
ClassAttributeWithStaticValue:
enabled: true
ClassesBeforeIds:
enabled: true
ConsecutiveComments:
enabled: true
ConsecutiveSilentScripts:
enabled: true
max_consecutive: 2
@@ -25,6 +32,7 @@ linters:
enabled: true
LineLength:
enabled: true
max: 80
MultilinePipe:
@@ -39,11 +47,24 @@ linters:
RuboCop:
enabled: false
RubyComments:
enabled: true
SpaceBeforeScript:
enabled: true
SpaceInsideHashAttributes:
enabled: true
style: no_space
TagName:
enabled: true
TrailingWhitespace:
enabled: true
UnnecessaryInterpolation:
enabled: true
UnnecessaryStringOutput:
enabled: true

View File

@@ -1,153 +0,0 @@
# This configuration was generated by
# `haml-lint --auto-gen-config`
# on 2025-10-30 09:19:50 +0100 using Haml-Lint version 0.66.0.
# The point is for the user to remove these configuration records
# one by one as the lints are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of Haml-Lint, may require this file to be generated again.
linters:
# Offense count: 35
ClassAttributeWithStaticValue:
enabled: false
# Offense count: 77
ClassesBeforeIds:
enabled: false
# Offense count: 18
ConsecutiveComments:
enabled: false
# Offense count: 22
ConsecutiveSilentScripts:
exclude:
- "app/views/admin/contents/_fieldset.html.haml"
- "app/views/admin/enterprises/form/_tag_rules.html.haml"
- "app/views/admin/order_cycles/edit.html.haml"
- "app/views/admin/products_v3/product_preview.turbo_stream.haml"
- "app/views/admin/reports/_date_range_form.html.haml"
- "app/views/checkout/_details.html.haml"
- "app/views/checkout/_payment.html.haml"
- "app/views/spree/admin/adjustments/_adjustments_table.html.haml"
- "app/views/spree/admin/orders/customer_details/_address_form.html.haml"
- "app/views/spree/admin/tax_categories/index.html.haml"
- "app/views/spree/admin/users/index.html.haml"
# Offense count: 14
FinalNewline:
exclude:
- "app/assets/javascripts/templates/shared/question_mark_with_tooltip.html.haml"
- "app/views/admin/enterprises/form/_social.html.haml"
- "app/views/admin/json/_injection_ams.html.haml"
- "app/views/admin/order_cycles/_date_time_warning_modal_content.html.haml"
- "app/views/admin/order_cycles/edit.html.haml"
- "app/views/admin/product_import/_ams_data.html.haml"
- "app/views/admin/reports/_row_group.haml"
- "app/views/admin/reports/filters/_enterprise_fee_summary.html.haml"
- "app/views/admin/reports/filters/_users_and_enterprises.html.haml"
- "app/views/shop/_blocked_cookies.html.haml"
- "app/views/spree/admin/orders/_invoice/_order_note.html.haml"
- "app/views/spree/admin/orders/invoice4.html.haml"
- "app/views/spree/admin/taxons/destroy_taxon.turbo_stream.haml"
- "app/views/spree/admin/users/_email_confirmation.html.haml"
# Offense count: 130
IdNames:
enabled: false
# Offense count: 5
Indentation:
exclude:
- "app/views/admin/products_v3/clone.turbo_stream.haml"
- "app/views/admin/products_v3/destroy_product_variant.turbo_stream.haml"
- "app/views/spree/admin/taxons/destroy_taxon.turbo_stream.haml"
# Offense count: 191
InlineStyles:
enabled: false
# Offense count: 589
InstanceVariables:
enabled: false
# Offense count: 2
LeadingCommentSpace:
exclude:
- "app/views/admin/reports/_row_group.haml"
# Offense count: 2331
LineLength:
enabled: false
# Offense count: 1
MultilinePipe:
exclude:
- "app/views/admin/reports/_rendering_options.html.haml"
# Offense count: 2
MultilineScript:
exclude:
- "app/views/admin/products_v3/product_preview.turbo_stream.haml"
- "app/views/checkout/_voucher_section.html.haml"
# Offense count: 2
RepeatedId:
exclude:
- "app/assets/javascripts/templates/admin/save_bar.html.haml"
# Offense count: 24
RubyComments:
enabled: false
# Offense count: 104
SpaceBeforeScript:
enabled: false
# Offense count: 3345
SpaceInsideHashAttributes:
enabled: false
# Offense count: 22
TrailingEmptyLines:
enabled: false
# Offense count: 73
TrailingWhitespace:
enabled: false
# Offense count: 13
UnnecessaryInterpolation:
exclude:
- "app/components/example_component/example_component.html.haml"
- "app/views/admin/product_import/_entries_table.html.haml"
- "app/views/admin/product_import/import.html.haml"
- "app/views/admin/variant_overrides/_filters.html.haml"
- "app/views/registration/steps/_introduction.html.haml"
- "app/views/spree/order_mailer/_shipping.html.haml"
- "app/views/spree/order_mailer/invoice_email.html.haml"
- "app/views/spree/shared/_shipment_delivery_details.html.haml"
- "app/views/spree/shared/_shipment_pickup_details.html.haml"
# Offense count: 68
UnnecessaryStringOutput:
enabled: false
# Offense count: 14
ViewLength:
exclude:
- "app/assets/javascripts/templates/admin/panels/enterprise_package.html.haml"
- "app/views/admin/customers/index.html.haml"
- "app/views/admin/enterprises/_new_form.html.haml"
- "app/views/admin/enterprises/form/_shop_preferences.html.haml"
- "app/views/admin/product_import/_import_review.html.haml"
- "app/views/admin/products_v3/product_preview.turbo_stream.haml"
- "app/views/checkout/_details.html.haml"
- "app/views/groups/show.html.haml"
- "app/views/producer_mailer/order_cycle_report.html.haml"
- "app/views/shared/_footer.html.haml"
- "app/views/spree/admin/orders/bulk_management.html.haml"
- "app/views/spree/admin/orders/invoice4.html.haml"
- "app/views/spree/admin/products/new.html.haml"
- "app/views/spree/admin/variants/_form.html.haml"

6
.hound.yml Normal file
View File

@@ -0,0 +1,6 @@
rubocop:
config_file: .rubocop_styleguide.yml
scss:
config_file: .scss-lint.yml
haml:
config_file: .haml-lint.yml

View File

@@ -1 +1 @@
24.10.0
17.9.1

View File

@@ -5,7 +5,6 @@
*.yaml
*.json
*.html
**/*.rb
# JS
# Enabled: app/webpacker/controllers/*.js and app/webpacker/packs/*.js
@@ -28,5 +27,6 @@ postcss.config.js
/coverage/
/engines/
/public/
/spec/
/tmp/
/vendor/

1
.rspec
View File

@@ -1 +0,0 @@
--require base_spec_helper

View File

@@ -4,13 +4,9 @@
#
# The configuration is split into three files. Look into those files for more details.
#
plugins:
- rubocop-capybara
- rubocop-factory_bot
require:
- rubocop-rails
- rubocop-rspec
- rubocop-rspec_rails
inherit_from:
# The automatically generated todo list to ignore all current violations.

View File

@@ -3,20 +3,16 @@
# These are the rules we agreed upon and we work towards.
AllCops:
NewCops: enable
MigratedSchemaVersion: "20250111000000"
SuggestExtensions: false
Exclude:
- bin/**/*
- db/**/*
- config/**/*
- db/bad_migrations/*
- db/migrate/201*
- db/migrate/202[0-4]*
- db/schema.rb
- script/**/*
- vendor/**/*
- node_modules/**/*
# Excluding: inadequate Naming/FileName rule rejects GemFile name with camelcase
- engines/web/Gemfile
- .undercover
Bundler/DuplicatedGem:
Enabled: false
@@ -94,7 +90,7 @@ Metrics/PerceivedComplexity:
Enabled: true
Max: 14 # default 8
Naming/PredicatePrefix:
Naming/PredicateName:
Enabled: false
Naming/VariableNumber:
@@ -103,8 +99,6 @@ Naming/VariableNumber:
- street_address_2
AllowedPatterns:
- _v[\d]+
# Cf. conversation https://github.com/openfoodfoundation/openfoodnetwork/pull/13306#pullrequestreview-2831644286
- menu_[\d]
Rails/ApplicationRecord:
Exclude:
@@ -118,19 +112,10 @@ Rails/ApplicationRecord:
Rails/HasAndBelongsToMany:
Enabled: false
# Cf. conversation https://github.com/openfoodfoundation/openfoodnetwork/pull/13251
Rails/LexicallyScopedActionFilter:
Enabled: false
Rails/OutputSafety:
Exclude:
- spec/**/*
Rails/RedundantActiveRecordAllMethod:
AllowedReceivers:
- ActionMailer::Preview
- ActiveSupport::TimeZone
Rails/SkipsModelValidations:
AllowedMethods:
- touch
@@ -140,16 +125,6 @@ Rails/SkipsModelValidations:
- update_column
- update_columns
Rails/UnknownEnv:
Environments:
- development
- production
- staging
- test
Rails/WhereExists:
EnforcedStyle: where # Cf. conversion https://github.com/openfoodfoundation/openfoodnetwork/pull/12363
Style/Documentation:
Enabled: false

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
3.3.10
3.1.4

19
.scss-lint.yml Normal file
View File

@@ -0,0 +1,19 @@
scss_files: 'app/assets/stylesheets/**/*.css.scss'
exclude: 'app/assets/stylesheets/shared/**'
linters:
ImportantRule:
enabled: false
VendorPrefix:
enabled: false
LeadingZero:
enabled: false
PropertySortOrder:
enabled: false
StringQuotes:
enabled: false
DeclarationOrder:
enabled: false
NestingDepth:
enabled: false

View File

@@ -1,4 +0,0 @@
# .secrets file define github secrets value locally
DEPENDABOT_PR_APP_ID=123456
DEPENDABOT_PR_APP_INSTALLATION_ID=123456
DEPENDABOT_PR_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n....\n-----END RSA PRIVATE KEY-----"

View File

@@ -1,18 +1,17 @@
#!/bin/env ruby
# frozen_string_literal: true
SimpleCov.start 'rails' do
# The rails profile contains some filters already:
#
# - "/test/"
# - "/features/"
# - "/spec/"
# - "/autotest/"
# - /^\/config\//
# - /^\/db\//
add_filter '/bin/'
add_filter '/config/' # to include engine config
add_filter '/config/'
add_filter '/jobs/application_job.rb'
add_filter '/schemas/'
add_filter '/lib/generators'
add_filter '/spec/'
add_filter '/vendor/'
add_filter '/public'
add_filter '/swagger'
add_filter '/script'
formatter SimpleCov::Formatter::SimpleFormatter
add_filter '/log'
add_filter '/db'
add_filter '/lib/tasks/sample_data/'
end

View File

@@ -1,9 +0,0 @@
#!/bin/env ruby
# frozen_string_literal: true
--compare master
# This shouldn't be needed in undercover > 0.7.4
#
# * https://github.com/grodowski/undercover/issues/233
--exclude-files "bin/*,db/*,config/*,spec/*,engines/*/config/*,engines/*/spec/*"

View File

@@ -1,34 +1,92 @@
FROM ruby:3.3.10-alpine3.19 AS base
ENV LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
TZ=Europe/London \
RAILS_ROOT=/usr/src/app \
BUNDLE_PATH=/bundles \
BUNDLE_APP_CONFIG=/bundles
RUN apk --no-cache upgrade && \
apk add --no-cache tzdata postgresql-client imagemagick imagemagick-jpeg && \
apk add --no-cache --virtual wkhtmltopdf
FROM ubuntu:20.04
WORKDIR $RAILS_ROOT
ENV TZ Europe/London
# Development dependencies
FROM base AS development-base
RUN apk add --no-cache --virtual .build-deps \
build-base postgresql-dev git nodejs yarn && \
apk add --no-cache --virtual .dev-utils \
bash curl less vim chromium-chromedriver zlib-dev openssl-dev cmake\
readline-dev yaml-dev sqlite-dev libxml2-dev libxslt-dev libffi-dev vips-dev && \
curl -o /usr/local/bin/wait-for-it https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh && \
chmod +x /usr/local/bin/wait-for-it
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# Install yarn dependencies separately for caching
FROM development-base AS yarn-dependencies
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
RUN echo "deb http://security.ubuntu.com/ubuntu bionic-security main" >> /etc/apt/sources.list
# Install Ruby gems
FROM development-base
COPY . $RAILS_ROOT
COPY Gemfile Gemfile.lock ./
RUN bundle install --jobs "$(nproc)"
COPY --from=yarn-dependencies $RAILS_ROOT/node_modules ./node_modules
# Install all the requirements
RUN apt-get update && apt-get install -y \
curl \
git \
build-essential \
software-properties-common \
wget \
zlib1g-dev \
libreadline-dev \
libyaml-dev \
libffi-dev \
libxml2-dev \
libxslt1-dev \
wait-for-it \
imagemagick \
unzip \
libjemalloc-dev \
libssl-dev \
ca-certificates \
gnupg
# Setup ENV variables
ENV PATH /usr/local/src/rbenv/shims:/usr/local/src/rbenv/bin:/usr/local/src/nodenv/shims:/usr/local/src/nodenv/bin:$PATH
ENV RBENV_ROOT /usr/local/src/rbenv
ENV NODENV_ROOT /usr/local/src/nodenv
ENV CONFIGURE_OPTS --disable-install-doc
ENV BUNDLE_PATH /bundles
ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so
WORKDIR /usr/src/app
# trim spaces and line return from .ruby-version file
COPY .ruby-version .ruby-version.raw
RUN cat .ruby-version.raw | tr -d '\r\t ' > .ruby-version
# Install Rbenv & Ruby
RUN git clone --depth 1 https://github.com/rbenv/rbenv.git ${RBENV_ROOT} && \
git clone --depth 1 https://github.com/rbenv/ruby-build.git ${RBENV_ROOT}/plugins/ruby-build && \
echo 'eval "$(rbenv init -)"' >> /etc/profile.d/rbenv.sh && \
RUBY_CONFIGURE_OPTS=--with-jemalloc rbenv install $(cat .ruby-version) && \
rbenv global $(cat .ruby-version)
# Install Postgres
RUN sh -c "echo 'deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main' >> /etc/apt/sources.list.d/pgdg.list" && \
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/trusted.gpg.d/apt.postgresql.org.gpg >/dev/null && \
apt-get update && \
apt-get install -yqq --no-install-recommends postgresql-client-10 libpq-dev
# trim spaces and line return from .node-version file
COPY .node-version .node-version.raw
RUN cat .node-version.raw | tr -d '\r\t ' > .node-version
# Install Node and Yarn with Nodenv
RUN git clone --depth 1 https://github.com/nodenv/nodenv.git ${NODENV_ROOT} && \
git clone --depth 1 https://github.com/nodenv/node-build.git ${NODENV_ROOT}/plugins/node-build && \
git clone --depth 1 https://github.com/pine/nodenv-yarn-install.git ${NODENV_ROOT}/plugins/nodenv-yarn-install && \
git clone --depth 1 https://github.com/nodenv/nodenv-package-rehash.git ${NODENV_ROOT}/plugins/nodenv-package-rehash && \
echo 'eval "$(nodenv init -)"' >> /etc/profile.d/nodenv.sh && \
nodenv install $(cat .node-version) && \
nodenv global $(cat .node-version)
# Install Chrome
RUN wget --quiet -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \
sh -c "echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' >> /etc/apt/sources.list.d/google-chrome.list" && \
apt-get update && \
apt-get install -fy google-chrome-stable
# Install Chromedriver
RUN wget https://chromedriver.storage.googleapis.com/2.41/chromedriver_linux64.zip && \
unzip chromedriver_linux64.zip -d /usr/bin && \
chmod u+x /usr/bin/chromedriver
# Copy code and install app dependencies
COPY . /usr/src/app/
# Install Bundler
RUN ./script/install-bundler
# Install front-end dependencies
RUN yarn install
# Run bundler install in parallel with the amount of available CPUs
RUN bundle install --jobs="$(nproc)"

View File

@@ -1,93 +0,0 @@
FROM ubuntu:20.04
ENV TZ Europe/London
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN echo "deb http://security.ubuntu.com/ubuntu bionic-security main" >> /etc/apt/sources.list
# Install all the requirements
RUN apt-get update && apt-get install -y \
curl \
git \
build-essential \
software-properties-common \
wget \
zlib1g-dev \
libreadline-dev \
libyaml-dev \
libffi-dev \
libxml2-dev \
libxslt1-dev \
wait-for-it \
imagemagick \
unzip \
libjemalloc-dev \
libssl-dev \
ca-certificates \
gnupg \
cmake
# Setup ENV variables
ENV PATH /usr/local/src/rbenv/shims:/usr/local/src/rbenv/bin:/usr/local/src/nodenv/shims:/usr/local/src/nodenv/bin:$PATH
ENV RBENV_ROOT /usr/local/src/rbenv
ENV NODENV_ROOT /usr/local/src/nodenv
ENV CONFIGURE_OPTS --disable-install-doc
ENV BUNDLE_PATH /bundles
ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so
WORKDIR /usr/src/app
# trim spaces and line return from .ruby-version file
COPY .ruby-version .ruby-version.raw
RUN cat .ruby-version.raw | tr -d '\r\t ' > .ruby-version
# Install Rbenv & Ruby
RUN git clone --depth 1 https://github.com/rbenv/rbenv.git ${RBENV_ROOT} && \
git clone --depth 1 https://github.com/rbenv/ruby-build.git ${RBENV_ROOT}/plugins/ruby-build && \
echo 'eval "$(rbenv init -)"' >> /etc/profile.d/rbenv.sh && \
RUBY_CONFIGURE_OPTS=--with-jemalloc rbenv install $(cat .ruby-version) && \
rbenv global $(cat .ruby-version)
# Install Postgres
RUN sh -c "echo 'deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main' >> /etc/apt/sources.list.d/pgdg.list" && \
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/trusted.gpg.d/apt.postgresql.org.gpg >/dev/null && \
apt-get update && \
apt-get install -yqq --no-install-recommends postgresql-client-10 libpq-dev
# trim spaces and line return from .node-version file
COPY .node-version .node-version.raw
RUN cat .node-version.raw | tr -d '\r\t ' > .node-version
# Install Node and Yarn with Nodenv
RUN git clone --depth 1 https://github.com/nodenv/nodenv.git ${NODENV_ROOT} && \
git clone --depth 1 https://github.com/nodenv/node-build.git ${NODENV_ROOT}/plugins/node-build && \
git clone --depth 1 https://github.com/pine/nodenv-yarn-install.git ${NODENV_ROOT}/plugins/nodenv-yarn-install && \
git clone --depth 1 https://github.com/nodenv/nodenv-package-rehash.git ${NODENV_ROOT}/plugins/nodenv-package-rehash && \
echo 'eval "$(nodenv init -)"' >> /etc/profile.d/nodenv.sh && \
nodenv install $(cat .node-version) && \
nodenv global $(cat .node-version)
# Install Chrome
RUN wget --quiet -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \
sh -c "echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' >> /etc/apt/sources.list.d/google-chrome.list" && \
apt-get update && \
apt-get install -fy google-chrome-stable
# Install Chromedriver
RUN wget https://chromedriver.storage.googleapis.com/2.41/chromedriver_linux64.zip && \
unzip chromedriver_linux64.zip -d /usr/bin && \
chmod u+x /usr/bin/chromedriver
# Copy code and install app dependencies
COPY . /usr/src/app/
# Install Bundler
RUN ./script/install-bundler
# Install front-end dependencies
RUN yarn install
# Run bundler install in parallel with the amount of available CPUs
RUN bundle install --jobs="$(nproc)"

View File

@@ -29,9 +29,9 @@ Fetch the latest version of `master` from `upstream` (ie. the main repo):
This project needs specific ruby/bundler versions as well as node/yarn specific versions. For a local setup you will need:
* Install or change your Ruby version according to the one specified at [.ruby-version](https://github.com/openfoodfoundation/openfoodnetwork/blob/master/.ruby-version) file.
- To manage versions, it's recommended to use [rbenv](https://github.com/rbenv/rbenv) or [RVM](https://rvm.io/).
- To manage versions, it's recommended to use [rbenv](https://github.com/rbenv/rbenv).
* Install [nodenv](https://github.com/nodenv/nodenv) to ensure the correct [.node-version](https://github.com/openfoodfoundation/openfoodnetwork/blob/master/.node-version) is used.
- [nodenv](https://github.com/nodenv/nodenv) is recommended as a node version manager.
- [nodevn](https://github.com/nodenv/nodenv) is recommended as a node version manager.
* PostgreSQL database
* Redis (for background jobs)
* Chrome (for testing)
@@ -73,7 +73,7 @@ To login as the default user, use:
email: ofn@example.com
password: ofn123
See [Locale and sample data] about loading data.
Seee [Locale and sample data] about loading data.
### Testing

44
Gemfile
View File

@@ -1,6 +1,6 @@
# frozen_string_literal: true
source 'https://gem.coop'
source 'https://rubygems.org'
git_source(:github) { |repo_name| "https://github.com/#{repo_name}.git" }
ruby File.read('.ruby-version').chomp
@@ -14,19 +14,18 @@ gem "active_storage_validations"
gem "aws-sdk-s3", require: false
gem "image_processing"
gem 'activemerchant'
gem 'angular-rails-templates'
gem 'activemerchant', '>= 1.78.0'
gem 'angular-rails-templates', '>= 0.3.0'
gem 'awesome_nested_set'
gem 'ransack', '~> 4.1.0'
gem 'responders'
gem 'rexml'
gem 'webpacker', '~> 5'
# Indirect dependency but we access it directly in JS specs.
# It turns out to be hard to upgrade but please do if you can.
gem 'sprockets', '~> 3.7'
gem 'i18n'
gem 'i18n-js', '~> 3.9.0'
gem 'rails-i18n'
gem 'rails_safe_tasks', '~> 1.0'
gem "activerecord-import"
gem "db2fog", github: "openfoodfoundation/db2fog", branch: "rails-7"
@@ -43,13 +42,13 @@ gem 'web', path: './engines/web'
gem "activerecord-postgresql-adapter"
gem "arel-helpers", "~> 2.12"
gem "pg"
gem "pg", "~> 1.2.3"
gem 'acts_as_list', '1.0.4'
gem 'cancancan', '~> 1.15.0'
gem 'digest'
gem 'ffaker'
gem 'highline'
gem 'highline', '2.0.3' # Necessary for the install generator
gem 'json'
gem 'monetize', '~> 1.11'
gem 'paranoia', '~> 2.4'
@@ -57,7 +56,7 @@ gem 'state_machines-activerecord'
gem 'stringex', '~> 2.8.5', require: false
gem 'paypal-sdk-merchant', '1.117.2'
gem 'stripe', '~> 15'
gem 'stripe'
gem 'devise'
gem 'devise-encryptable'
@@ -68,7 +67,7 @@ gem 'oauth2', '~> 1.4.7' # Used for Stripe Connect
gem 'datafoodconsortium-connector'
gem 'jsonapi-serializer'
gem 'pagy', '~> 9'
gem 'pagy', '~> 5.1'
gem 'rswag-api'
gem 'rswag-ui'
@@ -89,7 +88,7 @@ gem "active_model_serializers", "0.8.4"
gem 'activerecord-session_store'
gem 'acts-as-taggable-on'
gem 'angularjs-file-upload-rails', '~> 2.4.1'
gem 'bigdecimal'
gem 'bigdecimal', '3.0.2'
gem 'bootsnap', require: false
gem 'geocoder'
gem 'gmaps4rails'
@@ -104,11 +103,8 @@ gem 'redis'
gem 'sidekiq'
gem 'sidekiq-scheduler'
gem "cable_ready"
gem "stimulus_reflex"
gem "turbo_power"
gem "turbo-rails"
gem "cable_ready", "5.0.1"
gem "stimulus_reflex", "3.5.0.rc3"
gem 'combine_pdf'
gem 'wicked_pdf'
@@ -155,7 +151,6 @@ end
group :test, :development do
gem 'bullet'
gem 'capybara'
gem 'capybara-shadowdom'
gem 'cuprite'
gem 'database_cleaner', require: false
gem 'debug', '>= 1.0.0'
@@ -169,15 +164,14 @@ group :test, :development do
gem 'rspec-sql'
gem 'rswag'
gem 'shoulda-matchers'
gem 'stimulus_reflex_testing', github: "podia/stimulus_reflex_testing", branch: :main
gem 'stimulus_reflex_testing'
gem 'timecop'
end
group :test do
gem 'pdf-reader'
gem 'puffing-billy'
gem 'rails-controller-testing'
gem 'simplecov', require: false
gem 'undercover', require: false
gem 'vcr', require: false
gem 'webmock', require: false
# See spec/spec_helper.rb for instructions
@@ -185,19 +179,15 @@ group :test do
end
group :development do
gem 'debugger-linecache'
gem 'foreman'
gem 'haml_lint', require: false
gem 'i18n-tasks'
gem 'listen'
gem 'pry'
gem 'pry', '~> 0.13.0'
gem 'query_count'
gem 'rails-erd'
gem 'rubocop'
gem 'rubocop-capybara'
gem 'rubocop-factory_bot'
gem 'rubocop-rails'
gem 'rubocop-rspec'
gem 'rubocop-rspec_rails'
gem 'spring'
gem 'spring-commands-rspec'
gem 'spring-commands-rubocop'

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +0,0 @@
# Foreman Procfile for Docker env. Start all dev server processes with: `bundle exec foreman start -f Procfile.docker`
webpack: WEBPACKER_DEV_SERVER_HOST=0.0.0.0 ./bin/webpack-dev-server
sidekiq: DEV_CACHING=true bundle exec sidekiq -q mailers -q default
rails: WEBPACKER_DEV_SERVER_HOST=0.0.0.0 DEV_CACHING=true bundle exec rails s -p 3000 -b 0.0.0.0

View File

@@ -10,11 +10,11 @@
//= require jquery.ui.all
//= require jquery.powertip
//= require jquery.cookie
//= require jquery.jstree/jquery.jstree
//= require jquery.vAlign
//= require angular
//= require angular-resource
//= require angular-animate
//= require angular-sanitize
//= require angularjs-file-upload
//= require ../shared/ng-infinite-scroll.min.js
//= require ../shared/ng-tags-input.min.js
@@ -61,11 +61,19 @@
//= require ./variant_overrides/variant_overrides
// text, dates and translations
//= require textAngular-rangy.min.js
// This replaces angular-sanitize. We should include only one.
// https://github.com/textAngular/textAngular#where-to-get-it
//= require textAngular-sanitize.min.js
//= require textAngular.min.js
//= require i18n/translations
//= require darkswarm/i18n.translate.js
// foundation
//= require ../shared/mm-foundation-tpls-0.9.0-20180826174721.min.js
// LocalStorage
//= require ../shared/angular-local-storage.js
// requires the rest of the JS code in this folder
//= require_tree .

View File

@@ -0,0 +1,392 @@
angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout, $filter, $http, $window, $location, BulkProducts, DisplayProperties, DirtyProducts, VariantUnitManager, StatusMessage, producers, Taxons, Columns, tax_categories, RequestMonitor, SortOptions, ErrorsParser, ProductFiltersUrl) ->
$scope.StatusMessage = StatusMessage
$scope.columns = Columns.columns
$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.q = {
producerFilter: ""
categoryFilter: ""
importDateFilter: ""
query: ""
sorting: ""
}
$scope.sorting = "name asc"
$scope.producers = producers
$scope.taxons = Taxons.all
$scope.tax_categories = tax_categories
$scope.page = 1
$scope.per_page = 15
$scope.products = BulkProducts.products
$scope.DisplayProperties = DisplayProperties
$scope.sortOptions = SortOptions
$scope.initialise = ->
$scope.q = ProductFiltersUrl.loadFromUrl($location.search())
$scope.fetchProducts()
$scope.$watchCollection '[q.query, q.producerFilter, q.categoryFilter, q.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.q.query,
'q[supplier_id_eq]': $scope.q.producerFilter,
'q[primary_taxon_id_eq]': $scope.q.categoryFilter,
'q[s]': $scope.sorting,
import_date: $scope.q.importDateFilter,
page: $scope.page,
per_page: $scope.per_page
}
RequestMonitor.load(BulkProducts.fetch(params).$promise).then ->
# update url with the filters used
$location.search(ProductFiltersUrl.generate($scope.q))
$scope.resetProducts()
removeClearedValues = ->
delete $scope.q.producerFilter if $scope.q.producerFilter == "0"
delete $scope.q.categoryFilter if $scope.q.categoryFilter == "0"
delete $scope.q.importDateFilter if $scope.q.importDateFilter == "0"
$timeout ->
if $scope.showLatestImport
$scope.q.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.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.q.query = ""
$scope.q.producerFilter = "0"
$scope.q.categoryFilter = "0"
$scope.q.importDateFilter = "0"
$scope.fetchProducts()
$scope.$watch 'sortOptions', (sort) ->
return unless sort && sort.predicate != ""
$scope.sorting = sort.getSortingExpr(defaultDirection: "asc")
$scope.fetchProducts()
, true
confirm_unsaved_changes = () ->
(DirtyProducts.count() > 0 and confirm(t("unsaved_changes_confirmation"))) or (DirtyProducts.count() == 0)
editProductUrl = (product, variant) ->
"/admin/products/" + product.id + ((if variant then "/variants/" + variant.id else "")) + "/edit"
$scope.editWarn = (product, variant) ->
if confirm_unsaved_changes()
$window.location.href = ProductFiltersUrl.buildUrl(editProductUrl(product, variant), $scope.q)
$scope.toggleShowAllVariants = ->
showVariants = !DisplayProperties.showVariants 0
$scope.products.forEach (product) ->
DisplayProperties.setShowVariants product.id, showVariants
DisplayProperties.setShowVariants 0, showVariants
$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
tax_category_id: null
DisplayProperties.setShowVariants product.id, true
$scope.nextVariantId = ->
$scope.variantIdCounter = 0 unless $scope.variantIdCounter?
$scope.variantIdCounter -= 1
$scope.variantIdCounter
$scope.deleteProduct = (product) ->
if confirm(t('are_you_sure'))
$http(
method: "DELETE"
url: "/api/v0/products/" + product.id
).then (response) ->
$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/v0/products/" + product.id + "/variants/" + variant.id
).then (response) ->
$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()
$http(
method: "POST"
url: "/admin/products/bulk_update"
data:
products: productsToSubmit
filters:
'q[name_cont]': $scope.q.query
'q[supplier_id_eq]': $scope.q.producerFilter
'q[primary_taxon_id_eq]': $scope.q.categoryFilter
'q[s]': $scope.sorting
import_date: $scope.q.importDateFilter
page: $scope.page
per_page: $scope.per_page
).then((response) ->
DirtyProducts.clear()
BulkProducts.updateVariantLists(response.data.products || [])
$timeout -> $scope.displaySuccess()
).catch (response) ->
if response.status == 400 && response.data.errors?
errorsString = ErrorsParser.toString(response.data.errors, response.status)
$scope.displayFailure t("products_update_error") + "\n" + errorsString
else
$scope.displayFailure t("products_update_error_data") + response.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
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].replace(",", "."))
variant.unit_value = null if isNaN(variant.unit_value)
if variant.unit_value && product.variant_unit_scale
variant.unit_value = parseFloat(window.bigDecimal.multiply(variant.unit_value, product.variant_unit_scale, 2))
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) ->
filteredProducts = []
if productsToFilter instanceof Object
angular.forEach productsToFilter, (product) ->
if product.hasOwnProperty("id")
filteredProduct = {id: product.id}
filteredVariants = []
hasUpdatableProperty = false
if product.hasOwnProperty("variants")
angular.forEach product.variants, (variant) ->
result = filterSubmitVariant variant
filteredVariant = result.filteredVariant
variantHasUpdatableProperty = result.hasUpdatableProperty
filteredVariants.push filteredVariant if variantHasUpdatableProperty
if product.hasOwnProperty("sku")
filteredProduct.sku = product.sku
hasUpdatableProperty = true
if product.hasOwnProperty("name")
filteredProduct.name = product.name
hasUpdatableProperty = true
if product.hasOwnProperty("producer_id")
filteredProduct.supplier_id = product.producer_id
hasUpdatableProperty = true
if product.hasOwnProperty("price")
filteredProduct.price = product.price
hasUpdatableProperty = true
if product.hasOwnProperty("variant_unit_with_scale")
filteredProduct.variant_unit = product.variant_unit
filteredProduct.variant_unit_scale = product.variant_unit_scale
hasUpdatableProperty = true
if product.hasOwnProperty("variant_unit_name")
filteredProduct.variant_unit_name = product.variant_unit_name
hasUpdatableProperty = true
if product.hasOwnProperty("on_hand") and filteredVariants.length == 0 #only update if no variants present
filteredProduct.on_hand = product.on_hand
hasUpdatableProperty = true
if product.hasOwnProperty("on_demand") and filteredVariants.length == 0 #only update if no variants present
filteredProduct.on_demand = product.on_demand
hasUpdatableProperty = true
if product.hasOwnProperty("category_id")
filteredProduct.primary_taxon_id = product.category_id
hasUpdatableProperty = true
if product.hasOwnProperty("inherits_properties")
filteredProduct.inherits_properties = product.inherits_properties
hasUpdatableProperty = true
if filteredVariants.length > 0 # Note that the name of the property changes to enable mass assignment of variants attributes with rails
filteredProduct.variants_attributes = filteredVariants
hasUpdatableProperty = true
filteredProducts.push filteredProduct if hasUpdatableProperty
filteredProducts
filterSubmitVariant = (variant) ->
hasUpdatableProperty = false
filteredVariant = {}
if not variant.deleted_at? and variant.hasOwnProperty("id")
filteredVariant.id = variant.id unless variant.id <= 0
if variant.hasOwnProperty("sku")
filteredVariant.sku = variant.sku
hasUpdatableProperty = true
if variant.hasOwnProperty("on_hand")
filteredVariant.on_hand = variant.on_hand
hasUpdatableProperty = true
if variant.hasOwnProperty("on_demand")
filteredVariant.on_demand = variant.on_demand
hasUpdatableProperty = true
if variant.hasOwnProperty("price")
filteredVariant.price = variant.price
hasUpdatableProperty = true
if variant.hasOwnProperty("unit_value")
filteredVariant.unit_value = variant.unit_value
hasUpdatableProperty = true
if variant.hasOwnProperty("unit_description")
filteredVariant.unit_description = variant.unit_description
hasUpdatableProperty = true
if variant.hasOwnProperty("display_name")
filteredVariant.display_name = variant.display_name
hasUpdatableProperty = true
if variant.hasOwnProperty("tax_category_id")
filteredVariant.tax_category_id = variant.tax_category_id
hasUpdatableProperty = true
if variant.hasOwnProperty("display_as")
filteredVariant.display_as = variant.display_as
hasUpdatableProperty = true
{filteredVariant: filteredVariant, hasUpdatableProperty: hasUpdatableProperty}
toObjectWithIDKeys = (array) ->
object = {}
for i of array
if array[i] instanceof Object and array[i].hasOwnProperty("id")
object[array[i].id] = angular.copy(array[i])
object[array[i].id].variants = toObjectWithIDKeys(array[i].variants) if array[i].hasOwnProperty("variants") and array[i].variants instanceof Array
object

View File

@@ -11,9 +11,6 @@ angular.module("admin.customers").controller "customersCtrl", ($scope, $q, $filt
$scope.confirmRefresh = (event) ->
event.preventDefault() unless pendingChanges.unsavedCount() == 0 || confirm(t("unsaved_changes_warning"))
$scope.hasUnsavedChanges = ->
pendingChanges.yes()
$scope.$watch "shop_id", ->
if $scope.shop_id?
CurrentShop.shop = $filter('filter')($scope.shops, {id: parseInt($scope.shop_id)}, true)[0]

View File

@@ -4,30 +4,31 @@ angular.module("ofn.admin").directive "ofnDisplayAs", (OptionValueNamer) ->
scope.$watchCollection ->
return [
scope.$eval(attrs.ofnDisplayAs).unit_value_with_description
scope.variant.variant_unit_name
scope.variant.variant_unit_with_scale
scope.product.variant_unit_name
scope.product.variant_unit_with_scale
]
, ->
[variant_unit, variant_unit_scale] = productUnitProperties()
[unit_value, unit_description] = variantUnitProperties(variant_unit_scale)
variant_object =
variant_object =
unit_value: unit_value
unit_description: unit_description
variant_unit_scale: variant_unit_scale
variant_unit: variant_unit
variant_unit_name: scope.variant.variant_unit_name
product:
variant_unit_scale: variant_unit_scale
variant_unit: variant_unit
variant_unit_name: scope.product.variant_unit_name
scope.placeholder_text = new OptionValueNamer(variant_object).name()
productUnitProperties = ->
# get relevant product properties
if scope.variant.variant_unit_with_scale?
match = scope.variant.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
if scope.product.variant_unit_with_scale?
match = scope.product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
if match
variant_unit = match[1]
variant_unit_scale = parseFloat(match[2])
else
variant_unit = scope.variant.variant_unit_with_scale
variant_unit = scope.product.variant_unit_with_scale
variant_unit_scale = null
else
variant_unit = variant_unit_scale = null
@@ -44,4 +45,4 @@ angular.module("ofn.admin").directive "ofnDisplayAs", (OptionValueNamer) ->
unit_value = null if isNaN(unit_value)
unit_value *= variant_unit_scale if unit_value && variant_unit_scale
unit_description = match[3]
[unit_value, unit_description]
[unit_value, unit_description]

View File

@@ -0,0 +1,8 @@
angular.module("ofn.admin").directive "ofnMaintainUnitScale", ->
require: "ngModel"
link: (scope, element, attrs, ngModel) ->
scope.$watch 'product.variant_unit_with_scale', (newValue, oldValue) ->
if not (oldValue == newValue)
# Triggers track-variant directive to track the unit_value, so that changes to the unit are passed to the server
ngModel.$setViewValue ngModel.$viewValue

View File

@@ -0,0 +1,8 @@
angular.module("ofn.admin").directive "ofnTrackMaster", (DirtyProducts) ->
require: "ngModel"
link: (scope, element, attrs, ngModel) ->
ngModel.$parsers.push (viewValue) ->
if ngModel.$dirty
DirtyProducts.addMasterProperty scope.product.id, scope.product.master.id, attrs.ofnTrackMaster, viewValue
scope.displayDirtyProducts()
viewValue

View File

@@ -1 +1 @@
angular.module("admin.enterprise_groups", ["admin.side_menu", "admin.users", "ngSanitize"])
angular.module("admin.enterprise_groups", ["admin.side_menu", "admin.users", "textAngular"])

View File

@@ -55,6 +55,12 @@ angular.module("admin.enterprises")
else
alert ("#{manager.email}" + " " + t("is_already_manager"))
$scope.removeLogo = ->
$scope.performEnterpriseAction("removeLogo", "immediate_logo_removal_warning", "removed_logo_successfully")
$scope.removePromoImage = ->
$scope.performEnterpriseAction("removePromoImage", "immediate_promo_image_removal_warning", "removed_promo_image_successfully")
$scope.performEnterpriseAction = (enterpriseActionName, warning_message_key, success_message_key) ->
return unless confirm($scope.translation(warning_message_key))

View File

@@ -3,9 +3,24 @@ angular.module("admin.enterprises", [
"admin.utils",
"admin.shippingMethods",
"admin.users",
"textAngular",
"admin.side_menu",
"admin.taxons",
'admin.indexUtils',
'admin.tagRules',
'admin.dropdown',
'ngSanitize']
)
# For more options: https://github.com/textAngular/textAngular/blob/master/src/textAngularSetup.js
.config [
'$provide', ($provide) ->
$provide.decorator 'taTranslations', [
'$delegate'
(taTranslations) ->
taTranslations.insertLink = {
tooltip: t('admin.enterprises.form.shop_preferences.shopfront_message_link_tooltip'),
dialogPrompt: t('admin.enterprises.form.shop_preferences.shopfront_message_link_prompt')
}
taTranslations
]
]

View File

@@ -4,16 +4,15 @@ angular.module("admin.indexUtils").directive "objForUpdate", (switchClass, pendi
type: "@objForUpdate"
attr: "@attrForUpdate"
link: (scope, element, attrs) ->
scope.savedValue = scope.object()[scope.attr] || ""
scope.savedValue = scope.object()[scope.attr]
scope.$watch "object().#{scope.attr}", (value) ->
strValue = value || ""
if strValue == scope.savedValue
if value == scope.savedValue
pendingChanges.remove(scope.object().id, scope.attr)
scope.clear()
else
scope.pending()
addPendingChange(scope.attr, strValue)
addPendingChange(scope.attr, value ? "")
scope.reset = (value) ->
scope.savedValue = value
@@ -38,13 +37,16 @@ angular.module("admin.indexUtils").directive "objForUpdate", (switchClass, pendi
# To ensure the customer is still updated, we check on the $destroy event to see if
# the attribute has changed, if so we queue up the change.
scope.$on '$destroy', (value) ->
currentValue = scope.object()[scope.attr] || ""
# No update
return if currentValue is scope.savedValue
return if scope.object()[scope.attr] is scope.savedValue
# For some reason the code attribute is removed from the object when cleared, so we add
# an emptyvalue so it gets updated properly
if scope.attr is "code" and scope.object()[scope.attr] is undefined
scope.object()["code"] = ""
# Queuing up change
addPendingChange(scope.attr, currentValue)
addPendingChange(scope.attr, scope.object()[scope.attr])
# private

View File

@@ -1,16 +1,12 @@
# Used like a regular angular filter where an object is passed
# Adds the additional special case that a value of 0 for the filter
# acts as a bypass for that particular attribute
# NOTE the name doesn't reflect what the filter does, it only fiters on the variant.producer_id
angular.module("admin.indexUtils").filter "attrFilter", ($filter) ->
return (objects, filters) ->
filter = filters["producer_id"]
return objects if !filter? || filter == 0
return $filter('filter')(objects, (product) ->
for variant in product.variants
return true if variant["producer_id"] == filter
false
, true)
Object.keys(filters).reduce (filtered, attr) ->
filter = filters[attr]
return filtered if !filter? || filter == 0
return $filter('filter')(filtered, (object) ->
object[attr] == filter
)
, objects

View File

@@ -31,7 +31,7 @@ angular.module("admin.indexUtils").factory 'Columns', ($rootScope, $http, $injec
savePreferences: (action_name) =>
$http
method: "PUT"
url: "/admin/column_preferences/bulk_update.json"
url: "/admin/column_preferences/bulk_update"
data:
action_name: action_name
column_preferences: (preference for column_name, preference of @columns)

View File

@@ -16,10 +16,7 @@ angular.module("admin.indexUtils").factory "pendingChanges", ($q, resources, Sta
remove: (id, attr) =>
if @pendingChanges.hasOwnProperty("#{id}")
delete @pendingChanges["#{id}"]["#{attr}"]
if @changeCount( @pendingChanges["#{id}"] ) < 1
delete @pendingChanges["#{id}"]
StatusMessage.clear()
delete @pendingChanges["#{id}"] if @changeCount( @pendingChanges["#{id}"] ) < 1
submitAll: (form=null) =>
all = []
@@ -50,8 +47,5 @@ angular.module("admin.indexUtils").factory "pendingChanges", ($q, resources, Sta
unsavedCount: ->
Object.keys(@pendingChanges).length
yes: ->
@unsavedCount() > 0
changeCount: (objectChanges) ->
Object.keys(objectChanges).length

View File

@@ -1,4 +1,4 @@
angular.module("admin.indexUtils").factory "switchClass", ($timeout, StatusMessage) ->
angular.module("admin.indexUtils").factory "switchClass", ($timeout) ->
return (element, classToAdd, removeClasses, timeout) ->
$timeout.cancel element.timeout if element.timeout
element.removeClass className for className in removeClasses
@@ -7,6 +7,4 @@ angular.module("admin.indexUtils").factory "switchClass", ($timeout, StatusMessa
if timeout && intRegex.test(timeout)
element.timeout = $timeout(->
element.removeClass classToAdd
StatusMessage.clear()
, timeout, true)
element

View File

@@ -19,6 +19,18 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
$scope.page = 1
$scope.per_page = $scope.per_page_options[0].id
$scope.filterByVariantId = null
searchThrough = ["order_distributor_name",
"order_bill_address_phone",
"order_bill_address_firstname",
"order_bill_address_lastname",
"order_bill_address_full_name",
"order_bill_address_full_name_reversed",
"order_bill_address_full_name_with_comma",
"order_bill_address_full_name_with_comma_reversed",
"variant_product_supplier_name",
"order_email",
"order_number",
"product_name"].join("_or_") + "_cont"
$scope.confirmRefresh = ->
LineItems.allSaved() || confirm(t("unsaved_changes_warning"))
@@ -63,19 +75,19 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
[formattedStartDate, formattedEndDate] = $scope.formatDates($scope.startDate, $scope.endDate)
RequestMonitor.load LineItems.index(
"q[#{searchThrough}]": $scope.query,
"q[variant_id_eq]": $scope.filterByVariantId if $scope.filterByVariantId,
"q[order_state_not_eq]": "canceled",
"q[order_shipment_state_not_eq]": "shipped",
"q[order_completed_at_not_null]": "true",
"q[variant_id_eq]": $scope.filterByVariantId if $scope.filterByVariantId,
"q[order_distributor_id_eq]": $scope.distributorFilter,
"q[variant_supplier_id_eq]": $scope.supplierFilter,
"q[variant_product_supplier_id_eq]": $scope.supplierFilter,
"q[order_order_cycle_id_eq]": $scope.orderCycleFilter,
"q[order_completed_at_gteq]": if formattedStartDate then formattedStartDate else undefined,
"q[order_completed_at_lt]": if formattedEndDate then formattedEndDate else undefined,
"q[s]": "order_completed_at desc",
"page": $scope.page,
"per_page": $scope.per_page,
"search_query": $scope.query
"per_page": $scope.per_page
)
$scope.formatDates = (startDate, endDate) ->
@@ -85,7 +97,7 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
$scope.loadAssociatedData = ->
RequestMonitor.load $scope.distributors = Enterprises.index(action: "visible", ams_prefix: "basic", "q[sells_in][]": ["own", "any"])
RequestMonitor.load $scope.orderCycles = OrderCycles.index(ams_prefix: "basic", as: "distributor", "q[orders_close_at_gt]": "#{moment().subtract(1,'year').format()}")
RequestMonitor.load $scope.orderCycles = OrderCycles.index(ams_prefix: "basic", as: "distributor", "q[orders_close_at_gt]": "#{moment().subtract(90,'days').format()}")
RequestMonitor.load $scope.suppliers = Enterprises.index(action: "visible", ams_prefix: "basic", "q[is_primary_producer_eq]": "true")
$scope.dereferenceLoadedData = ->
@@ -93,7 +105,7 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
Dereferencer.dereferenceAttr $scope.line_items, "supplier", Enterprises.byID
$scope.loadOrders()
RequestMonitor.load $q.all([$scope.orders.$promise]).then ->
Dereferencer.dereferenceAttr $scope.line_items, "order", Orders.byID
Dereferencer.dereferenceAttr $scope.line_items, "order", Orders.byID
Dereferencer.dereferenceAttr $scope.orders, "distributor", Enterprises.byID
Dereferencer.dereferenceAttr $scope.orders, "order_cycle", OrderCycles.byID
$scope.bulk_order_form.$setPristine()
@@ -121,7 +133,7 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
return $http(
method: 'GET'
url: "/admin/orders/#{order.number}/fire?e=cancel&send_cancellation_email=#{sendEmailCancellation}&restock_items=#{restock_items}")
$scope.deleteLineItem = (lineItem) ->
if lineItem.order.item_count == 1
ofnCancelOrderAlert((confirm, sendEmailCancellation, restock_items) ->
@@ -155,7 +167,7 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
$scope.cancelOrder(order, sendEmailCancellation, restock_items).then(-> $scope.refreshData())
else
Promise.all(LineItems.delete(item) for item in items).then(-> $scope.refreshData())
, "js.admin.deleting_item_will_cancel_order")
, "js.admin.deleting_item_will_cancel_order")
else
ofnDeleteLineItemsAlert(() ->
Promise.all(LineItems.delete(item) for item in lineItemsToDelete).then(-> $scope.refreshData())
@@ -187,14 +199,14 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
$scope.refreshData()
$scope.getLineItemScale = (lineItem) ->
if lineItem.units_variant && lineItem.units_variant.variant_unit_scale && (lineItem.units_variant.variant_unit == "weight" || lineItem.units_variant.variant_unit == "volume")
lineItem.units_variant.variant_unit_scale
if lineItem.units_product && lineItem.units_variant && (lineItem.units_product.variant_unit == "weight" || lineItem.units_product.variant_unit == "volume")
lineItem.units_product.variant_unit_scale
else
1
$scope.sumUnitValues = ->
sum = $scope.filteredLineItems?.reduce (sum, lineItem) ->
if lineItem.units_variant.variant_unit == "items"
if lineItem.units_product.variant_unit == "items"
sum + lineItem.quantity
else
sum + $scope.roundToThreeDecimals(lineItem.final_weight_volume / $scope.getLineItemScale(lineItem))
@@ -202,7 +214,7 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
$scope.sumMaxUnitValues = ->
sum = $scope.filteredLineItems?.reduce (sum,lineItem) ->
if lineItem.units_variant.variant_unit == "items"
if lineItem.units_product.variant_unit == "items"
sum + lineItem.max_quantity
else
sum + lineItem.max_quantity * $scope.roundToThreeDecimals(lineItem.units_variant.unit_value / $scope.getLineItemScale(lineItem))
@@ -216,41 +228,39 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
return false if !lineItem.hasOwnProperty('final_weight_volume') || !(lineItem.final_weight_volume > 0)
true
$scope.getScale = (unitsVariant) ->
if unitsVariant.hasOwnProperty("variant_unit") && (unitsVariant.variant_unit == "weight" || unitsVariant.variant_unit == "volume")
unitsVariant.variant_unit_scale
else if unitsVariant.hasOwnProperty("variant_unit") && unitsVariant.variant_unit == "items"
$scope.getScale = (unitsProduct, unitsVariant) ->
if unitsProduct.hasOwnProperty("variant_unit") && (unitsProduct.variant_unit == "weight" || unitsProduct.variant_unit == "volume")
unitsProduct.variant_unit_scale
else if unitsProduct.hasOwnProperty("variant_unit") && unitsProduct.variant_unit == "items"
1
else
null
$scope.getFormattedValueWithUnitName = (value, unitsVariant, scale) ->
unit_name = VariantUnitManager.getUnitName(scale, unitsVariant.variant_unit)
$scope.getFormattedValueWithUnitName = (value, unitsProduct, unitsVariant, scale) ->
unit_name = VariantUnitManager.getUnitName(scale, unitsProduct.variant_unit)
$scope.roundToThreeDecimals(value) + " " + unit_name
$scope.getGroupBySizeFormattedValueWithUnitName = (value, unitsVariant) ->
scale = $scope.getScale(unitsVariant)
$scope.getGroupBySizeFormattedValueWithUnitName = (value, unitsProduct, unitsVariant) ->
scale = $scope.getScale(unitsProduct, unitsVariant)
if scale && value
value = value / scale if scale != 28.35 && scale != 1 && scale != 453.6 # divide by scale if not smallest unit
$scope.getFormattedValueWithUnitName(value, unitsVariant, scale)
$scope.getFormattedValueWithUnitName(value, unitsProduct, unitsVariant, scale)
else
''
$scope.formattedValueWithUnitName = (value, unitsVariant) ->
scale = $scope.getScale(unitsVariant)
$scope.formattedValueWithUnitName = (value, unitsProduct, unitsVariant) ->
scale = $scope.getScale(unitsProduct, unitsVariant)
if scale
$scope.getFormattedValueWithUnitName(value, unitsVariant, scale)
else
$scope.getFormattedValueWithUnitName(value, unitsProduct, unitsVariant, scale)
else
''
$scope.fulfilled = (sumOfUnitValues) ->
# A Units Variant is an API object which holds unit properies of a variant
if $scope.selectedUnitsProduct.hasOwnProperty("group_buy_unit_size") && $scope.selectedUnitsProduct.group_buy_unit_size > 0 &&
$scope.selectedUnitsVariant.hasOwnProperty("variant_unit")
if $scope.selectedUnitsVariant.variant_unit == "weight" || $scope.selectedUnitsVariant.variant_unit == "volume"
scale = $scope.selectedUnitsVariant.variant_unit_scale
if $scope.selectedUnitsProduct.hasOwnProperty("group_buy_unit_size")&& $scope.selectedUnitsProduct.group_buy_unit_size > 0 &&
$scope.selectedUnitsProduct.hasOwnProperty("variant_unit")
if $scope.selectedUnitsProduct.variant_unit == "weight" || $scope.selectedUnitsProduct.variant_unit == "volume"
scale = $scope.selectedUnitsProduct.variant_unit_scale
sumOfUnitValues = sumOfUnitValues * scale unless scale == 28.35 || scale == 453.6
$scope.roundToThreeDecimals(sumOfUnitValues / $scope.selectedUnitsProduct.group_buy_unit_size)
else

View File

@@ -3,7 +3,6 @@ angular.module('admin.orderCycles')
$controller('AdminOrderCycleBasicCtrl', {$scope: $scope, ocInstance: ocInstance})
order_cycle_id = $location.absUrl().match(/\/admin\/order_cycles\/(\d+)/)[1]
$scope.order_cycle_id = order_cycle_id
$scope.order_cycle = OrderCycle.load(order_cycle_id)
$scope.enterprises = Enterprise.index(order_cycle_id: order_cycle_id)
$scope.enterprise_fees = EnterpriseFee.index(order_cycle_id: order_cycle_id)
@@ -19,8 +18,6 @@ angular.module('admin.orderCycles')
$scope.submit = ($event, destination) ->
$event.preventDefault()
$scope.order_cycle?.trigger_action = $($event.target).data('trigger-action');
$scope.order_cycle?.confirm = $($event.target).data('confirm');
StatusMessage.display 'progress', t('js.saving')
OrderCycle.update(destination, $scope.order_cycle_form)
@@ -28,4 +25,4 @@ angular.module('admin.orderCycles')
if $scope.order_cycle_form?.$dirty
t('admin.unsaved_confirm_leave')
NavigationCheck.register(warnAboutUnsavedChanges)
NavigationCheck.register(warnAboutUnsavedChanges)

View File

@@ -1,9 +1,8 @@
angular.module('admin.orderCycles').controller 'AdminOrderCycleIncomingCtrl', ($scope, $rootScope, $controller, $location, Enterprise, EnterpriseFee, OrderCycle, ExchangeProduct, ocInstance) ->
angular.module('admin.orderCycles').controller 'AdminOrderCycleIncomingCtrl', ($scope, $rootScope, $controller, $location, Enterprise, OrderCycle, ExchangeProduct, ocInstance) ->
$controller('AdminOrderCycleExchangesCtrl', {$scope: $scope, ocInstance: ocInstance, $location: $location})
$scope.view = 'incoming'
# NB: weirdly at this next line $scope.order_cycle.id comes out undefined so we use $scope.order_cycle_id instead
$scope.enterprise_fees = EnterpriseFee.index(order_cycle_id: $scope.order_cycle_id, per_item: true)
$scope.exchangeTotalVariants = (exchange) ->
return unless $scope.enterprises? && $scope.enterprises[exchange.enterprise_id]?

View File

@@ -1,11 +1,7 @@
angular.module("admin.orderCycles").controller "OrderCyclesCtrl", ($scope, $q, Columns, StatusMessage, RequestMonitor, OrderCycles, Enterprises, Schedules, Dereferencer) ->
$scope.RequestMonitor = RequestMonitor
$scope.columns = Columns.columns
$scope.saveAll = ($event) ->
trigger_action = $($event.target).data('trigger-action')
confirm = $($event.target).data('confirm')
OrderCycles.saveChanges($scope.order_cycles_form, { trigger_action, confirm })
$scope.saveAll = -> OrderCycles.saveChanges($scope.order_cycles_form)
$scope.ordersCloseAtLimit = -31 # days
$scope.resetSelectFilters = ->

View File

@@ -22,8 +22,6 @@ angular.module('admin.orderCycles').controller "AdminSimpleEditOrderCycleCtrl",
$scope.submit = ($event, destination) ->
$event.preventDefault()
$scope.order_cycle?.trigger_action = $($event.target).data('trigger-action');
$scope.order_cycle?.confirm = $($event.target).data('confirm');
StatusMessage.display 'progress', t('js.saving')
OrderCycle.mirrorIncomingToOutgoingProducts()
OrderCycle.update(destination, $scope.order_cycle_form) if OrderCycle.confirmNoDistributors()

View File

@@ -6,8 +6,6 @@ angular.module('admin.orderCycles').factory('EnterpriseFee', ($resource) ->
params:
order_cycle_id: '@order_cycle_id'
coordinator_id: '@coordinator_id'
per_item: '@per_item'
per_order: '@per_order'
})
{

View File

@@ -161,11 +161,7 @@ angular.module('admin.orderCycles').factory 'OrderCycle', ($resource, $window, $
StatusMessage.display('failure', t('js.order_cycles.create_failure'))
update: (destination, form) ->
oc = new OrderCycleResource({
order_cycle: this.dataForSubmit(),
confirm: this.order_cycle.confirm,
trigger_action: this.order_cycle.trigger_action
})
oc = new OrderCycleResource({order_cycle: this.dataForSubmit()})
oc.$update {order_cycle_id: this.order_cycle.id, reloading: (if destination? then 1 else 0)}, (data) =>
form.$setPristine() if form
if destination?
@@ -175,8 +171,6 @@ angular.module('admin.orderCycles').factory 'OrderCycle', ($resource, $window, $
, (response) ->
if response.data.errors?
StatusMessage.display('failure', response.data.errors[0])
else if (response.data.trigger_action)
StatusMessage.display('notice', t('js.order_cycles.unsaved_changes'), response.data.trigger_action)
else
StatusMessage.display('failure', t('js.order_cycles.update_failure'))

View File

@@ -5,8 +5,6 @@ angular.module("admin.orders").controller "orderCtrl", ($scope, shops, orderCycl
$scope.distributor_id = parseInt($attrs.ofnDistributorId)
$scope.order_cycle_id = parseInt($attrs.ofnOrderCycleId)
$scope.search_variants_as = $attrs.ofnSearchVariantsAs
$scope.order_id = $attrs.ofnOrderId
$scope.validOrderCycle = (oc) ->
$scope.orderCycleHasDistributor oc, parseInt($scope.distributor_id)

View File

@@ -15,4 +15,4 @@ angular.module("admin.paymentMethods").controller "StripeController", ($scope, $
permalink = shops.filter((shop) ->
shop.id == $scope.paymentMethod.preferred_enterprise_id
)[0].permalink
"/admin/enterprises/#{permalink}/edit#/payment_methods_panel"
"/admin/enterprises/#{permalink}/edit#/payment_methods"

View File

@@ -0,0 +1,24 @@
angular.module("admin.products").controller "editUnitsCtrl", ($scope, VariantUnitManager) ->
$scope.product =
variant_unit: angular.element('#variant_unit').val()
variant_unit_scale: angular.element('#variant_unit_scale').val()
$scope.variant_unit_options = VariantUnitManager.variantUnitOptions()
if $scope.product.variant_unit == 'items'
$scope.variant_unit_with_scale = 'items'
else
$scope.variant_unit_with_scale = $scope.product.variant_unit + '_' + $scope.product.variant_unit_scale.replace(/\.0$/, '');
$scope.setFields = ->
if $scope.variant_unit_with_scale == 'items'
variant_unit = 'items'
variant_unit_scale = null
else
options = $scope.variant_unit_with_scale.split('_')
variant_unit = options[0]
variant_unit_scale = options[1]
$scope.product.variant_unit = variant_unit
$scope.product.variant_unit_scale = variant_unit_scale

View File

@@ -0,0 +1,8 @@
angular.module("ofn.admin").controller "ProductImageCtrl", ($scope, ProductImageService) ->
$scope.imageUploader = ProductImageService.imageUploader
$scope.imagePreview = ProductImageService.imagePreview
$scope.$watch 'product.image_url', (newValue, oldValue) ->
if newValue != oldValue
$scope.imagePreview = newValue
$scope.uploadModal.close()

View File

@@ -1,26 +1,24 @@
# Controller for "New Products" form (spree/admin/products/new)
angular.module("admin.products")
.controller "unitsCtrl", ($scope, VariantUnitManager, OptionValueNamer, UnitPrices, PriceParser) ->
$scope.product = {}
$scope.product = { master: {} }
$scope.product.master.product = $scope.product
$scope.placeholder_text = ""
$scope.$watchCollection '[product.variant_unit_with_scale, product.unit_value_with_description, product.price, product.variant_unit_name]', ->
$scope.$watchCollection '[product.variant_unit_with_scale, product.master.unit_value_with_description, product.price, product.variant_unit_name]', ->
$scope.processVariantUnitWithScale()
$scope.processUnitValueWithDescription()
$scope.processUnitPrice()
$scope.placeholder_text = new OptionValueNamer($scope.product).name() if $scope.product.variant_unit_scale
$scope.placeholder_text = new OptionValueNamer($scope.product.master).name() if $scope.product.variant_unit_scale
$scope.variant_unit_options = VariantUnitManager.variantUnitOptions()
# Extract variant_unit and variant_unit_scale from dropdown variant_unit_with_scale,
# and update hidden product fields
$scope.processVariantUnitWithScale = ->
if $scope.product.variant_unit_with_scale
match = $scope.product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/) # matches string like "weight_1000"
match = $scope.product.variant_unit_with_scale.match(/^([^_]+)_([\d\.]+)$/)
if match
$scope.product.variant_unit = match[1]
$scope.product.variant_unit_scale = parseFloat(match[2])
else # "items"
else
$scope.product.variant_unit = $scope.product.variant_unit_with_scale
$scope.product.variant_unit_scale = null
else if $scope.product.variant_unit
@@ -34,27 +32,24 @@ angular.module("admin.products")
else
$scope.product.variant_unit = $scope.product.variant_unit_scale = null
# Extract unit_value and unit_description from text field unit_value_with_description,
# and update hidden variant fields
$scope.processUnitValueWithDescription = ->
if $scope.product.hasOwnProperty("unit_value_with_description")
match = $scope.product.unit_value_with_description.match(/^([\d\.,]+(?= *|$)|)( *)(.*)$/)
if $scope.product.master.hasOwnProperty("unit_value_with_description")
match = $scope.product.master.unit_value_with_description.match(/^([\d\.,]+(?= *|$)|)( *)(.*)$/)
if match
$scope.product.unit_value = PriceParser.parse(match[1])
$scope.product.unit_value = null if isNaN($scope.product.unit_value)
$scope.product.unit_value = window.bigDecimal.multiply($scope.product.unit_value, $scope.product.variant_unit_scale, 2) if $scope.product.unit_value && $scope.product.variant_unit_scale
$scope.product.unit_description = match[3]
$scope.product.master.unit_value = PriceParser.parse(match[1])
$scope.product.master.unit_value = null if isNaN($scope.product.master.unit_value)
$scope.product.master.unit_value = window.bigDecimal.multiply($scope.product.master.unit_value, $scope.product.variant_unit_scale, 2) if $scope.product.master.unit_value && $scope.product.variant_unit_scale
$scope.product.master.unit_description = match[3]
else
value = $scope.product.unit_value
value = window.bigDecimal.divide(value, $scope.product.variant_unit_scale, 2) if $scope.product.unit_value && $scope.product.variant_unit_scale
$scope.product.unit_value_with_description = value + " " + $scope.product.unit_description
value = $scope.product.master.unit_value
value = window.bigDecimal.divide(value, $scope.product.variant_unit_scale, 2) if $scope.product.master.unit_value && $scope.product.variant_unit_scale
$scope.product.master.unit_value_with_description = value + " " + $scope.product.master.unit_description
# Calculate unit price based on product price and variant_unit_scale
$scope.processUnitPrice = ->
price = $scope.product.price
scale = $scope.product.variant_unit_scale
unit_type = $scope.product.variant_unit
unit_value = $scope.product.unit_value
unit_value = $scope.product.master.unit_value
variant_unit_name = $scope.product.variant_unit_name
$scope.unit_price = UnitPrices.displayableUnitPrice(price, scale, unit_type, unit_value, variant_unit_name)

View File

@@ -0,0 +1,32 @@
angular.module("admin.products").controller "variantUnitsCtrl", ($scope, VariantUnitManager, $timeout, UnitPrices, PriceParser) ->
$scope.unitName = (scale, type) ->
VariantUnitManager.getUnitName(scale, type)
$scope.$watchCollection "[unit_value_human, variant.price]", ->
$scope.processUnitPrice()
$scope.processUnitPrice = ->
if ($scope.variant)
price = $scope.variant.price
scale = $scope.scale
unit_type = angular.element("#product_variant_unit").val()
if (unit_type != "items")
$scope.updateValue()
unit_value = $scope.unit_value
else
unit_value = 1
variant_unit_name = angular.element("#product_variant_unit_name").val()
$scope.unit_price = UnitPrices.displayableUnitPrice(price, scale, unit_type, unit_value, variant_unit_name)
$scope.scale = angular.element('#product_variant_unit_scale').val()
$scope.updateValue = ->
unit_value_human = angular.element('#unit_value_human').val()
$scope.unit_value = bigDecimal.multiply(PriceParser.parse(unit_value_human), $scope.scale, 2)
variant_unit_value = angular.element('#variant_unit_value').val()
$scope.unit_value_human = parseFloat(bigDecimal.divide(variant_unit_value, $scope.scale, 2))
$timeout -> $scope.processUnitPrice()
$timeout -> $scope.updateValue()

View File

@@ -0,0 +1,6 @@
angular.module("ofn.admin").directive "imageModal", ($modal, ProductImageService) ->
restrict: 'C'
link: (scope, elem, attrs, ctrl) ->
elem.on "click", (ev) =>
scope.uploadModal = $modal.open(templateUrl: 'admin/modals/image_upload.html', controller: ctrl, scope: scope, windowClass: 'simple-modal')
ProductImageService.configure(scope.product)

View File

@@ -0,0 +1,19 @@
angular.module("admin.products").directive "setOnDemand", ->
link: (scope, element, attr) ->
onHand = element.context.querySelector("#variant_on_hand")
onDemand = element.context.querySelector("#variant_on_demand")
disableOnHandIfOnDemand = ->
if onDemand.checked
onHand.disabled = 'disabled'
onHand.dataStock = onHand.value
onHand.value = t('admin.products.variants.infinity')
disableOnHandIfOnDemand()
onDemand.addEventListener 'change', (event) ->
disableOnHandIfOnDemand()
if !onDemand.checked
onHand.removeAttribute('disabled')
onHand.value = onHand.dataStock

View File

@@ -1 +1 @@
angular.module("admin.products", ["ngSanitize", "admin.utils", "OFNShared"])
angular.module("admin.products", ["textAngular", "admin.utils", "OFNShared"])

View File

@@ -1,5 +1,4 @@
angular.module("admin.products").factory "OptionValueNamer", (VariantUnitManager) ->
# Javascript clone of VariantUnits::OptionValueNamer, for bulk product editing.
class OptionValueNamer
constructor: (@variant) ->
@@ -13,16 +12,16 @@ angular.module("admin.products").factory "OptionValueNamer", (VariantUnitManager
name_fields.join ' '
value_scaled: ->
@variant.variant_unit_scale?
@variant.product.variant_unit_scale?
option_value_value_unit: ->
if @variant.unit_value?
if @variant.variant_unit in ["weight", "volume"]
if @variant.product.variant_unit in ["weight", "volume"]
[value, unit_name] = @option_value_value_unit_scaled()
else
value = @variant.unit_value
unit_name = @pluralize(@variant.variant_unit_name, value)
unit_name = @pluralize(@variant.product.variant_unit_name, value)
value = parseInt(value, 10) if value == parseInt(value, 10)
@@ -58,13 +57,14 @@ angular.module("admin.products").factory "OptionValueNamer", (VariantUnitManager
# to >= 1 when expressed in it.
# If there is none available where this is true, use the smallest
# available unit.
scales = VariantUnitManager.compatibleUnitScales(@variant.variant_unit_scale, @variant.variant_unit)
product = @variant.product
scales = VariantUnitManager.compatibleUnitScales(product.variant_unit_scale, product.variant_unit)
variantUnitValue = @variant.unit_value
# sets largestScale = last element in filtered scales array
[_, ..., largestScale] = (scales.filter (s) -> variantUnitValue / s >= 1)
if (largestScale)
[largestScale, VariantUnitManager.getUnitName(largestScale, @variant.variant_unit)]
[largestScale, VariantUnitManager.getUnitName(largestScale, product.variant_unit)]
else
[scales[0], VariantUnitManager.getUnitName(scales[0], @variant.variant_unit)]
[scales[0], VariantUnitManager.getUnitName(scales[0], product.variant_unit)]

View File

@@ -8,4 +8,10 @@ angular.module("admin.resources").factory 'EnterpriseResource', ($resource) ->
isArray: true
'update':
method: 'PUT'
'removeLogo':
url: '/api/v0/enterprises/:id/logo.json'
method: 'DELETE'
'removePromoImage':
url: '/api/v0/enterprises/:id/promo_image.json'
method: 'DELETE'
})

View File

@@ -0,0 +1,6 @@
angular.module("admin.resources").factory 'ProductResource', ($resource) ->
$resource('/admin/product/:id/:action.json', {}, {
'index':
url: '/api/v0/products/bulk_products.json'
method: 'GET'
})

View File

@@ -39,6 +39,17 @@ angular.module("admin.resources").factory 'Enterprises', ($q, $filter, Enterpris
resetAttribute: (enterprise, attribute) ->
enterprise[attribute] = @pristineByID[enterprise.id][attribute]
performActionOnEnterpriseResource = (resourceAction) ->
(enterprise) ->
deferred = $q.defer()
resourceAction({id: enterprise.permalink}, ((data) =>
@pristineByID[enterprise.id] = angular.copy(data)
deferred.resolve(data)
), ((response) ->
deferred.reject(response)
))
deferred.promise
findByID: (id) ->
@byID[id]
@@ -50,3 +61,5 @@ angular.module("admin.resources").factory 'Enterprises', ($q, $filter, Enterpris
$filter('filter')(enterprises, term)
removeLogo: performActionOnEnterpriseResource(EnterpriseResource.removeLogo)
removePromoImage: performActionOnEnterpriseResource(EnterpriseResource.removePromoImage)

View File

@@ -29,13 +29,13 @@ angular.module("admin.resources").factory 'OrderCycles', ($q, $injector, OrderCy
deferred.reject(response)
deferred.promise
saveChanges: (form, params = {}) ->
saveChanges: (form) ->
changed = {}
for id, orderCycle of @byID when not @saved(orderCycle)
changed[Object.keys(changed).length] = @changesFor(orderCycle)
if Object.keys(changed).length > 0
StatusMessage.display('progress', "Saving...")
OrderCycleResource.bulkUpdate { order_cycle_set: { collection_attributes: changed }, confirm: params['confirm'], trigger_action: params['trigger_action'] }, (data) =>
OrderCycleResource.bulkUpdate { order_cycle_set: { collection_attributes: changed } }, (data) =>
for orderCycle in data
delete orderCycle.coordinator
delete orderCycle.producers
@@ -47,10 +47,8 @@ angular.module("admin.resources").factory 'OrderCycles', ($q, $injector, OrderCy
, (response) =>
if response.data.errors?
StatusMessage.display('failure', response.data.errors[0])
else if (response.data.trigger_action)
StatusMessage.display('notice', t('js.order_cycles.unsaved_changes'), response.data.trigger_action)
else
StatusMessage.display('failure', t('js.order_cycles.bulk_save_error'))
StatusMessage.display('failure', "Oh no! I was unable to save your changes.")
saved: (order_cycle) ->
@diff(order_cycle).length == 0

View File

@@ -0,0 +1,78 @@
angular.module("ofn.admin").factory "BulkProducts", (ProductResource, dataFetcher, $http) ->
new class BulkProducts
products: []
pagination: {}
fetch: (params) ->
ProductResource.index params, (data) =>
@products.length = 0
@addProducts data.products
angular.extend(@pagination, data.pagination)
cloneProduct: (product) ->
$http.post("/api/v0/products/" + product.id + "/clone").then (response) =>
dataFetcher("/api/v0/products/" + response.data.id + "?template=bulk_show").then (newProduct) =>
@unpackProduct newProduct
@insertProductAfter(product, newProduct)
updateVariantLists: (serverProducts) ->
for server_product in serverProducts
product = @findProductInList(server_product.id, @products)
product.variants = server_product.variants
@loadVariantUnitValues product
find: (id) ->
@findProductInList id, @products
findProductInList: (id, product_list) ->
products = (product for product in product_list when product.id == id)
if products.length == 0 then null else products[0]
addProducts: (products) ->
for product in products
@unpackProduct product
@products.push product
insertProductAfter: (product, newProduct) ->
index = @products.indexOf(product)
@products.splice(index + 1, 0, newProduct)
unpackProduct: (product) ->
#$scope.matchProducer product
@loadVariantUnit product
loadVariantUnit: (product) ->
product.variant_unit_with_scale =
if product.variant_unit && product.variant_unit_scale && product.variant_unit != 'items'
"#{product.variant_unit}_#{product.variant_unit_scale}"
else if product.variant_unit
product.variant_unit
else
null
@loadVariantUnitValues product if product.variants
@loadVariantUnitValue product, product.master if product.master
loadVariantUnitValues: (product) ->
for variant in product.variants
@loadVariantUnitValue product, variant
loadVariantUnitValue: (product, variant) ->
unit_value = @variantUnitValue product, variant
unit_value = if unit_value? then unit_value else ''
variant.unit_value_with_description = "#{unit_value} #{variant.unit_description || ''}".trim()
variantUnitValue: (product, variant) ->
if variant.unit_value?
if product.variant_unit_scale
variant_unit_value = @divideAsInteger variant.unit_value, product.variant_unit_scale
parseFloat(window.bigDecimal.round(variant_unit_value, 2))
else
variant.unit_value
else
null
# forces integer division to avoid javascript floating point imprecision
# using one billion as the multiplier so that it works for numbers with up to 9 decimal places
divideAsInteger: (a, b) ->
(a * 1000000000) / (b * 1000000000)

View File

@@ -0,0 +1,29 @@
angular.module("admin.indexUtils").factory 'KeyValueMapStore', (localStorageService)->
new class KeyValueMapStore
localStorageKey: ''
storableKeys: []
constructor: ->
localStorageService.setStorageType("sessionStorage")
getStoredKeyValueMap: ->
localStorageService.get(@localStorageKey) || {}
setStoredValues: (source) ->
keyValueMap = {}
for key in @storableKeys
keyValueMap[key] = source[key]
localStorageService.set(@localStorageKey, keyValueMap)
restoreValues: (target) ->
storedKeyValueMap = @getStoredKeyValueMap()
return false if _.isEmpty(storedKeyValueMap)
for k,v of storedKeyValueMap
target[k] = v
return true
clearKeyValueMap: () ->
localStorageService.remove(@localStorageKey)

View File

@@ -187,17 +187,18 @@ addVariantFromStockLocation = function() {
$('#stock_details').hide();
var variant_id = $('input.variant_autocomplete').val();
var quantity = $("input.quantity").val();
var stock_location_id = $(this).data('stock-location-id');
var quantity = $("input.quantity[data-stock-location-id='" + stock_location_id + "']").val();
var shipment = _.find(shipments, function(shipment){
return shipment.state == 'ready' || shipment.state == 'pending';
return shipment.stock_location_id == stock_location_id && (shipment.state == 'ready' || shipment.state == 'pending');
});
if(shipment==undefined){
$.ajax({
type: "POST",
url: Spree.url(Spree.routes.orders_api + "/" + order_number + "/shipments.json"),
data: { variant_id: variant_id, quantity: quantity }
data: { variant_id: variant_id, quantity: quantity, stock_location_id: stock_location_id }
}).done(function( msg ) {
window.location.reload();
}).error(function( msg ) {

View File

@@ -0,0 +1,21 @@
root = exports ? this
root.taxon_tree_menu = (obj, context) ->
base_url = Spree.url(Spree.routes.taxonomy_taxons)
admin_base_url = Spree.url(Spree.routes.admin_taxonomy_taxons)
edit_url = Spree.url(Spree.routes.admin_taxonomy_taxons + '/' + obj.attr("id") + "/edit");
create:
label: "<i class='icon-plus'></i> " + Spree.translations.add,
action: (obj) -> context.create(obj)
rename:
label: "<i class='icon-pencil'></i> " + Spree.translations.rename,
action: (obj) -> context.rename(obj)
remove:
label: "<i class='icon-trash'></i> " + Spree.translations.remove,
action: (obj) -> context.remove(obj)
edit:
separator_before: true,
label: "<i class='icon-edit'></i> " + Spree.translations.edit,
action: (obj) -> window.location = edit_url.toString()

View File

@@ -0,0 +1,139 @@
handle_ajax_error = (XMLHttpRequest, textStatus, errorThrown) ->
$.jstree.rollback(last_rollback)
$("#ajax_error").show().html("<strong>" + server_error + "</strong><br />" + taxonomy_tree_error)
handle_move = (e, data) ->
last_rollback = data.rlbk
position = data.rslt.cp
node = data.rslt.o
new_parent = data.rslt.np
url = new URL(Spree.routes.admin_taxonomy_taxons)
url.pathname = url.pathname + '/' + node.attr("id")
data = {
_method: "put",
"taxon[position]": position,
"taxon[parent_id]": if !isNaN(new_parent.attr("id")) then new_parent.attr("id") else undefined
}
$.ajax
type: "POST",
dataType: "json",
url: url.toString(),
data: data,
error: handle_ajax_error
true
handle_create = (e, data) ->
last_rollback = data.rlbk
node = data.rslt.obj
name = data.rslt.name
position = data.rslt.position
new_parent = data.rslt.parent
data = {
"taxon[name]": name,
"taxon[position]": position
"taxon[parent_id]": if !isNaN(new_parent.attr("id")) then new_parent.attr("id") else undefined
}
$.ajax
type: "POST",
dataType: "json",
url: base_url.toString(),
data: data,
error: handle_ajax_error,
success: (data,result) ->
node.attr('id', data.id)
handle_rename = (e, data) ->
last_rollback = data.rlbk
node = data.rslt.obj
name = data.rslt.new_name
# change the name inside the main input field as well if taxon is the root one
document.getElementById("taxonomy_name").value = name if node.parents("[id]").attr("id") == "taxonomy_tree"
url = new URL(base_url)
url.pathname = url.pathname + '/' + node.attr("id")
$.ajax
type: "POST",
dataType: "json",
url: url.toString(),
data: {_method: "put", "taxon[name]": name },
error: handle_ajax_error
handle_delete = (e, data) ->
last_rollback = data.rlbk
node = data.rslt.obj
delete_url = new URL(base_url)
delete_url.pathname = delete_url.pathname + '/' + node.attr("id")
if confirm(Spree.translations.are_you_sure_delete)
$.ajax
type: "POST",
dataType: "json",
url: delete_url.toString(),
data: {_method: "delete"},
error: handle_ajax_error
else
$.jstree.rollback(last_rollback)
last_rollback = null
root = exports ? this
root.setup_taxonomy_tree = (taxonomy_id) ->
if taxonomy_id != undefined
# this is defined within admin/taxonomies/edit
root.base_url = Spree.url(Spree.routes.taxonomy_taxons)
$.ajax
url: base_url.pathname.replace("/taxons", "/jstree"),
success: (taxonomy) ->
last_rollback = null
conf =
json_data:
data: taxonomy,
ajax:
url: (e) ->
base_url.pathname + '/' + e.attr('id') + '/jstree'
themes:
theme: "apple",
url: "/assets/jquery.jstree/themes/apple/style.css"
strings:
new_node: new_taxon,
loading: Spree.translations.loading + "..."
crrm:
move:
check_move: (m) ->
position = m.cp
node = m.o
new_parent = m.np
# no parent or cant drag and drop
if !new_parent || node.attr("rel") == "root"
return false
# can't drop before root
if new_parent.attr("id") == "taxonomy_tree" && position == 0
return false
true
contextmenu:
items: (obj) ->
taxon_tree_menu(obj, this)
plugins: ["themes", "json_data", "dnd", "crrm", "contextmenu"]
$("#taxonomy_tree").jstree(conf)
.bind("move_node.jstree", handle_move)
.bind("remove.jstree", handle_delete)
.bind("create.jstree", handle_create)
.bind("rename.jstree", handle_rename)
.bind "loaded.jstree", ->
$(this).jstree("core").toggle_node($('.jstree-icon').first())
$("#taxonomy_tree a").on "dblclick", (e) ->
$("#taxonomy_tree").jstree("rename", this)
# surpress form submit on enter/return
$(document).keypress (e) ->
if e.keyCode == 13
e.preventDefault()

View File

@@ -0,0 +1,58 @@
angular.module("admin.tagRules").controller "TagRulesCtrl", ($scope, $http, $filter, enterprise) ->
$scope.tagGroups = enterprise.tag_groups
$scope.defaultTagGroup = enterprise.default_tag_group
$scope.visibilityOptions = [ { id: "visible", name: t('js.tag_rules.visible') }, { id: "hidden", name: t('js.tag_rules.not_visible') } ]
$scope.updateRuleCounts = ->
index = $scope.defaultTagGroup.rules.length
for tagGroup in $filter('orderBy')($scope.tagGroups, 'position')
tagGroup.startIndex = index
index = index + tagGroup.rules.length
$scope.updateRuleCounts()
$scope.updateTagsRulesFor = (tagGroup) ->
for tagRule in tagGroup.rules
tagRule.preferred_customer_tags = (tag.text for tag in tagGroup.tags).join(",")
$scope.addNewRuleTo = (tagGroup, ruleType) ->
newRule =
id: null
is_default: tagGroup == $scope.defaultTagGroup
preferred_customer_tags: (tag.text for tag in tagGroup.tags).join(",")
type: "TagRule::#{ruleType}"
switch ruleType
when "FilterShippingMethods"
newRule.peferred_shipping_method_tags = []
newRule.preferred_matched_shipping_methods_visibility = "visible"
when "FilterPaymentMethods"
newRule.peferred_payment_method_tags = []
newRule.preferred_matched_payment_methods_visibility = "visible"
when "FilterProducts"
newRule.peferred_variant_tags = []
newRule.preferred_matched_variants_visibility = "visible"
when "FilterOrderCycles"
newRule.peferred_exchange_tags = []
newRule.preferred_matched_order_cycles_visibility = "visible"
tagGroup.rules.push(newRule)
$scope.updateRuleCounts()
$scope.addNewTag = ->
$scope.tagGroups.push { tags: [], rules: [], position: $scope.tagGroups.length + 1 }
$scope.deleteTagRule = (tagGroup, tagRule) ->
index = tagGroup.rules.indexOf(tagRule)
return unless index >= 0
if tagRule.id is null
tagGroup.rules.splice(index, 1)
$scope.updateRuleCounts()
else
if confirm("Are you sure?")
$http
method: "DELETE"
url: "/admin/enterprises/#{enterprise.id}/tag_rules/#{tagRule.id}.json"
.then ->
tagGroup.rules.splice(index, 1)
$scope.updateRuleCounts()
$scope.enterprise_form.$setDirty()

View File

@@ -0,0 +1,11 @@
angular.module("admin.tagRules").directive "invertNumber", ->
restrict: "A"
require: "ngModel"
link: (scope, element, attrs, ngModel) ->
ngModel.$parsers.push (viewValue) ->
return -parseInt(viewValue) unless isNaN(parseInt(viewValue))
viewValue
ngModel.$formatters.push (modelValue) ->
return -parseInt(modelValue) unless isNaN(parseInt(modelValue))
modelValue

View File

@@ -0,0 +1,31 @@
angular.module("admin.tagRules").directive 'newTagRuleDialog', ($rootScope, $compile, $templateCache, DialogDefaults) ->
restrict: 'A'
scope:
tagGroup: '='
addNewRuleTo: '='
link: (scope, element, attr) ->
# Compile modal template
template = $compile($templateCache.get('admin/new_tag_rule_dialog.html'))(scope)
scope.ruleTypes = [
{ id: "FilterProducts", name: t('js.tag_rules.show_hide_variants') }
{ id: "FilterShippingMethods", name: t('js.tag_rules.show_hide_shipping') }
{ id: "FilterPaymentMethods", name: t('js.tag_rules.show_hide_payment') }
{ id: "FilterOrderCycles", name: t('js.tag_rules.show_hide_order_cycles') }
]
scope.ruleType = scope.ruleTypes[0].id
# Set Dialog options
template.dialog(DialogDefaults)
# Link opening of dialog to click event on element
element.bind 'click', (e) ->
template.dialog('open')
$rootScope.$evalAsync()
scope.addRule = (tagGroup, ruleType) ->
scope.addNewRuleTo(tagGroup, ruleType)
template.dialog('close')
$rootScope.$evalAsync()
return

View File

@@ -0,0 +1,41 @@
angular.module("admin.tagRules").directive "tagRule", ->
restrict: "C"
templateUrl: "admin/tag_rules/tag_rule.html"
link: (scope, element, attrs) ->
scope.opt =
"TagRule::FilterShippingMethods":
textTop: t('js.admin.tag_rules.shipping_method_tagged_top')
textBottom: t('js.admin.tag_rules.shipping_method_tagged_bottom')
taggable: "shipping_method"
tagsAttr: "shipping_method_tags"
tagListAttr: "preferred_shipping_method_tags"
inputTemplate: "admin/tag_rules/filter_shipping_methods_input.html"
tagListFor: (rule) ->
rule.preferred_shipping_method_tags
"TagRule::FilterPaymentMethods":
textTop: t('js.admin.tag_rules.payment_method_tagged_top')
textBottom: t('js.admin.tag_rules.payment_method_tagged_bottom')
taggable: "payment_method"
tagsAttr: "payment_method_tags"
tagListAttr: "preferred_payment_method_tags"
inputTemplate: "admin/tag_rules/filter_payment_methods_input.html"
tagListFor: (rule) ->
rule.preferred_payment_method_tags
"TagRule::FilterOrderCycles":
textTop: t('js.admin.tag_rules.order_cycle_tagged_top')
textBottom: t('js.admin.tag_rules.order_cycle_tagged_bottom')
taggable: "exchange"
tagsAttr: "exchange_tags"
tagListAttr: "preferred_exchange_tags"
inputTemplate: "admin/tag_rules/filter_order_cycles_input.html"
tagListFor: (rule) ->
rule.preferred_exchange_tags
"TagRule::FilterProducts":
textTop: t('js.admin.tag_rules.inventory_tagged_top')
textBottom: t('js.admin.tag_rules.inventory_tagged_bottom')
taggable: "variant"
tagsAttr: "variant_tags"
tagListAttr: "preferred_variant_tags"
inputTemplate: "admin/tag_rules/filter_products_input.html"
tagListFor: (rule) ->
rule.preferred_variant_tags

View File

@@ -0,0 +1,6 @@
angular.module("admin.utils").directive "textangularLinksTargetBlank", () ->
restrict: 'CA'
link: (scope, element, attrs) ->
setTimeout ->
element.find(".ta-editor").scope().defaultTagAttributes.a.target = '_blank'
, 500

View File

@@ -0,0 +1,11 @@
angular.module("admin.utils").directive "textangularStrip", () ->
restrict: 'CA'
link: (scope, element, attrs) ->
scope.stripFormatting = ($html) ->
element = document.createElement("div")
element.innerHTML = String($html)
allTags = element.getElementsByTagName("*")
for child in allTags
child.removeAttribute("style")
child.removeAttribute("class")
return element.innerHTML

View File

@@ -26,8 +26,6 @@ angular.module("admin.utils").directive "variantAutocomplete", ($timeout) ->
order_cycle_id: scope.order_cycle_id
eligible_for_subscriptions: scope.eligible_for_subscriptions
include_out_of_stock: scope.include_out_of_stock
search_variants_as: scope.search_variants_as
order_id: scope.order_id
results: (data, page) ->
window.variants = data # this is how spree auto complete JS code picks up variants
results: data

View File

@@ -9,7 +9,6 @@ angular.module("admin.utils")
$window.onbeforeunload = @onBeforeUnloadHandler
$rootScope.$on "$locationChangeStart", @locationChangeStartHandler
$window.onBeforeUnloadHandler = @onBeforeUnloadHandler
# Action for regular browser navigation.
onBeforeUnloadHandler: ($event) =>

View File

@@ -10,9 +10,7 @@ angular.module("admin.utils").factory "StatusMessage", ->
statusMessage:
text: ""
style: {},
type: null,
actionName: null
style: {}
invalidMessage: ""
@@ -25,15 +23,11 @@ angular.module("admin.utils").factory "StatusMessage", ->
active: ->
@statusMessage.text != ''
display: (type, text, actionName = null) ->
display: (type, text) ->
@statusMessage.text = text
@statusMessage.type = type
@statusMessage.actionName = actionName
@statusMessage.style = @types[type].style
null
clear: ->
@statusMessage.text = ''
@statusMessage.style = {}
@statusMessage.type = null
@statusMessage.actionName = null

View File

@@ -2,10 +2,10 @@ angular.module("admin.variantOverrides").directive "trackInheritance", (VariantO
require: "ngModel"
link: (scope, element, attrs, ngModel) ->
# This is a bit hacky, but it allows us to load the inherit property on the VO, but then not submit it
scope.inherit = angular.equals scope.variantOverrides[scope.hub_id][scope.variant.id], VariantOverrides.newFor scope.hub_id, scope.variant
scope.inherit = angular.equals scope.variantOverrides[scope.hub_id][scope.variant.id], VariantOverrides.newFor scope.hub_id, scope.variant.id
ngModel.$parsers.push (viewValue) ->
if ngModel.$dirty && viewValue
DirtyVariantOverrides.inherit scope.hub_id, scope.variant, scope.variantOverrides[scope.hub_id][scope.variant.id].id
DirtyVariantOverrides.inherit scope.hub_id, scope.variant.id, scope.variantOverrides[scope.hub_id][scope.variant.id].id
scope.displayDirty()
viewValue

View File

@@ -2,8 +2,4 @@ angular.module("admin.variantOverrides").filter "hubPermissions", ($filter) ->
return (products, hubPermissions, hub_id) ->
return [] if !hub_id
return [] if !hubPermissions[hub_id]
return $filter('filter')(products, ((product) ->
for variant in product.variants
return hubPermissions[hub_id].indexOf(variant.producer_id) > -1
), true)
return $filter('filter')(products, ((product) -> hubPermissions[hub_id].indexOf(product.producer_id) > -1), true)

View File

@@ -12,11 +12,11 @@ angular.module("admin.variantOverrides").factory "DirtyVariantOverrides", ($http
@add(hub_id, variant_id, vo_id)
@dirtyVariantOverrides[hub_id][variant_id][attr] = value
inherit: (hub_id, variant, vo_id) ->
@add(hub_id, variant.id, vo_id)
blankVo = angular.copy(VariantOverrides.inherit(hub_id, variant))
inherit: (hub_id, variant_id, vo_id) ->
@add(hub_id, variant_id, vo_id)
blankVo = angular.copy(VariantOverrides.inherit(hub_id, variant_id))
delete blankVo[attr] for attr, value of blankVo when attr not in @requiredAttrs()
@dirtyVariantOverrides[hub_id][variant.id] = blankVo
@dirtyVariantOverrides[hub_id][variant_id] = blankVo
count: ->
count = 0

View File

@@ -13,18 +13,17 @@ angular.module("admin.variantOverrides").factory "VariantOverrides", (variantOve
@variantOverrides[hub.id] ||= {}
for product in products
for variant in product.variants
@inherit(hub.id, variant) unless @variantOverrides[hub.id][variant.id]
@inherit(hub.id, variant.id) unless @variantOverrides[hub.id][variant.id]
inherit: (hub_id, variant) ->
inherit: (hub_id, variant_id) ->
# This method is called from the trackInheritance directive, to reinstate inheritance
@variantOverrides[hub_id][variant.id] ||= {}
angular.extend @variantOverrides[hub_id][variant.id], @newFor(hub_id, variant)
@variantOverrides[hub_id][variant_id] ||= {}
angular.extend @variantOverrides[hub_id][variant_id], @newFor hub_id, variant_id
newFor: (hub_id, variant) ->
newFor: (hub_id, variant_id) ->
# These properties need to match those checked in VariantOverrideSet.deletable?
hub_id: hub_id
variant_id: variant.id
producer_id: variant.producer_id
variant_id: variant_id
sku: null
price: null
count_on_hand: null

View File

@@ -17,6 +17,7 @@
#= require angular-google-maps.min.js
#= require ../shared/mm-foundation-tpls-0.9.0-20180826174721.min.js
#= require ../shared/ng-infinite-scroll.min.js
#= require ../shared/angular-local-storage.js
#= require ../shared/angular-slideables.js
#= require ../shared/shared
#= require_tree ../shared/directives

View File

@@ -4,18 +4,15 @@ angular.module('Darkswarm').controller "ProductsCtrl", ($scope, $sce, $filter, $
$scope.query = ""
$scope.taxonSelectors = FilterSelectorsService.createSelectors()
$scope.propertySelectors = FilterSelectorsService.createSelectors()
$scope.producerPropertySelectors = FilterSelectorsService.createSelectors()
$scope.filtersActive = true
$scope.page = 1
$scope.per_page = 10
$scope.order_cycle = OrderCycle.order_cycle
$scope.supplied_taxons = null
$scope.supplied_properties = null
$scope.supplied_producer_properties = null
$scope.showFilterSidebar = false
$scope.activeTaxons = []
$scope.activeProperties = []
$scope.activeProducerProperties = []
# Update filters after initial load of shop tab
$timeout =>
@@ -48,12 +45,6 @@ angular.module('Darkswarm').controller "ProductsCtrl", ($scope, $sce, $filter, $
$scope.supplied_properties[property.id] = Properties.properties_by_id[property.id]
)
OrderCycleResource.producerProperties params, (data)=>
$scope.supplied_producer_properties = {}
data.map( (property) ->
$scope.supplied_producer_properties[property.id] = Properties.properties_by_id[property.id]
)
$scope.loadMore = ->
if ($scope.page * $scope.per_page) <= Products.products.length
$scope.loadMoreProducts()
@@ -61,7 +52,6 @@ angular.module('Darkswarm').controller "ProductsCtrl", ($scope, $sce, $filter, $
$scope.$watch 'query', (newValue, oldValue) -> $scope.loadProducts() if newValue != oldValue
$scope.$watchCollection 'activeTaxons', (newValue, oldValue) -> $scope.loadProducts() if newValue != oldValue
$scope.$watchCollection 'activeProperties', (newValue, oldValue) -> $scope.loadProducts() if newValue != oldValue
$scope.$watchCollection 'activeProducerProperties', (newValue, oldValue) -> $scope.loadProducts() if newValue != oldValue
$scope.loadProducts = ->
$scope.page = 1
@@ -76,10 +66,9 @@ angular.module('Darkswarm').controller "ProductsCtrl", ($scope, $sce, $filter, $
id: $scope.order_cycle.order_cycle_id,
page: page || $scope.page,
per_page: $scope.per_page,
'q[name_or_meta_keywords_or_variants_display_as_or_variants_display_name_or_variants_supplier_name_cont]': $scope.query,
'q[name_or_meta_keywords_or_variants_display_as_or_variants_display_name_or_supplier_name_cont]': $scope.query,
'q[with_properties][]': $scope.activeProperties,
'q[with_variants_supplier_properties][]': $scope.activeProducerProperties,
'q[variants_primary_taxon_id_in_any][]': $scope.activeTaxons
'q[primary_taxon_id_in_any][]': $scope.activeTaxons
}
$scope.searchKeypress = (e)->
@@ -97,12 +86,6 @@ angular.module('Darkswarm').controller "ProductsCtrl", ($scope, $sce, $filter, $
Properties.properties_by_id[property_id].name
).join($scope.filtersJoinWord()) if $scope.activeProperties?
$scope.appliedProducerPropertiesList = ->
$scope.activeProducerProperties.map( (property_id) ->
Properties.properties_by_id[property_id].name
).join($scope.filtersJoinWord()) if $scope.activeProducerProperties?
$scope.filtersJoinWord = ->
$sce.trustAsHtml(" <span class='join-word'>#{t('products_or')}</span> ")
@@ -116,7 +99,6 @@ angular.module('Darkswarm').controller "ProductsCtrl", ($scope, $sce, $filter, $
$scope.clearFilters = ->
$scope.taxonSelectors.clearAll()
$scope.propertySelectors.clearAll()
$scope.producerPropertySelectors.clearAll()
$scope.refreshStaleData = ->
# If the products template has already been loaded but the controller is being initialized
@@ -127,7 +109,7 @@ angular.module('Darkswarm').controller "ProductsCtrl", ($scope, $sce, $filter, $
$scope.loadProducts()
$scope.filtersCount = () ->
$scope.taxonSelectors.totalActive() + $scope.propertySelectors.totalActive() + $scope.producerPropertySelectors.totalActive()
$scope.taxonSelectors.totalActive() + $scope.propertySelectors.totalActive()
$scope.toggleFilterSidebar = ->
$scope.showFilterSidebar = !$scope.showFilterSidebar

View File

@@ -42,17 +42,8 @@ angular.module('Darkswarm').controller "RegistrationCtrl", ($scope, Registration
$scope.toggleAddressConfirmed = ->
$scope.addressConfirmed = !$scope.addressConfirmed
if $scope.addressConfirmed
$scope.setLatLongIfUsingOpenStreetMap()
$scope.enterprise.address.latitude = $scope.latLong.latitude
$scope.enterprise.address.longitude = $scope.latLong.longitude
else
$scope.enterprise.address.latitude = null
$scope.enterprise.address.longitude = null
# When OpenStreetMaps is enabled the latitude and longitude are calculated via a Stimulus
# controller, so they need to be read from data properties to be accessible here.
$scope.setLatLongIfUsingOpenStreetMap = ->
openStreetMap = document.getElementById("open-street-map")
if !$scope.latLong && openStreetMap && openStreetMap.dataset.latitude && openStreetMap.dataset.longitude
$scope.latLong = { latitude: openStreetMap.dataset.latitude, longitude: openStreetMap.dataset.longitude }

View File

@@ -0,0 +1,2 @@
angular.module('Darkswarm').controller "TmpCtrl", ($scope)->
$scope.test = {foo: "bar"}

View File

@@ -1,6 +1,7 @@
angular.module("Darkswarm", [
'ngResource',
'mm.foundation',
'LocalStorageModule',
'infinite-scroll',
'angular-flash.service',
'templates',

View File

@@ -1,6 +1,6 @@
angular.module('Darkswarm').directive "activeTableHubLink", (CurrentHub, CurrentOrder) ->
# Change the text of the hub link based on CurrentHub
# To be used with ofnChangeHub
# To be used with ofnEmptiesCart
# Takes "change" and "shop" as text string attributes
restrict: "A"
scope:

View File

@@ -0,0 +1,11 @@
angular.module('Darkswarm').directive "darkerBackground", ->
restrict: "A"
link: (scope, elm, attr)->
toggleClass = (value) ->
elm.closest('.page-view').toggleClass("with-darker-background", value)
toggleClass(true)
# if an OrderCycle is selected, disable darker background
scope.$watch 'order_cycle.order_cycle_id', (newvalue, oldvalue) ->
toggleClass(false) if newvalue

View File

@@ -0,0 +1,14 @@
# Allows disabling of link buttons via disabled attribute.
# This is normally ignored, ie the link appears disabled but is still clickable.
angular.module('Darkswarm').directive "disableDynamically", ->
restrict: 'A'
link: (scope, element, attrs) ->
element.on 'click', (e) ->
if attrs.disabled
e.preventDefault()
return
scope.$on "$destroy", ->
element.off("click")

View File

@@ -0,0 +1,10 @@
angular.module('Darkswarm').directive "ofnDisableScroll", ()->
# Stops scrolling from incrementing or decrementing input value
# Useful for number inputs
restrict: 'A'
link: (scope, element, attrs)->
element.bind 'focus', ->
element.bind 'mousewheel', (e)->
e.preventDefault()
element.bind 'blur', ->
element.unbind 'mousewheel'

View File

@@ -0,0 +1,7 @@
angular.module('Darkswarm').directive "ofnInlineAlert", ->
restrict: 'A'
scope: true
link: (scope, elem, attrs) ->
scope.visible = true
scope.close = ->
scope.visible = false

View File

@@ -0,0 +1,5 @@
angular.module('Darkswarm').directive "integer", ->
restrict: 'A'
link: (scope, elem, attr) ->
elem.bind 'input', ->
elem.val Math.round(elem.val())

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