Merge remote-tracking branch 'origin/master' into discourse-sso

This commit is contained in:
Maikel Linke
2016-02-19 11:02:59 +11:00
150 changed files with 1906 additions and 842 deletions

13
.codeclimate.yml Normal file
View File

@@ -0,0 +1,13 @@
engines:
rubocop:
enabled: true
scss-lint:
enabled: true
ratings:
paths:
- app/**
- lib/**
- "**.rb"
exclude_paths:
- spec/**/*
- vendor/**/*

29
.rubocop.yml Normal file
View File

@@ -0,0 +1,29 @@
AllCops:
Include:
- '**/Rakefile'
- '**/config.ru'
Exclude:
- 'db/**/*'
- 'config/**/*'
- 'script/**/*'
- 'spec/**/*'
- !ruby/regexp /old_and_unused\.rb$/
Documentation:
Enabled: false
Style/EmptyLinesAroundClassBody:
Enabled: false
Style/BracesAroundHashParameters:
Enabled: false
Metrics/LineLength:
Enabled: false
Max: 120
MethodLength:
Enabled: false
StringLiterals:
Enabled: false

3
.scss-lint.yml Normal file
View File

@@ -0,0 +1,3 @@
scss_files: 'app/assets/stylesheets/**/*.css.scss'
exclude: 'app/assets/stylesheets/shared/**'

View File

@@ -14,12 +14,13 @@ env:
global:
- TZ="Australia/Melbourne"
- TIMEZONE="Australia/Melbourne"
- CI_NODE_TOTAL=5
matrix:
- TEST_CASES="./spec/features/admin" GITHUB_DEPLOY="true"
- TEST_CASES="./spec/features/consumer ./spec/serializers ./spec/performance"
- TEST_CASES="./spec/models"
- TEST_CASES="./spec/controllers ./spec/views ./spec/jobs"
- TEST_CASES="./spec/requests ./spec/helpers ./spec/mailers ./spec/lib" KARMA="true"
- CI_NODE_INDEX=0
- CI_NODE_INDEX=1
- CI_NODE_INDEX=2
- CI_NODE_INDEX=3
- CI_NODE_INDEX=4 KARMA="true" GITHUB_DEPLOY="true"
before_script:
- cp config/database.travis.yml config/database.yml
@@ -35,8 +36,9 @@ before_script:
fi
script:
- '[ "$KARMA" = "true" ] && bundle exec rake karma:run || echo "Skipping karma run"'
- "bundle exec rspec $TEST_CASES"
- 'if [ "$KARMA" = "true" ]; then bundle exec rake karma:run; else echo "Skipping karma run"; fi'
#- "KNAPSACK_GENERATE_REPORT=true bundle exec rspec spec"
- "bundle exec rake knapsack:rspec"
after_success:
- >

View File

@@ -1,7 +1,7 @@
# Contributing
We love pull requests from everyone. Here are some instructions for
contributing code to Open Food Network.
contributing code to Open Food Network. See the [developer wiki](https://github.com/openfoodfoundation/openfoodnetwork/wiki) for more information.
Fork, then clone the repo:

View File

@@ -105,6 +105,7 @@ group :test, :development do
gem 'json_spec'
gem 'unicorn-rails'
gem 'atomic'
gem 'knapsack'
end
group :test do

View File

@@ -122,7 +122,7 @@ GEM
rack-test (~> 0.6.1)
sprockets (~> 2.2.1)
active_link_to (1.0.0)
active_model_serializers (0.8.1)
active_model_serializers (0.8.3)
activemodel (>= 3.0)
activemerchant (1.48.0)
activesupport (>= 3.2.14, < 5.0.0)
@@ -431,6 +431,9 @@ GEM
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
kgio (2.9.3)
knapsack (1.5.1)
rake
timecop (>= 0.1.0)
launchy (2.1.2)
addressable (~> 2.3)
letter_opener (1.0.0)
@@ -684,6 +687,7 @@ DEPENDENCIES
immigrant
jquery-rails
json_spec
knapsack
letter_opener
momentjs-rails
newrelic_rpm

View File

@@ -1,3 +1,4 @@
[![Build Status](https://travis-ci.org/openfoodfoundation/openfoodnetwork.svg?branch=master)](https://travis-ci.org/openfoodfoundation/openfoodnetwork)
[![Code Climate](https://codeclimate.com/github/openfoodfoundation/openfoodnetwork.png)](https://codeclimate.com/github/openfoodfoundation/openfoodnetwork)
# Open Food Network
@@ -14,10 +15,12 @@ We're part of global movement - get involved!
## Getting started
Below are instructions for setting up a development environment for Open Food Network. If you're interested in provisioning a server, see [the project's Ansible playbooks](https://github.com/openfoodfoundation/ofn_deployment).
Below are instructions for setting up a development environment for Open Food Network. More information is in the [developer wiki](https://github.com/openfoodfoundation/openfoodnetwork/wiki).
If you're interested in provisioning a server, see [the project's Ansible playbooks](https://github.com/openfoodfoundation/ofn_deployment).
## Dependencies
### Dependencies
* Rails 3.2.x
* Ruby 2.1.5
@@ -26,7 +29,7 @@ Below are instructions for setting up a development environment for Open Food Ne
* See Gemfile for a list of gems required
## Get it
### Get it
The source code is managed with Git (a version control system) and
hosted at GitHub.
@@ -40,7 +43,7 @@ You can download the source with the command:
git clone https://github.com/openfoodfoundation/openfoodnetwork.git
## Get it running
### Get it running
For those new to Rails, the following tutorial will help get you up to speed with configuring a Rails environment: http://guides.rubyonrails.org/getting_started.html .
@@ -58,6 +61,15 @@ Configure the site:
cp config/application.yml.example config/application.yml
edit config/application.yml
Create a PostgreSQL user:
* Login as your system postrgresql priviledged user: `sudo -i -u postgres` (this may vary on your OS). Now your prompt looks like: `[postgres@your_host ~]$`
* Create the `ofn` database superuser and give it the password `f00d`:
```
createuser -s -P ofn
```
Create the development and test databases, using the settings specified in `config/database.yml`, and populate them with a schema and seed data:
rake db:setup
@@ -71,7 +83,7 @@ At long last, your dreams of spinning up a development server can be realised:
rails server
## Testing
### Testing
Tests, both unit and integration, are based on RSpec. To run the test suite, first prepare the test database:
@@ -97,6 +109,10 @@ usage instructions.
* Will Marshall (http://soundcloud.com/willmarshall)
* Laura Summers (https://github.com/summerscope)
* Maikel Linke (https://github.com/mkllnk)
* Lynne Davis (https://github.com/lin-d-hop)
* Paul Mackay (https://github.com/pmackay)
* Steve Petitt (https://github.com/stveep)
## Licence

View File

@@ -5,3 +5,5 @@
require File.expand_path('../config/application', __FILE__)
Openfoodnetwork::Application.load_tasks
Knapsack.load_tasks if defined?(Knapsack)

View File

@@ -39,6 +39,7 @@
//= require ./taxons/taxons
//= require ./utils/utils
//= require ./users/users
//= require ./variant_overrides/variant_overrides
//= require textAngular.min.js
//= require textAngular-sanitize.min.js
//= require ../shared/bindonce.min.js

View File

@@ -352,6 +352,9 @@ filterSubmitVariant = (variant) ->
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

View File

@@ -1,67 +0,0 @@
angular.module("ofn.admin").controller "AdminVariantOverridesCtrl", ($scope, $timeout, Indexer, SpreeApiAuth, PagedFetcher, StatusMessage, hubs, producers, hubPermissions, VariantOverrides, DirtyVariantOverrides) ->
$scope.hubs = hubs
$scope.hub = null
$scope.products = []
$scope.producers = Indexer.index producers
$scope.hubPermissions = hubPermissions
$scope.variantOverrides = VariantOverrides.variantOverrides
$scope.StatusMessage = StatusMessage
$scope.initialise = ->
SpreeApiAuth.authorise()
.then ->
$scope.spree_api_key_ok = true
$scope.fetchProducts()
.catch (message) ->
$scope.api_error_msg = message
$scope.fetchProducts = ->
url = "/api/products/overridable?page=::page::;per_page=100"
PagedFetcher.fetch url, (data) => $scope.addProducts data.products
$scope.addProducts = (products) ->
$scope.products = $scope.products.concat products
VariantOverrides.ensureDataFor hubs, products
$scope.selectHub = ->
$scope.hub = (hub for hub in hubs when hub.id == $scope.hub_id)[0]
$scope.displayDirty = ->
if DirtyVariantOverrides.count() > 0
num = if DirtyVariantOverrides.count() == 1 then "one override" else "#{DirtyVariantOverrides.count()} overrides"
StatusMessage.display 'notice', "Changes to #{num} remain unsaved."
else
StatusMessage.clear()
$scope.update = ->
if DirtyVariantOverrides.count() == 0
StatusMessage.display 'alert', 'No changes to save.'
else
StatusMessage.display 'progress', 'Saving...'
DirtyVariantOverrides.save()
.success (updatedVos) ->
DirtyVariantOverrides.clear()
VariantOverrides.updateIds updatedVos
$timeout -> StatusMessage.display 'success', 'Changes saved.'
.error (data, status) ->
$timeout -> StatusMessage.display 'failure', $scope.updateError(data, status)
$scope.updateError = (data, status) ->
if status == 401
"I couldn't get authorisation to save those changes, so they remain unsaved."
else if status == 400 && data.errors?
errors = []
for field, field_errors of data.errors
errors = errors.concat field_errors
errors = errors.join ', '
"I had some trouble saving: #{errors}"
else
"Oh no! I was unable to save your changes."

View File

@@ -1,8 +1,9 @@
angular.module("admin.dropdown").directive "ofnDropDown", ($document) ->
restrict: 'C'
link: (scope, element, attrs) ->
outsideClickListener = (event) ->
unless $(event.target).is("div.ofn_drop_down##{attrs.id} div.menu") ||
$(event.target).parents("div.ofn_drop_down##{attrs.id} div.menu").length > 0
unless $(event.target).is("div.ofn-drop-down##{attrs.id} div.menu") ||
$(event.target).parents("div.ofn-drop-down##{attrs.id} div.menu").length > 0
scope.$emit "offClick"
element.click (event) ->

View File

@@ -0,0 +1,28 @@
angular.module("admin.indexUtils").directive "ofnSelect2", ($timeout, blankOption) ->
require: 'ngModel'
restrict: 'C'
scope:
data: "="
minSearch: "@?"
text: "@?"
blank: "=?"
link: (scope, element, attrs, ngModel) ->
$timeout ->
scope.text ||= 'name'
scope.data.unshift(scope.blank) if scope.blank? && typeof scope.blank is "object"
element.select2
minimumResultsForSearch: scope.minSearch || 0
data: { results: scope.data, text: scope.text }
initSelection: (element, callback) ->
callback scope.data[0]
formatSelection: (item) ->
item[scope.text]
formatResult: (item) ->
item[scope.text]
attrs.$observe 'disabled', (value) ->
element.select2('enable', !value)
ngModel.$formatters.push (value) ->
element.select2('val', value)
value

View File

@@ -1,7 +0,0 @@
angular.module("admin.indexUtils").directive "saveBar", ->
restrict: "E"
scope:
save: "&"
saving: "&"
dirty: "&"
templateUrl: "admin/save_bar.html"

View File

@@ -0,0 +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
angular.module("admin.indexUtils").filter "attrFilter", ($filter) ->
return (objects, filters) ->
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

@@ -1,4 +1,4 @@
angular.module("ofn.admin").factory "dataFetcher", [
angular.module("admin.indexUtils").factory "dataFetcher", [
"$http", "$q"
($http, $q) ->
return (dataLocation) ->
@@ -9,4 +9,4 @@ angular.module("ofn.admin").factory "dataFetcher", [
deferred.reject()
deferred.promise
]
]

View File

@@ -4,7 +4,7 @@
# Indexer.index producers
# -> {1: {id: 1, name: 'one'}, 2: {id: 2, name: 'two'}}
angular.module("ofn.admin").factory 'Indexer', ->
angular.module("admin.indexUtils").factory 'Indexer', ->
new class Indexer
index: (data, key='id') ->
index = {}

View File

@@ -1,4 +1,4 @@
angular.module("ofn.admin").factory "PagedFetcher", (dataFetcher) ->
angular.module("admin.indexUtils").factory "PagedFetcher", (dataFetcher) ->
new class PagedFetcher
# Given a URL like http://example.com/foo?page=::page::&per_page=20
# And the response includes an attribute pages with the number of pages to fetch
@@ -13,4 +13,4 @@ angular.module("ofn.admin").factory "PagedFetcher", (dataFetcher) ->
processData data
urlForPage: (url, page) ->
url.replace("::page::", page)
url.replace("::page::", page)

View File

@@ -1,4 +1,4 @@
angular.module("ofn.admin").factory "SpreeApiAuth", ($q, $http, SpreeApiKey) ->
angular.module("admin.indexUtils").factory "SpreeApiAuth", ($q, $http, SpreeApiKey) ->
new class SpreeApiAuth
authorise: ->
deferred = $q.defer()

View File

@@ -1,7 +1,6 @@
angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout, $http, $q, Columns, Dereferencer, Orders, LineItems, Enterprises, OrderCycles, blankOption, VariantUnitManager, RequestMonitor) ->
angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout, $http, $q, StatusMessage, Columns, Dereferencer, Orders, LineItems, Enterprises, OrderCycles, blankOption, VariantUnitManager, RequestMonitor) ->
$scope.initialized = false
$scope.RequestMonitor = RequestMonitor
$scope.saving = false
$scope.filteredLineItems = []
$scope.confirmDelete = true
$scope.startDate = formatDate daysFromToday -7
@@ -55,6 +54,7 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
Dereferencer.dereferenceAttr $scope.lineItems, "supplier", Enterprises.enterprisesByID
Dereferencer.dereferenceAttr $scope.lineItems, "order", Orders.ordersByID
$scope.bulk_order_form.$setPristine()
StatusMessage.clear()
unless $scope.initialized
$scope.initialized = true
$timeout ->
@@ -62,16 +62,20 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
$scope.refreshData()
$scope.submit = =>
$scope.$watch 'bulk_order_form.$dirty', (newVal, oldVal) ->
if newVal == true
StatusMessage.display 'notice', "You have unsaved changes"
$scope.submit = ->
if $scope.bulk_order_form.$valid
$scope.saving = true
StatusMessage.display 'progress', "Saving..."
$q.all(LineItems.saveAll()).then(->
StatusMessage.display 'success', "All changes saved"
$scope.bulk_order_form.$setPristine()
$scope.saving = false
).catch ->
alert "Some errors must be resolved be before you can update orders.\nAny fields with red borders contain errors."
StatusMessage.display 'failure', "Fields with red borders contain errors."
else
alert "Some errors must be resolved be before you can update orders.\nAny fields with red borders contain errors."
StatusMessage.display 'failure', "Fields with red borders contain errors."
$scope.deleteLineItem = (lineItem) ->
if ($scope.confirmDelete && confirm("Are you sure?")) || !$scope.confirmDelete

View File

@@ -1 +1 @@
angular.module("admin.lineItems", ["admin.indexUtils", "admin.products", "admin.orders", "admin.enterprises", "admin.orderCycles"])
angular.module("admin.lineItems", ["admin.indexUtils", "admin.utils", "admin.products", "admin.orders", "admin.enterprises", "admin.orderCycles"])

View File

@@ -0,0 +1,83 @@
angular.module('admin.orderCycles')
.controller 'AdminCreateOrderCycleCtrl', ($scope, $filter, OrderCycle, Enterprise, EnterpriseFee, ocInstance, StatusMessage) ->
$scope.enterprises = Enterprise.index(coordinator_id: ocInstance.coordinator_id)
$scope.supplier_enterprises = Enterprise.producer_enterprises
$scope.distributor_enterprises = Enterprise.hub_enterprises
$scope.supplied_products = Enterprise.supplied_products
$scope.enterprise_fees = EnterpriseFee.index(coordinator_id: ocInstance.coordinator_id)
$scope.OrderCycle = OrderCycle
$scope.order_cycle = OrderCycle.new({ coordinator_id: ocInstance.coordinator_id})
$scope.StatusMessage = StatusMessage
$scope.loaded = ->
Enterprise.loaded && EnterpriseFee.loaded
$scope.suppliedVariants = (enterprise_id) ->
Enterprise.suppliedVariants(enterprise_id)
$scope.exchangeSelectedVariants = (exchange) ->
OrderCycle.exchangeSelectedVariants(exchange)
$scope.setExchangeVariants = (exchange, variants, selected) ->
OrderCycle.setExchangeVariants(exchange, variants, selected)
$scope.enterpriseTotalVariants = (enterprise) ->
Enterprise.totalVariants(enterprise)
$scope.productSuppliedToOrderCycle = (product) ->
OrderCycle.productSuppliedToOrderCycle(product)
$scope.variantSuppliedToOrderCycle = (variant) ->
OrderCycle.variantSuppliedToOrderCycle(variant)
$scope.incomingExchangeVariantsFor = (enterprise_id) ->
$filter('filterExchangeVariants')(OrderCycle.incomingExchangesVariants(), $scope.order_cycle.visible_variants_for_outgoing_exchanges[enterprise_id])
$scope.exchangeDirection = (exchange) ->
OrderCycle.exchangeDirection(exchange)
$scope.enterprisesWithFees = ->
$scope.enterprises[id] for id in OrderCycle.participatingEnterpriseIds() when $scope.enterpriseFeesForEnterprise(id).length > 0
$scope.toggleProducts = ($event, exchange) ->
$event.preventDefault()
OrderCycle.toggleProducts(exchange)
$scope.enterpriseFeesForEnterprise = (enterprise_id) ->
EnterpriseFee.forEnterprise(parseInt(enterprise_id))
$scope.addSupplier = ($event) ->
$event.preventDefault()
OrderCycle.addSupplier($scope.new_supplier_id)
$scope.addDistributor = ($event) ->
$event.preventDefault()
OrderCycle.addDistributor($scope.new_distributor_id)
$scope.removeExchange = ($event, exchange) ->
$event.preventDefault()
OrderCycle.removeExchange(exchange)
$scope.addCoordinatorFee = ($event) ->
$event.preventDefault()
OrderCycle.addCoordinatorFee()
$scope.removeCoordinatorFee = ($event, index) ->
$event.preventDefault()
OrderCycle.removeCoordinatorFee(index)
$scope.addExchangeFee = ($event, exchange) ->
$event.preventDefault()
OrderCycle.addExchangeFee(exchange)
$scope.removeExchangeFee = ($event, exchange, index) ->
$event.preventDefault()
OrderCycle.removeExchangeFee(exchange, index)
$scope.removeDistributionOfVariant = (variant_id) ->
OrderCycle.removeDistributionOfVariant(variant_id)
$scope.submit = (destination) ->
OrderCycle.create(destination)

View File

@@ -0,0 +1,84 @@
angular.module('admin.orderCycles')
.controller 'AdminEditOrderCycleCtrl', ($scope, $filter, $location, OrderCycle, Enterprise, EnterpriseFee, StatusMessage) ->
order_cycle_id = $location.absUrl().match(/\/admin\/order_cycles\/(\d+)/)[1]
$scope.enterprises = Enterprise.index(order_cycle_id: order_cycle_id)
$scope.supplier_enterprises = Enterprise.producer_enterprises
$scope.distributor_enterprises = Enterprise.hub_enterprises
$scope.supplied_products = Enterprise.supplied_products
$scope.enterprise_fees = EnterpriseFee.index(order_cycle_id: order_cycle_id)
$scope.OrderCycle = OrderCycle
$scope.order_cycle = OrderCycle.load(order_cycle_id)
$scope.StatusMessage = StatusMessage
$scope.loaded = ->
Enterprise.loaded && EnterpriseFee.loaded && OrderCycle.loaded
$scope.suppliedVariants = (enterprise_id) ->
Enterprise.suppliedVariants(enterprise_id)
$scope.exchangeSelectedVariants = (exchange) ->
OrderCycle.exchangeSelectedVariants(exchange)
$scope.setExchangeVariants = (exchange, variants, selected) ->
OrderCycle.setExchangeVariants(exchange, variants, selected)
$scope.enterpriseTotalVariants = (enterprise) ->
Enterprise.totalVariants(enterprise)
$scope.productSuppliedToOrderCycle = (product) ->
OrderCycle.productSuppliedToOrderCycle(product)
$scope.variantSuppliedToOrderCycle = (variant) ->
OrderCycle.variantSuppliedToOrderCycle(variant)
$scope.incomingExchangeVariantsFor = (enterprise_id) ->
$filter('filterExchangeVariants')(OrderCycle.incomingExchangesVariants(), $scope.order_cycle.visible_variants_for_outgoing_exchanges[enterprise_id])
$scope.exchangeDirection = (exchange) ->
OrderCycle.exchangeDirection(exchange)
$scope.enterprisesWithFees = ->
$scope.enterprises[id] for id in OrderCycle.participatingEnterpriseIds() when $scope.enterpriseFeesForEnterprise(id).length > 0
$scope.toggleProducts = ($event, exchange) ->
$event.preventDefault()
OrderCycle.toggleProducts(exchange)
$scope.enterpriseFeesForEnterprise = (enterprise_id) ->
EnterpriseFee.forEnterprise(parseInt(enterprise_id))
$scope.addSupplier = ($event) ->
$event.preventDefault()
OrderCycle.addSupplier($scope.new_supplier_id)
$scope.addDistributor = ($event) ->
$event.preventDefault()
OrderCycle.addDistributor($scope.new_distributor_id)
$scope.removeExchange = ($event, exchange) ->
$event.preventDefault()
OrderCycle.removeExchange(exchange)
$scope.addCoordinatorFee = ($event) ->
$event.preventDefault()
OrderCycle.addCoordinatorFee()
$scope.removeCoordinatorFee = ($event, index) ->
$event.preventDefault()
OrderCycle.removeCoordinatorFee(index)
$scope.addExchangeFee = ($event, exchange) ->
$event.preventDefault()
OrderCycle.addExchangeFee(exchange)
$scope.removeExchangeFee = ($event, exchange, index) ->
$event.preventDefault()
OrderCycle.removeExchangeFee(exchange, index)
$scope.removeDistributionOfVariant = (variant_id) ->
OrderCycle.removeDistributionOfVariant(variant_id)
$scope.submit = (destination) ->
OrderCycle.update(destination)

View File

@@ -1,204 +0,0 @@
angular.module('admin.orderCycles', ['ngResource', 'admin.utils'])
.controller('AdminCreateOrderCycleCtrl', ['$scope', '$filter', 'OrderCycle', 'Enterprise', 'EnterpriseFee', 'ocInstance', 'StatusMessage', ($scope, $filter, OrderCycle, Enterprise, EnterpriseFee, ocInstance, StatusMessage) ->
$scope.enterprises = Enterprise.index(coordinator_id: ocInstance.coordinator_id)
$scope.supplier_enterprises = Enterprise.producer_enterprises
$scope.distributor_enterprises = Enterprise.hub_enterprises
$scope.supplied_products = Enterprise.supplied_products
$scope.enterprise_fees = EnterpriseFee.index(coordinator_id: ocInstance.coordinator_id)
$scope.OrderCycle = OrderCycle
$scope.order_cycle = OrderCycle.new({ coordinator_id: ocInstance.coordinator_id})
$scope.StatusMessage = StatusMessage
$scope.loaded = ->
Enterprise.loaded && EnterpriseFee.loaded
$scope.suppliedVariants = (enterprise_id) ->
Enterprise.suppliedVariants(enterprise_id)
$scope.exchangeSelectedVariants = (exchange) ->
OrderCycle.exchangeSelectedVariants(exchange)
$scope.setExchangeVariants = (exchange, variants, selected) ->
OrderCycle.setExchangeVariants(exchange, variants, selected)
$scope.enterpriseTotalVariants = (enterprise) ->
Enterprise.totalVariants(enterprise)
$scope.productSuppliedToOrderCycle = (product) ->
OrderCycle.productSuppliedToOrderCycle(product)
$scope.variantSuppliedToOrderCycle = (variant) ->
OrderCycle.variantSuppliedToOrderCycle(variant)
$scope.incomingExchangeVariantsFor = (enterprise_id) ->
$filter('filterExchangeVariants')(OrderCycle.incomingExchangesVariants(), $scope.order_cycle.visible_variants_for_outgoing_exchanges[enterprise_id])
$scope.exchangeDirection = (exchange) ->
OrderCycle.exchangeDirection(exchange)
$scope.enterprisesWithFees = ->
$scope.enterprises[id] for id in OrderCycle.participatingEnterpriseIds() when $scope.enterpriseFeesForEnterprise(id).length > 0
$scope.toggleProducts = ($event, exchange) ->
$event.preventDefault()
OrderCycle.toggleProducts(exchange)
$scope.enterpriseFeesForEnterprise = (enterprise_id) ->
EnterpriseFee.forEnterprise(parseInt(enterprise_id))
$scope.addSupplier = ($event) ->
$event.preventDefault()
OrderCycle.addSupplier($scope.new_supplier_id)
$scope.addDistributor = ($event) ->
$event.preventDefault()
OrderCycle.addDistributor($scope.new_distributor_id)
$scope.removeExchange = ($event, exchange) ->
$event.preventDefault()
OrderCycle.removeExchange(exchange)
$scope.addCoordinatorFee = ($event) ->
$event.preventDefault()
OrderCycle.addCoordinatorFee()
$scope.removeCoordinatorFee = ($event, index) ->
$event.preventDefault()
OrderCycle.removeCoordinatorFee(index)
$scope.addExchangeFee = ($event, exchange) ->
$event.preventDefault()
OrderCycle.addExchangeFee(exchange)
$scope.removeExchangeFee = ($event, exchange, index) ->
$event.preventDefault()
OrderCycle.removeExchangeFee(exchange, index)
$scope.removeDistributionOfVariant = (variant_id) ->
OrderCycle.removeDistributionOfVariant(variant_id)
$scope.submit = (destination) ->
OrderCycle.create(destination)
])
.controller('AdminEditOrderCycleCtrl', ['$scope', '$filter', '$location', 'OrderCycle', 'Enterprise', 'EnterpriseFee', 'StatusMessage', ($scope, $filter, $location, OrderCycle, Enterprise, EnterpriseFee, StatusMessage) ->
order_cycle_id = $location.absUrl().match(/\/admin\/order_cycles\/(\d+)/)[1]
$scope.enterprises = Enterprise.index(order_cycle_id: order_cycle_id)
$scope.supplier_enterprises = Enterprise.producer_enterprises
$scope.distributor_enterprises = Enterprise.hub_enterprises
$scope.supplied_products = Enterprise.supplied_products
$scope.enterprise_fees = EnterpriseFee.index(order_cycle_id: order_cycle_id)
$scope.OrderCycle = OrderCycle
$scope.order_cycle = OrderCycle.load(order_cycle_id)
$scope.StatusMessage = StatusMessage
$scope.loaded = ->
Enterprise.loaded && EnterpriseFee.loaded && OrderCycle.loaded
$scope.suppliedVariants = (enterprise_id) ->
Enterprise.suppliedVariants(enterprise_id)
$scope.exchangeSelectedVariants = (exchange) ->
OrderCycle.exchangeSelectedVariants(exchange)
$scope.setExchangeVariants = (exchange, variants, selected) ->
OrderCycle.setExchangeVariants(exchange, variants, selected)
$scope.enterpriseTotalVariants = (enterprise) ->
Enterprise.totalVariants(enterprise)
$scope.productSuppliedToOrderCycle = (product) ->
OrderCycle.productSuppliedToOrderCycle(product)
$scope.variantSuppliedToOrderCycle = (variant) ->
OrderCycle.variantSuppliedToOrderCycle(variant)
$scope.incomingExchangeVariantsFor = (enterprise_id) ->
$filter('filterExchangeVariants')(OrderCycle.incomingExchangesVariants(), $scope.order_cycle.visible_variants_for_outgoing_exchanges[enterprise_id])
$scope.exchangeDirection = (exchange) ->
OrderCycle.exchangeDirection(exchange)
$scope.enterprisesWithFees = ->
$scope.enterprises[id] for id in OrderCycle.participatingEnterpriseIds() when $scope.enterpriseFeesForEnterprise(id).length > 0
$scope.toggleProducts = ($event, exchange) ->
$event.preventDefault()
OrderCycle.toggleProducts(exchange)
$scope.enterpriseFeesForEnterprise = (enterprise_id) ->
EnterpriseFee.forEnterprise(parseInt(enterprise_id))
$scope.addSupplier = ($event) ->
$event.preventDefault()
OrderCycle.addSupplier($scope.new_supplier_id)
$scope.addDistributor = ($event) ->
$event.preventDefault()
OrderCycle.addDistributor($scope.new_distributor_id)
$scope.removeExchange = ($event, exchange) ->
$event.preventDefault()
OrderCycle.removeExchange(exchange)
$scope.addCoordinatorFee = ($event) ->
$event.preventDefault()
OrderCycle.addCoordinatorFee()
$scope.removeCoordinatorFee = ($event, index) ->
$event.preventDefault()
OrderCycle.removeCoordinatorFee(index)
$scope.addExchangeFee = ($event, exchange) ->
$event.preventDefault()
OrderCycle.addExchangeFee(exchange)
$scope.removeExchangeFee = ($event, exchange, index) ->
$event.preventDefault()
OrderCycle.removeExchangeFee(exchange, index)
$scope.removeDistributionOfVariant = (variant_id) ->
OrderCycle.removeDistributionOfVariant(variant_id)
$scope.submit = (destination) ->
OrderCycle.update(destination)
])
.config(['$httpProvider', ($httpProvider) ->
$httpProvider.defaults.headers.common['X-CSRF-Token'] = $('meta[name=csrf-token]').attr('content')
])
.directive('datetimepicker', ['$parse', ($parse) ->
(scope, element, attrs) ->
# using $parse instead of scope[attrs.datetimepicker] for cases
# where attrs.datetimepicker is 'foo.bar.lol'
$(element).datetimepicker
dateFormat: 'yy-mm-dd'
timeFormat: 'HH:mm:ss'
showOn: "button"
buttonImage: "<%= asset_path 'datepicker/cal.gif' %>"
buttonImageOnly: true
stepMinute: 15
onSelect: (dateText, inst) ->
scope.$apply ->
parsed = $parse(attrs.datetimepicker)
parsed.assign(scope, dateText)
])
.directive('ofnOnChange', ->
(scope, element, attrs) ->
element.bind 'change', ->
scope.$apply(attrs.ofnOnChange)
)
.directive('ofnSyncDistributions', ->
(scope, element, attrs) ->
element.bind 'change', ->
if !$(this).is(':checked')
scope.$apply ->
scope.removeDistributionOfVariant(attrs.ofnSyncDistributions)
)

View File

@@ -1 +1,32 @@
angular.module('admin.orderCycles', ['ngResource', 'admin.indexUtils'])
angular.module('admin.orderCycles', ['ngResource', 'admin.utils', 'admin.indexUtils'])
.config ($httpProvider) ->
$httpProvider.defaults.headers.common['X-CSRF-Token'] = $('meta[name=csrf-token]').attr('content')
.directive 'datetimepicker', ($parse) ->
(scope, element, attrs) ->
# using $parse instead of scope[attrs.datetimepicker] for cases
# where attrs.datetimepicker is 'foo.bar.lol'
$(element).datetimepicker
dateFormat: 'yy-mm-dd'
timeFormat: 'HH:mm:ss'
showOn: "button"
buttonImage: "<%= asset_path 'datepicker/cal.gif' %>"
buttonImageOnly: true
stepMinute: 15
onSelect: (dateText, inst) ->
scope.$apply ->
parsed = $parse(attrs.datetimepicker)
parsed.assign(scope, dateText)
.directive 'ofnOnChange', ->
(scope, element, attrs) ->
element.bind 'change', ->
scope.$apply(attrs.ofnOnChange)
.directive 'ofnSyncDistributions', ->
(scope, element, attrs) ->
element.bind 'change', ->
if !$(this).is(':checked')
scope.$apply ->
scope.removeDistributionOfVariant(attrs.ofnSyncDistributions)

View File

@@ -1,26 +0,0 @@
angular.module("ofn.admin").factory "StatusMessage", ($timeout) ->
new class StatusMessage
types:
progress: {timeout: false, style: {color: '#ff9906'}}
alert: {timeout: 5000, style: {color: 'grey'}}
notice: {timeout: false, style: {color: 'grey'}}
success: {timeout: 5000, style: {color: '#9fc820'}}
failure: {timeout: false, style: {color: '#da5354'}}
statusMessage:
text: ""
style: {}
display: (type, text) ->
@statusMessage.text = text
@statusMessage.style = @types[type].style
$timeout.cancel @statusMessage.timeout if @statusMessage.timeout
timeout = @types[type].timeout
if timeout
@statusMessage.timeout = $timeout =>
@clear()
, timeout, true
clear: ->
@statusMessage.text = ''
@statusMessage.style = {}

View File

@@ -1,23 +0,0 @@
angular.module("ofn.admin").factory "VariantOverrides", (variantOverrides, Indexer) ->
new class VariantOverrides
variantOverrides: {}
constructor: ->
for vo in variantOverrides
@variantOverrides[vo.hub_id] ||= {}
@variantOverrides[vo.hub_id][vo.variant_id] = vo
ensureDataFor: (hubs, products) ->
for hub in hubs
@variantOverrides[hub.id] ||= {}
for product in products
for variant in product.variants
@variantOverrides[hub.id][variant.id] ||=
variant_id: variant.id
hub_id: hub.id
price: ''
count_on_hand: ''
updateIds: (updatedVos) ->
for vo in updatedVos
@variantOverrides[vo.hub_id][vo.variant_id].id = vo.id

View File

@@ -0,0 +1,8 @@
angular.module("admin.utils").directive "saveBar", (StatusMessage) ->
restrict: "E"
scope:
save: "&"
form: "="
templateUrl: "admin/save_bar.html"
link: (scope, element, attrs) ->
scope.StatusMessage = StatusMessage

View File

@@ -11,6 +11,9 @@ angular.module("admin.utils").factory "StatusMessage", ($timeout) ->
text: ""
style: {}
active: ->
@statusMessage.text != ''
display: (type, text) ->
@statusMessage.text = text
@statusMessage.style = @types[type].style
@@ -20,6 +23,7 @@ angular.module("admin.utils").factory "StatusMessage", ($timeout) ->
@statusMessage.timeout = $timeout =>
@clear()
, timeout, true
null # So we don't return weird timeouts
clear: ->
@statusMessage.text = ''

View File

@@ -0,0 +1,102 @@
angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", ($scope, $http, $timeout, Indexer, Columns, SpreeApiAuth, PagedFetcher, StatusMessage, hubs, producers, hubPermissions, VariantOverrides, DirtyVariantOverrides) ->
$scope.hubs = Indexer.index hubs
$scope.hub = null
$scope.products = []
$scope.producers = producers
$scope.producersByID = Indexer.index producers
$scope.hubPermissions = hubPermissions
$scope.variantOverrides = VariantOverrides.variantOverrides
$scope.StatusMessage = StatusMessage
$scope.columns = Columns.setColumns
producer: { name: "Producer", visible: true }
product: { name: "Product", visible: true }
sku: { name: "SKU", visible: false }
price: { name: "Price", visible: true }
on_hand: { name: "On Hand", visible: true }
on_demand: { name: "On Demand", visible: false }
reset: { name: "Reset Stock Level", visible: false }
inheritance: { name: "Inheritance", visible: false }
$scope.resetSelectFilters = ->
$scope.producerFilter = 0
$scope.query = ''
$scope.resetSelectFilters()
$scope.initialise = ->
SpreeApiAuth.authorise()
.then ->
$scope.spree_api_key_ok = true
$scope.fetchProducts()
.catch (message) ->
$scope.api_error_msg = message
$scope.fetchProducts = ->
url = "/api/products/overridable?page=::page::;per_page=100"
PagedFetcher.fetch url, (data) => $scope.addProducts data.products
$scope.addProducts = (products) ->
$scope.products = $scope.products.concat products
VariantOverrides.ensureDataFor hubs, products
$scope.selectHub = ->
$scope.hub = $scope.hubs[$scope.hub_id]
$scope.displayDirty = ->
if DirtyVariantOverrides.count() > 0
num = if DirtyVariantOverrides.count() == 1 then "one override" else "#{DirtyVariantOverrides.count()} overrides"
StatusMessage.display 'notice', "Changes to #{num} remain unsaved."
else
StatusMessage.clear()
$scope.update = ->
if DirtyVariantOverrides.count() == 0
StatusMessage.display 'alert', 'No changes to save.'
else
StatusMessage.display 'progress', 'Saving...'
DirtyVariantOverrides.save()
.success (updatedVos) ->
DirtyVariantOverrides.clear()
VariantOverrides.updateIds updatedVos
$scope.variant_overrides_form.$setPristine()
StatusMessage.display 'success', 'Changes saved.'
VariantOverrides.updateData updatedVos # Refresh page data
.error (data, status) ->
StatusMessage.display 'failure', $scope.updateError(data, status)
$scope.updateError = (data, status) ->
if status == 401
"I couldn't get authorisation to save those changes, so they remain unsaved."
else if status == 400 && data.errors?
errors = []
for field, field_errors of data.errors
errors = errors.concat field_errors
errors = errors.join ', '
"I had some trouble saving: #{errors}"
else
"Oh no! I was unable to save your changes."
$scope.resetStock = ->
if DirtyVariantOverrides.count() > 0
StatusMessage.display 'alert', 'Save changes first.'
$timeout ->
$scope.displayDirty()
, 3000 # 3 second delay
else
return unless $scope.hub_id?
StatusMessage.display 'progress', 'Changing on hand stock levels...'
$http
method: "POST"
url: "/admin/variant_overrides/bulk_reset"
data: { hub_id: $scope.hub_id }
.success (updatedVos) ->
VariantOverrides.updateData updatedVos
StatusMessage.display 'success', 'Stocks reset to defaults.'
.error (data, status) ->
$timeout -> StatusMessage.display 'failure', $scope.updateError(data, status)

View File

@@ -0,0 +1,12 @@
angular.module("admin.variantOverrides").directive "trackInheritance", (VariantOverrides, DirtyVariantOverrides) ->
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.id
ngModel.$parsers.push (viewValue) ->
if ngModel.$dirty && viewValue
variantOverride = VariantOverrides.inherit(scope.hub.id, scope.variant.id)
DirtyVariantOverrides.add variantOverride
scope.displayDirty()
viewValue

View File

@@ -1,9 +1,10 @@
angular.module("ofn.admin").directive "ofnTrackVariantOverride", (DirtyVariantOverrides) ->
angular.module("admin.variantOverrides").directive "ofnTrackVariantOverride", (DirtyVariantOverrides) ->
require: "ngModel"
link: (scope, element, attrs, ngModel) ->
ngModel.$parsers.push (viewValue) ->
if ngModel.$dirty
variantOverride = scope.variantOverrides[scope.hub.id][scope.variant.id]
scope.inherit = false
DirtyVariantOverrides.add variantOverride
scope.displayDirty()
viewValue

View File

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

View File

@@ -1,4 +1,4 @@
angular.module("ofn.admin").factory "DirtyVariantOverrides", ($http) ->
angular.module("admin.variantOverrides").factory "DirtyVariantOverrides", ($http) ->
new class DirtyVariantOverrides
dirtyVariantOverrides: {}

View File

@@ -0,0 +1,40 @@
angular.module("admin.variantOverrides").factory "VariantOverrides", (variantOverrides) ->
new class VariantOverrides
variantOverrides: {}
constructor: ->
for vo in variantOverrides
@variantOverrides[vo.hub_id] ||= {}
@variantOverrides[vo.hub_id][vo.variant_id] = vo
ensureDataFor: (hubs, products) ->
for hub_id, hub of hubs
@variantOverrides[hub.id] ||= {}
for product in products
for variant in product.variants
@inherit(hub.id, variant.id) unless @variantOverrides[hub.id][variant.id]
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_id
newFor: (hub_id, variant_id) ->
# These properties need to match those checked in VariantOverrideSet.deletable?
hub_id: hub_id
variant_id: variant_id
sku: null
price: null
count_on_hand: null
on_demand: null
default_stock: null
resettable: false
updateIds: (updatedVos) ->
for vo in updatedVos
@variantOverrides[vo.hub_id][vo.variant_id].id = vo.id
updateData: (updatedVos) ->
for vo in updatedVos
@variantOverrides[vo.hub_id][vo.variant_id] = vo

View File

@@ -0,0 +1 @@
angular.module("admin.variantOverrides", ["pasvaz.bindonce", "admin.indexUtils", "admin.utils", "admin.dropdown"])

View File

@@ -1,9 +1,7 @@
Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, Enterprises, Search, $document, HashNavigation, FilterSelectorsService, EnterpriseModal, enterpriseMatchesNameQueryFilter, distanceWithinKmFilter) ->
$scope.Enterprises = Enterprises
$scope.totalActive = FilterSelectorsService.totalActive
$scope.clearAll = FilterSelectorsService.clearAll
$scope.filterText = FilterSelectorsService.filterText
$scope.FilterSelectorsService = FilterSelectorsService
$scope.producers_to_filter = Enterprises.producers
$scope.filterSelectors = FilterSelectorsService.createSelectors()
$scope.query = Search.search()
$scope.openModal = EnterpriseModal.open
$scope.activeTaxons = []

View File

@@ -1,8 +1,5 @@
Darkswarm.controller "GroupEnterprisesCtrl", ($scope, Search, FilterSelectorsService, EnterpriseModal) ->
$scope.totalActive = FilterSelectorsService.totalActive
$scope.clearAll = FilterSelectorsService.clearAll
$scope.filterText = FilterSelectorsService.filterText
$scope.FilterSelectorsService = FilterSelectorsService
$scope.filterSelectors = FilterSelectorsService.createSelectors()
$scope.query = Search.search()
$scope.openModal = EnterpriseModal.open
$scope.activeTaxons = []

View File

@@ -15,6 +15,8 @@ Darkswarm.controller "GroupPageCtrl", ($scope, group_enterprises, Enterprises, M
$scope.group_hubs = visible_enterprises.filter (enterprise) ->
enterprise.category in ["hub", "hub_profile", "producer_hub", "producer_shop"]
$scope.producers_to_filter = $scope.group_producers
$scope.map = angular.copy MapConfiguration.options
$scope.mapMarkers = OfnMap.enterprise_markers visible_enterprises

View File

@@ -1,10 +1,8 @@
Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Products, OrderCycle, FilterSelectorsService, Cart, Taxons, Properties) ->
$scope.Products = Products
$scope.Cart = Cart
$scope.totalActive = FilterSelectorsService.totalActive
$scope.clearAll = FilterSelectorsService.clearAll
$scope.filterText = FilterSelectorsService.filterText
$scope.FilterSelectorsService = FilterSelectorsService
$scope.taxonSelectors = FilterSelectorsService.createSelectors()
$scope.propertySelectors = FilterSelectorsService.createSelectors()
$scope.filtersActive = true
$scope.limit = 3
$scope.order_cycle = OrderCycle.order_cycle
@@ -33,4 +31,5 @@ Darkswarm.controller "ProductsCtrl", ($scope, $rootScope, Products, OrderCycle,
$scope.clearAll = ->
$scope.query = ""
FilterSelectorsService.clearAll()
$scope.taxonSelectors.clearAll()
$scope.propertySelectors.clearAll()

View File

@@ -1,9 +1,10 @@
Darkswarm.directive "filterSelector", (FilterSelectorsService)->
Darkswarm.directive "filterSelector", ->
# Automatically builds activeSelectors for taxons
# Lots of magic here
restrict: 'E'
replace: true
scope:
selectorSet: '='
objects: "&"
activeSelectors: "=?"
allSelectors: "=?" # Optional
@@ -36,7 +37,7 @@ Darkswarm.directive "filterSelector", (FilterSelectorsService)->
if selector = selectors_by_id[id]
selectors.push selector
else
selector = selectors_by_id[id] = FilterSelectorsService.new
selector = selectors_by_id[id] = scope.selectorSet.new
object: object
selectors.push selector
selectors

View File

@@ -1,4 +1,4 @@
Darkswarm.directive "shippingTypeSelector", (FilterSelectorsService)->
Darkswarm.directive "shippingTypeSelector", ->
# Builds selector for shipping types
restrict: 'E'
replace: true
@@ -8,10 +8,10 @@ Darkswarm.directive "shippingTypeSelector", (FilterSelectorsService)->
pickup: false
delivery: false
scope.selectors =
delivery: FilterSelectorsService.new
scope.selectors =
delivery: scope.filterSelectors.new
icon: "ofn-i_039-delivery"
pickup: FilterSelectorsService.new
pickup: scope.filterSelectors.new
icon: "ofn-i_038-takeaway"
scope.emit = ->

View File

@@ -2,6 +2,7 @@ Darkswarm.directive 'singleLineSelectors', ($timeout, $filter) ->
restrict: 'E'
templateUrl: "single_line_selectors.html"
scope:
selectors: "="
objects: "&"
activeSelectors: "="
selectorName: "@activeSelectors"

View File

@@ -2,8 +2,11 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, storage)->
# Handles syncing of current cart/order state to server
new class Cart
dirty: false
update_running: false
update_enqueued: false
order: CurrentOrder.order
line_items: CurrentOrder.order?.line_items || []
constructor: ->
for line_item in @line_items
line_item.variant.line_item = line_item
@@ -12,15 +15,31 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, storage)->
orderChanged: =>
@unsaved()
if !@update_running
@scheduleUpdate()
else
@update_enqueued = true
scheduleUpdate: =>
if @promise
$timeout.cancel(@promise)
@promise = $timeout @update, 1000
update: =>
@update_running = true
$http.post('/orders/populate', @data()).success (data, status)=>
@saved()
@update_running = false
@popQueue() if @update_enqueued
.error (response, status)=>
@scheduleRetry()
@scheduleRetry(status)
@update_running = false
popQueue: =>
@update_enqueued = false
@scheduleUpdate()
data: =>
variants = {}
@@ -30,7 +49,7 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, storage)->
max_quantity: li.max_quantity
{variants: variants}
scheduleRetry: =>
scheduleRetry: (status) =>
console.log "Error updating cart: #{status}. Retrying in 3 seconds..."
$timeout =>
console.log "Retrying cart update"

View File

@@ -1,8 +1,11 @@
# Returns a factory with the only function `createSelectors()`.
# That function creates objects managing a list of filter selectors.
Darkswarm.factory "FilterSelectorsService", ->
# This stores all filters so we can access in-use counts etc
# Accessed via activeSelector Directive
new class FilterSelectorsService
selectors: []
class FilterSelectors
constructor: ->
@selectors = []
new: (obj = {})->
obj.active = false
@selectors.push obj
@@ -26,3 +29,8 @@ Darkswarm.factory "FilterSelectorsService", ->
for selector in @selectors
selector.active = false
selector.emit()
# Creates instances of `FilterSelectors`
new class FilterSelectorsService
createSelectors: ->
new FilterSelectors

View File

@@ -1,4 +1,4 @@
.ofn_drop_down{ "ofn-drop-down" => true }
.ofn-drop-down
%span
%i.icon-check
Actions

View File

@@ -1,10 +1,6 @@
#save-bar.animate-show{ ng: { show: 'dirty()' } }
#save-bar.animate-show{ ng: { show: 'form.$dirty || StatusMessage.active()' } }
.twelve.columns.alpha
%h5{ ng: { show: "dirty() && !saving()" } }
You have unsaved changes
%h5{ ng: { hide: "dirty() || saving()" } }
All changes saved
%h5{ ng: { show: "saving()" } }
Saving...
%h5#status-message{ ng: { style: 'StatusMessage.statusMessage.style' } }
{{ StatusMessage.statusMessage.text || "&nbsp;" }}
.four.columns.omega.text-right
%input.red{type: "button", value: "Save Changes", ng: { click: "save()" } }
%input.red{type: "button", value: "Save Changes", ng: { disabled: '!form.$dirty', click: "save()" } }

View File

@@ -1,3 +1,4 @@
%active-selector{"ng-repeat" => "(name, selector) in selectors"}
%i{"ng-class" => "selector.icon"}
{{ name | capitalize }}
%ul.small-block-grid-2.medium-block-grid-4.large-block-grid-2
%active-selector{"ng-repeat" => "(name, selector) in selectors"}
%i{"ng-class" => "selector.icon"}
{{ name | capitalize }}

View File

@@ -1,5 +1,5 @@
-# In order for the single-line-selector scope to have access to the available selectors,
%filter-selector{objects: "objects()", "active-selectors" => "activeSelectors", "all-selectors" => "allSelectors" }
%filter-selector{"selector-set" => "selectors", objects: "objects()", "active-selectors" => "activeSelectors", "all-selectors" => "allSelectors" }
%ul{ ng: { if: "overFlowSelectors().length > 0 || fitting" } }
%li.more

View File

@@ -0,0 +1,13 @@
label.disabled {
color: #c3c3c3;
pointer-events: none;
}
input[type='button']:disabled {
background-color: #c3c3c3;
color: #ffffff;
}
.select2-container-disabled {
pointer-events: none;
}

View File

@@ -0,0 +1,70 @@
#content-header .ofn-drop-down {
border: none;
background-color: #5498da;
color: #fff;
float: none;
margin-left: 3px;
}
.ofn-drop-down:hover, .ofn-drop-down.expanded {
border: 1px solid #adadad;
color: #575757;
}
.ofn-drop-down {
padding: 7px 15px;
border-radius: 3px;
border: 1px solid #d4d4d4;
background-color: #f5f5f5;
position: relative;
display: block;
float: left;
color: #828282;
cursor: pointer;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
text-align: center;
&.right {
float: right;
}
&:hover, &.expanded {
border: 1px solid #adadad;
color: #575757;
}
> span {
width: auto;
text-transform: uppercase;
font-size: 85%;
font-weight: 600;
}
.menu {
margin-top: 1px;
position: absolute;
float: none;
top:100%;
left: 0px;
padding: 5px 0px;
border: 1px solid #adadad;
background-color: #ffffff;
box-shadow: 1px 3px 10px #888888;
z-index: 100;
.menu_item {
margin: 0px;
padding: 2px 0px;
color: #454545;
text-align: left;
}
.menu_item:hover {
background-color: #ededed;
}
}
}

View File

@@ -0,0 +1,3 @@
.filters, .controls, .divider {
margin-bottom: 15px;
}

View File

@@ -184,68 +184,6 @@ table#listing_enterprise_groups {
}
}
#content-header .ofn_drop_down {
border: none;
background-color: #5498da;
color: #fff;
float: none;
margin-left: 3px;
}
.ofn_drop_down {
padding: 6px 15px;
border-radius: 3px;
border: 1px solid #d4d4d4;
background-color: #f5f5f5;
position: relative;
display: block;
float: left;
color: #828282;
cursor: pointer;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
text-align: center;
> span {
width: auto;
text-transform: uppercase;
font-size: 85%;
font-weight: 600;
}
.menu {
margin-top: 1px;
position: absolute;
float: none;
top:100%;
left: 0px;
padding: 5px 0px;
border: 1px solid #adadad;
background-color: #ffffff;
box-shadow: 1px 3px 10px #888888;
z-index: 100;
.menu_item {
margin: 0px;
padding: 2px 0px;
color: #454545;
text-align: left;
}
.menu_item:hover {
background-color: #ededed;
}
}
}
.ofn_drop_down:hover, .ofn_drop_down.expanded {
border: 1px solid #adadad;
color: #575757;
}
.field_with_errors > input {
border-color: red;
}

View File

@@ -1,7 +1,3 @@
.filter_select, .date_filter {
margin-bottom: 10px;
}
input, div {
&.update-pending {
border: solid 1px orange;

View File

@@ -13,7 +13,7 @@
height: 7rem
float: left
display: block
z-index: 999999
z-index: 1
background-color: white
overflow: hidden
i
@@ -56,4 +56,4 @@
width: 0rem
height: 0rem

View File

@@ -11,6 +11,10 @@
background-size: 922px 922px
@include sidepaddingSm
@include panepadding
h1, p.text
font-weight: 300
h1
font-size: 350%
a > .group-name
&:hover, &:focus, &:active
text-decoration: underline
@@ -97,11 +101,13 @@
// Producers tab
.producers
background-image: none
background-color: initial
.active_table .active_table_node a.is_distributor, .active_table .active_table_node a.is_distributor i.ofn-i_059-producer
color: $clr-turquoise
padding: 0
// Hubs tab
.hubs
background-image: none
padding-top: 0
padding-bottom: 0

View File

@@ -48,3 +48,7 @@ dialog .close-reveal-modal, .reveal-modal .close-reveal-modal
&:hover, &:active, &:focus
background-color: rgba(205,205,205,1)
color: #333
// Prevent body from scrolling when a modal is open
body.modal-open
overflow: hidden

View File

@@ -13,6 +13,11 @@
right: 10px
top: 55px
width: 480px
@media screen and (min-width: 641px)
overflow-y: auto
max-height: calc(95vh - 55px)
@media screen and (max-width: 640px)
width: 96%
@@ -48,7 +53,7 @@
.cart-item-delete
a.delete
font-size: 1.125em
.item-thumb-image
display: none
@media screen and (min-width: 640px)

View File

@@ -89,7 +89,7 @@ module Admin
end
def collection_actions
[:index, :for_order_cycle]
[:index, :for_order_cycle, :bulk_update]
end
def current_enterprise

View File

@@ -175,5 +175,9 @@ module Admin
def ams_prefix_whitelist
[:basic]
end
def collection_actions
[:index, :bulk_update]
end
end
end

View File

@@ -4,32 +4,43 @@ module Admin
class VariantOverridesController < ResourceController
include OpenFoodNetwork::SpreeApiKeyLoader
prepend_before_filter :load_data
before_filter :load_collection, only: [:bulk_update]
before_filter :load_spree_api_key, only: :index
before_filter :load_data
def index
end
def bulk_update
collection_hash = Hash[params[:variant_overrides].each_with_index.map { |vo, i| [i, vo] }]
vo_set = VariantOverrideSet.new @variant_overrides, collection_attributes: collection_hash
# Ensure we're authorised to update all variant overrides
vo_set.collection.each { |vo| authorize! :update, vo }
@vo_set.collection.each { |vo| authorize! :update, vo }
if vo_set.save
if @vo_set.save
# Return saved VOs with IDs
render json: vo_set.collection, each_serializer: Api::Admin::VariantOverrideSerializer
render json: @vo_set.collection, each_serializer: Api::Admin::VariantOverrideSerializer
else
if vo_set.errors.present?
render json: { errors: vo_set.errors }, status: 400
if @vo_set.errors.present?
render json: { errors: @vo_set.errors }, status: 400
else
render nothing: true, status: 500
end
end
end
def bulk_reset
# Ensure we're authorised to update all variant overrides.
@collection.each { |vo| authorize! :bulk_reset, vo }
@collection.each(&:reset_stock!)
if collection_errors.present?
render json: { errors: collection_errors }, status: 400
else
render json: @collection, each_serializer: Api::Admin::VariantOverrideSerializer
end
end
private
@@ -43,10 +54,28 @@ module Admin
@hub_permissions = OpenFoodNetwork::Permissions.new(spree_current_user).
variant_override_enterprises_per_hub
@variant_overrides = VariantOverride.for_hubs(@hubs)
end
def load_collection
collection_hash = Hash[params[:variant_overrides].each_with_index.map { |vo, i| [i, vo] }]
@vo_set = VariantOverrideSet.new @variant_overrides, collection_attributes: collection_hash
end
def collection
@variant_overrides = VariantOverride.for_hubs(params[:hub_id] || @hubs)
end
def collection_actions
[:index, :bulk_update, :bulk_reset]
end
# This has been pulled from ModelSet as it is useful for compiling a list of errors on any generic collection (not necessarily a ModelSet)
# Could be pulled down into a lower level controller if it is useful in other high level controllers
def collection_errors
errors = ActiveModel::Errors.new self
full_messages = @collection.map { |element| element.errors.full_messages }.flatten
full_messages.each { |fm| errors.add(:base, fm) }
errors
end
end
end

View File

@@ -23,13 +23,23 @@ Spree::OrdersController.class_eval do
end
def populate
populator = Spree::OrderPopulator.new(current_order(true), current_currency)
if populator.populate(params.slice(:products, :variants, :quantity), true)
fire_event('spree.cart.add')
fire_event('spree.order.contents_changed')
render json: true, status: 200
else
render json: false, status: 402
# Without intervention, the Spree::Adjustment#update_adjustable callback is called many times
# during cart population, for both taxation and enterprise fees. This operation triggers a
# costly Spree::Order#update!, which only needs to be run once. We avoid this by disabling
# callbacks on Spree::Adjustment and then manually invoke Spree::Order#update! on success.
Spree::Adjustment.without_callbacks do
populator = Spree::OrderPopulator.new(current_order(true), current_currency)
if populator.populate(params.slice(:products, :variants, :quantity), true)
fire_event('spree.cart.add')
fire_event('spree.order.contents_changed')
current_order.update!
render json: true, status: 200
else
render json: false, status: 402
end
end
end

View File

@@ -31,12 +31,12 @@ module Admin
admin_inject_json_ams_array ngModule, "shops", @shops, Api::Admin::IdNameSerializer
end
def admin_inject_hubs
admin_inject_json_ams_array "ofn.admin", "hubs", @hubs, Api::Admin::IdNameSerializer
def admin_inject_hubs(opts={module: 'ofn.admin'})
admin_inject_json_ams_array opts[:module], "hubs", @hubs, Api::Admin::IdNameSerializer
end
def admin_inject_producers
admin_inject_json_ams_array "ofn.admin", "producers", @producers, Api::Admin::IdNameSerializer
def admin_inject_producers(opts={module: 'ofn.admin'})
admin_inject_json_ams_array opts[:module], "producers", @producers, Api::Admin::IdNameSerializer
end
def admin_inject_enterprise_permissions
@@ -49,7 +49,7 @@ module Admin
end
def admin_inject_hub_permissions
render partial: "admin/json/injection_ams", locals: {ngModule: "ofn.admin", name: "hubPermissions", json: @hub_permissions.to_json}
render partial: "admin/json/injection_ams", locals: {ngModule: "admin.variantOverrides", name: "hubPermissions", json: @hub_permissions.to_json}
end
def admin_inject_products
@@ -69,7 +69,7 @@ module Admin
end
def admin_inject_variant_overrides
admin_inject_json_ams_array "ofn.admin", "variantOverrides", @variant_overrides, Api::Admin::VariantOverrideSerializer
admin_inject_json_ams_array "admin.variantOverrides", "variantOverrides", @variant_overrides, Api::Admin::VariantOverrideSerializer
end
def admin_inject_order_cycle_instance
@@ -85,7 +85,7 @@ module Admin
end
def admin_inject_spree_api_key
render partial: "admin/json/injection_ams", locals: {ngModule: 'ofn.admin', name: 'SpreeApiKey', json: "'#{@spree_api_key.to_s}'"}
render partial: "admin/json/injection_ams", locals: {ngModule: 'admin.indexUtils', name: 'SpreeApiKey', json: "'#{@spree_api_key.to_s}'"}
end
def admin_inject_json_ams(ngModule, name, data, serializer, opts = {})

View File

@@ -6,7 +6,7 @@ module InjectionHelper
end
def inject_group_enterprises
inject_json_ams "group_enterprises", @group.enterprises, Api::EnterpriseSerializer, enterprise_injection_data
inject_json_ams "group_enterprises", @group.enterprises.activated.all, Api::EnterpriseSerializer, enterprise_injection_data
end
def inject_current_hub

View File

@@ -10,4 +10,4 @@ Spree::BaseMailer.class_eval do
# This lets us specify assets using relative paths in email templates
super.merge(url_options: {host: URI(spree.root_url).host })
end
end
end

View File

@@ -16,8 +16,8 @@ class ModelSet
end
end
def collection_attributes=(attributes)
attributes.each do |k, attributes|
def collection_attributes=(collection_attributes)
collection_attributes.each do |k, attributes|
# attributes == {:id => 123, :next_collection_at => '...'}
e = @collection.detect { |e| e.id.to_s == attributes[:id].to_s && !e.id.nil? }
if e.nil?
@@ -41,7 +41,11 @@ class ModelSet
end
def collection_to_delete
collection.select { |e| @delete_if.andand.call(e.attributes) }
# Remove all elements to be deleted from collection and return them
# Allows us to render @model_set.collection without deleted elements
deleted = []
collection.delete_if { |e| deleted << e if @delete_if.andand.call(e.attributes) }
deleted
end
def collection_to_keep
@@ -51,5 +55,4 @@ class ModelSet
def persisted?
false
end
end

View File

@@ -16,7 +16,7 @@ class OrderCycle < ActiveRecord::Base
scope :inactive, lambda { where('order_cycles.orders_open_at > ? OR order_cycles.orders_close_at < ?', Time.zone.now, Time.zone.now) }
scope :upcoming, lambda { where('order_cycles.orders_open_at > ?', Time.zone.now) }
scope :closed, lambda { where('order_cycles.orders_close_at < ?', Time.zone.now).order("order_cycles.orders_close_at DESC") }
scope :undated, where(orders_open_at: nil, orders_close_at: nil)
scope :undated, where('order_cycles.orders_open_at IS NULL OR orders_close_at IS NULL')
scope :soonest_closing, lambda { active.order('order_cycles.orders_close_at ASC') }
# TODO This method returns all the closed orders. So maybe we can replace it with :recently_closed.
@@ -182,7 +182,7 @@ class OrderCycle < ActiveRecord::Base
end
def undated?
self.orders_open_at.nil? && self.orders_close_at.nil?
self.orders_open_at.nil? || self.orders_close_at.nil?
end
def upcoming?

View File

@@ -66,7 +66,7 @@ class AbilityDecorator
def add_enterprise_management_abilities(user)
# Spree performs authorize! on (:create, nil) when creating a new order from admin, and also (:search, nil)
# when searching for variants to add to the order
can [:create, :search, :bulk_update], nil
can [:create, :search], nil
can [:admin, :index], :overview
@@ -111,7 +111,9 @@ class AbilityDecorator
OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include? variant.product.supplier
end
can [:admin, :index, :read, :update, :bulk_update], VariantOverride do |vo|
can [:admin, :index, :read, :update, :bulk_update, :bulk_reset], VariantOverride do |vo|
next false unless vo.hub.present? && vo.variant.andand.product.andand.supplier.present?
hub_auth = OpenFoodNetwork::Permissions.new(user).
variant_override_hubs.
include? vo.hub

View File

@@ -35,5 +35,19 @@ module Spree
def display_included_tax
Spree::Money.new(included_tax, { :currency => currency })
end
def self.without_callbacks
skip_callback :save, :after, :update_adjustable
skip_callback :destroy, :after, :update_adjustable
result = yield
ensure
set_callback :save, :after, :update_adjustable
set_callback :destroy, :after, :update_adjustable
result
end
end
end

View File

@@ -3,6 +3,8 @@ class VariantOverride < ActiveRecord::Base
belongs_to :variant, class_name: 'Spree::Variant'
validates_presence_of :hub_id, :variant_id
# Default stock can be nil, indicating stock should not be reset or zero, meaning reset to zero. Need to ensure this can be set by the user.
validates :default_stock, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
scope :for_hubs, lambda { |hubs|
where(hub_id: hubs)
@@ -49,6 +51,21 @@ class VariantOverride < ActiveRecord::Base
end
end
def default_stock?
default_stock.present?
end
def reset_stock!
if resettable
if default_stock?
self.attributes = { count_on_hand: default_stock }
self.save
else
Bugsnag.notify RuntimeError.new "Attempting to reset stock level for a variant with no default stock level."
end
end
self
end
private

View File

@@ -1,6 +1,16 @@
class VariantOverrideSet < ModelSet
def initialize(collection, attributes={})
super(VariantOverride, collection, attributes, nil,
proc { |attrs| attrs['price'].blank? && attrs['count_on_hand'].blank? } )
super(VariantOverride, collection, attributes, nil, proc { |attrs| deletable?(attrs) } )
end
private
def deletable?(attrs)
attrs['price'].blank? &&
attrs['count_on_hand'].blank? &&
attrs['default_stock'].blank? &&
attrs['resettable'].blank? &&
attrs['sku'].nil? &&
attrs['on_demand'].nil?
end
end

View File

@@ -1,5 +1,5 @@
class Api::Admin::ExchangeSerializer < ActiveModel::Serializer
attributes :id, :sender_id, :receiver_id, :incoming, :variants, :pickup_time, :pickup_instructions
attributes :id, :sender_id, :receiver_id, :incoming, :variants, :receival_instructions, :pickup_time, :pickup_instructions
has_many :enterprise_fees, serializer: Api::Admin::BasicEnterpriseFeeSerializer

View File

@@ -1,3 +1,3 @@
class Api::Admin::VariantOverrideSerializer < ActiveModel::Serializer
attributes :id, :hub_id, :variant_id, :price, :count_on_hand
attributes :id, :hub_id, :variant_id, :sku, :price, :count_on_hand, :on_demand, :default_stock, :resettable
end

View File

@@ -1,5 +1,5 @@
class Api::Admin::VariantSerializer < ActiveModel::Serializer
attributes :id, :options_text, :unit_value, :unit_description, :unit_to_display, :on_demand, :display_as, :display_name, :name_to_display
attributes :id, :options_text, :unit_value, :unit_description, :unit_to_display, :on_demand, :display_as, :display_name, :name_to_display, :sku
attributes :on_hand, :price
has_many :variant_overrides

View File

@@ -1,5 +1,5 @@
class Api::LineItemSerializer < ActiveModel::Serializer
attributes :id, :quantity, :price
attributes :id, :quantity, :max_quantity, :price
has_one :variant, serializer: Api::VariantSerializer
end

View File

@@ -12,25 +12,13 @@
.seven.columns.omega &nbsp;
.row{ 'ng-hide' => '!loaded() || filteredCustomers.length == 0' }
.controls{ :class => "sixteen columns alpha", :style => "margin-bottom: 15px;" }
.controls.sixteen.columns.alpha.omega
.five.columns.alpha
%input{ :class => "fullwidth", :type => "text", :id => 'quick_search', 'ng-model' => 'quickSearch', :placeholder => 'Quick Search' }
%input.fullwidth{ :type => "text", :id => 'quick_search', 'ng-model' => 'quickSearch', :placeholder => 'Quick Search' }
.five.columns &nbsp;
-# %div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "bulk_actions_dropdown", 'ofn-drop-down' => true }
-# %span{ :class => 'icon-check' } &nbsp; Actions
-# %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" }
-# %div.menu{ 'ng-show' => "expanded" }
-# %div.menu_item{ :class => "three columns alpha", 'ng-repeat' => "action in bulkActions", 'ng-click' => "selectedBulkAction.callback(filteredCustomers)", 'ofn-close-on-click' => true }
-# %span{ :class => 'three columns omega' } {{action.name }}
-# =render 'admin/shared/bulk_actions_dropdown'
.three.columns &nbsp;
.three.columns.omega
%div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "columns_dropdown", 'ofn-drop-down' => true, :style => 'float:right;' }
%span{ :class => 'icon-reorder' } &nbsp; Columns
%span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" }
%div.menu{ 'ng-show' => "expanded" }
%div.menu_item{ :class => "three columns alpha", 'ng-repeat' => "column in columns", 'ofn-toggle-column' => true }
%span{ :class => 'one column alpha', :style => 'text-align: center'} {{ column.visible && "&#10003;" || !column.visible && "&nbsp;" }}
%span{ :class => 'two columns omega' } {{column.name }}
= render 'admin/shared/columns_dropdown'
.row{ 'ng-if' => 'shop && !loaded()' }
.sixteen.columns.alpha#loading
%img.spinner{ src: "/assets/spinning-circles.svg" }

View File

@@ -4,21 +4,9 @@
.four.columns.alpha
%input{ :class => "fullwidth", :type => "text", :id => 'quick_search', 'ng-model' => 'quickSearch', :placeholder => 'Search By Name' }
.six.columns &nbsp;
-# %div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "bulk_actions_dropdown", 'ofn-drop-down' => true }
-# %span{ :class => 'icon-check' } &nbsp; Actions
-# %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" }
-# %div.menu{ 'ng-show' => "expanded" }
-# %div.menu_item{ :class => "three columns alpha", 'ng-repeat' => "action in bulkActions", 'ng-click' => "selectedBulkAction.callback(filteredEnterprises)", 'ofn-close-on-click' => true }
-# %span{ :class => 'three columns omega' } {{action.name }}
-# = render 'admin/shared/bulk_actions_dropdown'
.three.columns &nbsp;
.three.columns.omega
%div.ofn_drop_down{ 'ng-controller' => "DropDownCtrl", :id => "columns_dropdown", 'ofn-drop-down' => true, :style => 'float:right;' }
%span{ :class => 'icon-reorder' } &nbsp; Columns
%span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" }
%div.menu{ 'ng-show' => "expanded" }
%div.menu_item{ :class => "three columns alpha", 'ng-repeat' => "column in columns", 'ofn-toggle-column' => true }
%span{ :class => 'one column alpha', :style => 'text-align: center'} {{ column.visible && "&#10003;" || !column.visible && "&nbsp;" }}
%span{ :class => 'two columns omega' } {{column.name }}
= render 'admin/shared/columns_dropdown'
.row{ 'ng-if' => '!loaded' }
.sixteen.columns.alpha#loading
%img.spinner{ src: "/assets/spinning-circles.svg" }

View File

@@ -0,0 +1,7 @@
.three.columns
.ofn-drop-down#bulk-actions-dropdown{ 'ng-controller' => "DropDownCtrl" }
%span.icon-check &nbsp; Actions
%span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" }
%div.menu{ 'ng-show' => "expanded" }
.three.columns.alpha.menu_item{ 'ng-repeat' => "action in bulkActions", 'ng-click' => "$eval(action.callback)(filteredLineItems)", 'ofn-close-on-click' => true }
%span.three.columns.omega {{action.name }}

View File

@@ -0,0 +1,8 @@
%div.three.columns.omega
%div.ofn-drop-down.right#columns-dropdown{ 'ng-controller' => "DropDownCtrl" }
%span{ :class => 'icon-reorder' } &nbsp; Columns
%span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" }
%div.menu{ 'ng-show' => "expanded" }
%div.menu_item.three.columns.alpha.omega{ 'ng-repeat' => "column in columns", 'ofn-toggle-column' => true }
%span.one.column.alpha.text-center {{ column.visible && "&#10003;" || !column.visible && "&nbsp;" }}
%span.two.columns.omega {{column.name }}

View File

@@ -1,4 +0,0 @@
.row
%input.four.columns.alpha{type: 'button', value: 'Save Changes', 'ng-click' => 'update()'}
.twelve.columns.omega
= render 'spree/admin/shared/status_message'

View File

@@ -1,5 +1,5 @@
= admin_inject_spree_api_key
= admin_inject_hubs
= admin_inject_hubs module: 'admin.variantOverrides'
= admin_inject_hub_permissions
= admin_inject_producers
= admin_inject_producers module: 'admin.variantOverrides'
= admin_inject_variant_overrides

View File

@@ -0,0 +1,26 @@
.filters.sixteen.columns.alpha
.filter.four.columns.alpha
%label{ :for => 'query', ng: {class: '{disabled: !hub.id}'} }Quick Search
%br
%input.fullwidth{ :type => "text", :id => 'query', ng: { model: 'query', disabled: '!hub.id'} }
.two.columns &nbsp;
.filter_select.four.columns
%label{ :for => 'hub_id', ng: { bind: 'hub_id ? "Shop" : "Select a shop"' } }
%br
%select.select2.fullwidth#hub_id{ 'ng-model' => 'hub_id', name: 'hub_id', ng: { options: 'hub.id as hub.name for (id, hub) in hubs', change: 'selectHub()' } }
.filter_select.four.columns
%label{ :for => 'producer_filter', ng: {class: '{disabled: !hub.id}'} }Producer
%br
%input.ofn-select2.fullwidth{ :id => 'producer_filter', type: 'number', style: 'display:none', data: 'producers', blank: "{id: 0, name: 'All'}", ng: { model: 'producerFilter', disabled: '!hub.id' } }
-# .filter_select{ :class => "three columns" }
-# %label{ :for => 'distributor_filter' }Hub
-# %br
-# %select{ :class => "three columns alpha", :id => 'distributor_filter', 'select2-min-search' => 5, 'ng-model' => 'distributorFilter', 'ng-options' => 'd.id as d.name for d in distributors'}
-# .filter_select{ :class => "three columns" }
-# %label{ :for => 'order_cycle_filter' }Order Cycle
-# %br
-# %select{ :class => "three columns alpha", :id => 'order_cycle_filter', 'select2-min-search' => 5, 'ng-model' => 'orderCycleFilter', 'ng-options' => 'oc.id as oc.name for oc in orderCycles', 'confirm-change' => "confirmRefresh()", 'ng-change' => 'refreshData()'}
.filter_clear.two.columns.omega
%label{ :for => 'clear_all_filters' }
%br
%input.red.fullwidth{ :type => 'button', :id => 'clear_all_filters', :value => "Clear All", ng: { click: "resetSelectFilters()", disabled: '!hub.id'} }

View File

@@ -1,7 +0,0 @@
.row
.two.columns.alpha
Hub
.four.columns
%select.select2.fullwidth#hub_id{ 'ng-model' => 'hub_id', name: 'hub_id', 'ng-options' => 'hub.id as hub.name for hub in hubs' }
.ten.columns.omega
%input{ type: 'button', value: 'Go', 'ng-click' => 'selectHub()' }

View File

@@ -1,10 +1,23 @@
%table.index.bulk{ng: {show: 'hub'}}
%table.index.bulk{ ng: {show: 'hub'}}
%col.producer{ width: "20%", ng: { show: 'columns.producer.visible' } }
%col.product{ width: "20%", ng: { show: 'columns.product.visible' } }
%col.sku{ width: "20%", ng: { show: 'columns.sku.visible' } }
%col.price{ width: "10%", ng: { show: 'columns.price.visible' } }
%col.on_hand{ width: "10%", ng: { show: 'columns.on_hand.visible' } }
%col.on_demand{ width: "10%", ng: { show: 'columns.on_demand.visible' } }
%col.reset{ width: "1%", ng: { show: 'columns.reset.visible' } }
%col.reset{ width: "15%", ng: { show: 'columns.reset.visible' } }
%col.inheritance{ width: "5%", ng: { show: 'columns.inheritance.visible' } }
%thead
%tr
%th Producer
%th Product
%th Price
%th On hand
%tbody{ng: {repeat: 'product in products | hubPermissions:hubPermissions:hub.id'}}
%tr{ ng: { controller: "ColumnsCtrl" } }
%th.producer{ ng: { show: 'columns.producer.visible' } } Producer
%th.product{ ng: { show: 'columns.product.visible' } } Product
%th.sku{ ng: { show: 'columns.sku.visible' } } SKU
%th.price{ ng: { show: 'columns.price.visible' } } Price
%th.on_hand{ ng: { show: 'columns.on_hand.visible' } } On hand
%th.on_demand{ ng: { show: 'columns.on_demand.visible' } } On Demand?
%th.reset{ colspan: 2, ng: { show: 'columns.reset.visible' } } Enable Stock Level Reset?
%th.inheritance{ ng: { show: 'columns.inheritance.visible' } } Inherit?
%tbody{bindonce: true, ng: {repeat: 'product in products | hubPermissions:hubPermissions:hub.id | attrFilter:{producer_id:producerFilter} | filter:query' } }
= render 'admin/variant_overrides/products_product'
= render 'admin/variant_overrides/products_variants'

View File

@@ -1,5 +1,9 @@
%tr.product.even
%td {{ producers[product.producer_id].name }}
%td {{ product.name }}
%td
%td
%td.producer{ ng: { show: 'columns.producer.visible' }, bo: { bind: 'producersByID[product.producer_id].name'} }
%td.product{ ng: { show: 'columns.product.visible' }, bo: { bind: 'product.name'} }
%td.sku{ ng: { show: 'columns.sku.visible' } }
%td.price{ ng: { show: 'columns.price.visible' } }
%td.on_hand{ ng: { show: 'columns.on_hand.visible' } }
%td.on_demand{ ng: { show: 'columns.on_demand.visible' } }
%td.reset{ colspan: 2, ng: { show: 'columns.reset.visible' } }
%td.inheritance{ ng: { show: 'columns.inheritance.visible' } }

View File

@@ -1,10 +1,19 @@
%tr.variant{ng: {repeat: 'variant in product.variants'}}
%td
%td
{{ variant.display_name }}
.variant-override-unit {{ variant.unit_to_display }}
%td
%tr.variant{ id: "v_{{variant.id}}", ng: {repeat: 'variant in product.variants'}}
%td.producer{ ng: { show: 'columns.producer.visible' } }
%td.product{ ng: { show: 'columns.product.visible' } }
%span{ bo: { bind: 'variant.display_name || ""'} }
.variant-override-unit{ bo: { bind: 'variant.unit_to_display'} }
%td.sku{ ng: { show: 'columns.sku.visible' } }
%input{name: 'variant-overrides-{{ variant.id }}-sku', type: 'text', ng: {model: 'variantOverrides[hub.id][variant.id].sku'}, placeholder: '{{ variant.sku }}', 'ofn-track-variant-override' => 'sku'}
%td.price{ ng: { show: 'columns.price.visible' } }
%input{name: 'variant-overrides-{{ variant.id }}-price', type: 'text', ng: {model: 'variantOverrides[hub.id][variant.id].price'}, placeholder: '{{ variant.price }}', 'ofn-track-variant-override' => 'price'}
%td
%input{name: 'variant-overrides-{{ variant.id }}-count-on-hand', type: 'text', ng: {model: 'variantOverrides[hub.id][variant.id].count_on_hand'}, placeholder: '{{ variant.on_hand }}', 'ofn-track-variant-override' => 'price'}
%td.on_hand{ ng: { show: 'columns.on_hand.visible' } }
%input{name: 'variant-overrides-{{ variant.id }}-count_on_hand', type: 'text', ng: {model: 'variantOverrides[hub.id][variant.id].count_on_hand'}, placeholder: '{{ variant.on_hand }}', 'ofn-track-variant-override' => 'count_on_hand'}
%td.on_demand{ ng: { show: 'columns.on_demand.visible' } }
%input.field{ :type => 'checkbox', name: 'variant-overrides-{{ variant.id }}-on_demand', ng: { model: 'variantOverrides[hub.id][variant.id].on_demand' }, 'ofn-track-variant-override' => 'on_demand' }
%td.reset{ ng: { show: 'columns.reset.visible' } }
%input{name: 'variant-overrides-{{ variant.id }}-resettable', type: 'checkbox', ng: {model: 'variantOverrides[hub.id][variant.id].resettable'}, placeholder: '{{ variant.resettable }}', 'ofn-track-variant-override' => 'resettable'}
%td.reset{ ng: { show: 'columns.reset.visible' } }
%input{name: 'variant-overrides-{{ variant.id }}-default_stock', type: 'text', ng: {model: 'variantOverrides[hub.id][variant.id].default_stock'}, placeholder: '{{ variant.default_stock ? variant.default_stock : "Default stock"}}', 'ofn-track-variant-override' => 'default_stock'}
%td.inheritance{ ng: { show: 'columns.inheritance.visible' } }
%input.field{ :type => 'checkbox', name: 'variant-overrides-{{ variant.id }}-inherit', ng: { model: 'inherit' }, 'track-inheritance' => true }

View File

@@ -1,11 +1,14 @@
= render 'admin/variant_overrides/header'
= render 'admin/variant_overrides/data'
%div{ ng: { app: 'ofn.admin', controller: 'AdminVariantOverridesCtrl', init: 'initialise()' } }
= render 'admin/variant_overrides/hub_choice'
%div{ ng: { app: 'admin.variantOverrides', controller: 'AdminVariantOverridesCtrl', init: 'initialise()' } }
= render 'admin/variant_overrides/filters'
%hr.divider.sixteen.columns.alpha.omega{ ng: { show: 'hub' } }
.controls.sixteen.columns.alpha.omega{ ng: { show: 'hub' } }
%input.four.columns.alpha{ type: 'button', value: 'Reset Stock to Defaults', 'ng-click' => 'resetStock()' }
%div.nine.columns.alpha &nbsp;
= render 'admin/shared/columns_dropdown'
%div{ng: {show: 'hub'}}
%h2 {{ hub.name }}
= render 'admin/variant_overrides/actions'
= render 'admin/variant_overrides/products'
%form{ name: 'variant_overrides_form' }
%save-bar{ save: "update()", form: "variant_overrides_form" }
= render 'admin/variant_overrides/products'

View File

@@ -1,5 +1,9 @@
- content_for(:title) do
= current_distributor.name
- content_for(:description) do
= current_distributor.description
- content_for(:image) do
= current_distributor.logo.url
= inject_enterprises

View File

@@ -0,0 +1,21 @@
.row
= render partial: 'shared/components/filter_controls'
= render partial: 'shared/components/show_profiles'
.row.animate-show{"ng-show" => "filtersActive"}
.small-12.columns
.row.filter-box
.small-12.large-9.columns
%h5.tdhead
.light
= t :hubs_filter_by
= t :hubs_filter_type
%filter-selector.small-block-grid-2.medium-block-grid-4.large-block-grid-5{"selector-set" => "filterSelectors", objects: "group_hubs | searchEnterprises:query | shipping:shippingTypes | showHubProfiles:show_profiles | taxonsOf", "active-selectors" => "activeTaxons"}
.small-12.large-3.columns
%h5.tdhead
.light
= t :hubs_filter_by
= t :hubs_filter_delivery
%shipping-type-selector
= render partial: 'shared/components/filter_box'

View File

@@ -7,10 +7,14 @@
angular.module('Darkswarm').value('groups', #{render partial: "json/groups", object: @groups})
#groups.pad-top.footer-pad{"ng-controller" => "GroupsCtrl"}
#active-table-search.row.pad-top
.small-12.columns
.row
.small-12.medium-6.medium-offset-3.columns.text-center
%h1
= t :groups_headline
%p.text
= t :groups_text
#active-table-search.row.pad-top
.small-12.columns
%p
%input{type: :text,
"ng-model" => "query",

View File

@@ -1,5 +1,9 @@
- content_for(:title) do
= @group.name
- content_for(:description) do
= @group.description
- content_for(:image) do
= @group.logo.url
-# inject all enterprises as "enterprises"
-# it could be more efficient to inject only the enterprises that are related to the group
@@ -55,14 +59,13 @@
%h1
= t :groups_producers
= render partial: "shared/components/enterprise_search"
-# TODO: find out why this is not working
-#= render partial: "producers/filters"
= render partial: "producers/filters"
.row{bindonce: true}
.small-12.columns
.active_table
%producer.active_table_node.row.animate-repeat{id: "{{producer.path}}",
"ng-repeat" => "producer in filteredEnterprises = (group_producers | visible | searchEnterprises:query | taxons:activeTaxons)",
"ng-repeat" => "producer in filteredEnterprises = (group_producers | searchEnterprises:query | taxons:activeTaxons)",
"ng-controller" => "GroupEnterpriseNodeCtrl",
"ng-class" => "{'closed' : !open(), 'open' : open(), 'inactive' : !producer.active}",
id: "{{producer.hash}}"}
@@ -83,17 +86,13 @@
= t :groups_hubs
= render partial: "shared/components/enterprise_search"
-# TODO: find out why this is not working
-#= render partial: "home/filters"
.small-12.medium-6.columns
%span &nbsp;
= render partial: 'shared/components/show_profiles'
= render partial: "hub_filters"
.row{bindonce: true}
.small-12.columns
.active_table
%hub.active_table_node.row.animate-repeat{id: "{{hub.hash}}",
"ng-repeat" => "hub in filteredEnterprises = (group_hubs | visible | searchEnterprises:query | taxons:activeTaxons | shipping:shippingTypes | showHubProfiles:show_profiles | orderBy:['-active', '+orders_close_at'])",
"ng-repeat" => "hub in filteredEnterprises = (group_hubs | searchEnterprises:query | taxons:activeTaxons | shipping:shippingTypes | showHubProfiles:show_profiles | orderBy:['-active', '+orders_close_at'])",
"ng-class" => "{'is_profile' : hub.category == 'hub_profile', 'closed' : !open(), 'open' : open(), 'inactive' : !hub.active, 'current' : current()}",
"ng-controller" => "GroupEnterpriseNodeCtrl"}
.small-12.columns

View File

@@ -11,13 +11,12 @@
.light
= t :hubs_filter_by
= t :hubs_filter_type
%filter-selector.small-block-grid-2.medium-block-grid-4.large-block-grid-5{ objects: "visibleMatches | visible | taxonsOf", "active-selectors" => "activeTaxons" }
%filter-selector.small-block-grid-2.medium-block-grid-4.large-block-grid-5{ "selector-set" => "filterSelectors", objects: "visibleMatches | visible | taxonsOf", "active-selectors" => "activeTaxons" }
.small-12.large-3.columns
%h5.tdhead
.light
= t :hubs_filter_by
= t :hubs_filter_delivery
%ul.small-block-grid-2.medium-block-grid-4.large-block-grid-2
%shipping-type-selector{results: "shippingTypes"}
%shipping-type-selector
= render partial: 'shared/components/filter_box'

View File

@@ -2,7 +2,9 @@
%head
%meta{charset: 'utf-8'}/
%meta{name: 'viewport', content: "width=device-width,initial-scale=1.0"}/
%meta{property: "og:title", content: content_for?(:title) ? yield(:title) : t(:title)}
%meta{property: "og:description", content: content_for?(:description) ? yield(:description) : t(:site_meta_description)}
%meta{property: "og:image", content: content_for?(:image) ? yield(:image) : ContentConfig.logo.url}
%title= content_for?(:title) ? "#{yield(:title)} - #{t(:title)}".html_safe : "#{t(:welcome_to)} #{t(:title)}"
- if Rails.env.production?
= favicon_link_tag

View File

@@ -11,5 +11,5 @@
.light
= t :producers_filter
= t :producers_filter_type
%filter-selector.small-block-grid-2.medium-block-grid-4.large-block-grid-6{objects: "Enterprises.producers | searchEnterprises:query | taxonsOf", "active-selectors" => "activeTaxons"}
%filter-selector.small-block-grid-2.medium-block-grid-4.large-block-grid-6{"selector-set" => "filterSelectors", objects: "producers_to_filter | searchEnterprises:query | taxonsOf", "active-selectors" => "activeTaxons"}
= render partial: 'shared/components/filter_box'

View File

@@ -150,7 +150,7 @@
= t :footer_legal_tos
&#124;
= t :footer_legal_visit
%a{href:"https://github.com/openfoodfoundation/openfoodnetwork", target: "_blank"} Github
%a{href:"https://github.com/openfoodfoundation/openfoodnetwork", target: "_blank"} GitHub
%p.text-small
= t :footer_legal_text_html, {content_license: link_to('CC BY-SA 3.0', 'https://creativecommons.org/licenses/by-sa/3.0/'), code_license: link_to('AGPL 3', 'https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)')}

View File

@@ -1,5 +1,5 @@
.row.filter-box.clear-filters.animate-show{"ng-show" => "filtersActive && totalActive() > 0"}
.row.filter-box.clear-filters.animate-show{"ng-show" => "filtersActive && filterSelectors.totalActive() > 0"}
.small-12.columns
%a.button.secondary.small.expand{"ng-click" => "clearAll()"}
%a.button.secondary.small.expand{"ng-click" => "filterSelectors.clearAll()"}
%i.ofn-i_009-close
= t :components_filters_clearfilters

View File

@@ -1,4 +0,0 @@
%span.animate-show{"ng-show" => "filtersActive && totalActive() > 0"}
%a.button.secondary.tiny{"ng-click" => "clearAll()"}
%i.ofn-i_009-close
= t :components_filters_clearfilters

View File

@@ -1,9 +1,9 @@
.small-12.medium-6.columns
%a.button.success.tiny.filterbtn{"ng-click" => "filtersActive = !filtersActive",
"ng-show" => "FilterSelectorsService.selectors.length > 0"}
{{ filterText(filtersActive) }}
"ng-show" => "filterSelectors.selectors.length > 0"}
{{ filterSelectors.filterText(filtersActive) }}
%i.ofn-i_005-caret-down{"ng-show" => "!filtersActive"}
%i.ofn-i_006-caret-up{"ng-show" => "filtersActive"}
%a.button.secondary.tiny.filterbtn.disabled{"ng-show" => "FilterSelectorsService.selectors.length == 0"}
%a.button.secondary.tiny.filterbtn.disabled{"ng-show" => "filterSelectors.selectors.length == 0"}
= t :components_filters_nofilters

View File

@@ -1,8 +0,0 @@
%a.button.success.tiny.filterbtn{"ng-click" => "filtersActive = !filtersActive",
"ng-show" => "FilterSelectorsService.selectors.length > 0"}
{{ filterText(filtersActive) }}
%i.ofn-i_005-caret-down{"ng-show" => "!filtersActive"}
%i.ofn-i_006-caret-up{"ng-show" => "filtersActive"}
%a.button.secondary.tiny.filterbtn.disabled{"ng-show" => "FilterSelectorsService.selectors.length == 0"}
= t :components_filters_nofilters

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