Merge branch 'uk/trial-length' of https://github.com/openfoodfoundation/openfoodnetwork into uk/trial-length

This commit is contained in:
Lynne Davis
2016-04-15 18:51:14 +01:00
267 changed files with 5927 additions and 800 deletions

View File

@@ -38,7 +38,7 @@ before_script:
script:
- '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"
- "bundle exec rake 'knapsack:rspec[--tag ~performance]'"
after_success:
- >

11
Gemfile
View File

@@ -5,16 +5,18 @@ gem 'rails', '3.2.21'
gem 'rails-i18n', '~> 3.0.0'
gem 'i18n', '~> 0.6.11'
gem 'nokogiri'
# Patched version. See http://rubysec.com/advisories/CVE-2015-5312/.
gem 'nokogiri', '>= 1.6.7.1'
gem 'pg'
gem 'spree', github: 'openfoodfoundation/spree', branch: '1-3-stable'
gem 'spree_i18n', github: 'spree/spree_i18n', branch: '1-3-stable'
gem 'spree_auth_devise', github: 'spree/spree_auth_devise', branch: '1-3-stable'
# Waiting on merge of PR #117
# https://github.com/spree-contrib/better_spree_paypal_express/pull/117
gem 'spree_paypal_express', :github => "openfoodfoundation/better_spree_paypal_express", :branch => "1-3-stable"
# Our branch contains two changes
# - Pass customer email and phone number to PayPal (merged to upstream master)
# - Change type of password from string to password to hide it in the form
gem 'spree_paypal_express', :github => "openfoodfoundation/better_spree_paypal_express", :branch => "hide-password"
#gem 'spree_paypal_express', :github => "spree-contrib/better_spree_paypal_express", :branch => "1-3-stable"
gem 'delayed_job_active_record'
@@ -55,6 +57,7 @@ gem 'figaro'
gem 'blockenspiel'
gem 'acts-as-taggable-on', '~> 3.4'
gem 'paper_trail', '~> 3.0.8'
gem 'diffy'
gem 'wicked_pdf'
gem 'wkhtmltopdf-binary'

View File

@@ -14,8 +14,8 @@ GIT
GIT
remote: git://github.com/openfoodfoundation/better_spree_paypal_express.git
revision: cdd61161ccd27cd8d183f9321422c7be113796b8
branch: 1-3-stable
revision: 840d973cd5bd3250b17674a624dad494aeb09eb3
branch: hide-password
specs:
spree_paypal_express (2.0.3)
paypal-sdk-merchant (= 1.106.1)
@@ -248,6 +248,7 @@ GEM
devise-encryptable (0.1.2)
devise (>= 2.1.0)
diff-lcs (1.2.4)
diffy (3.1.0)
em-websocket (0.5.0)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.5.3)
@@ -449,7 +450,7 @@ GEM
treetop (~> 1.4.8)
method_source (0.8.2)
mime-types (1.25.1)
mini_portile (0.6.2)
mini_portile2 (2.0.0)
momentjs-rails (2.5.1)
railties (>= 3.1)
money (5.1.1)
@@ -457,8 +458,8 @@ GEM
multi_json (1.11.2)
multi_xml (0.5.5)
newrelic_rpm (3.12.0.288)
nokogiri (1.6.6.4)
mini_portile (~> 0.6.0)
nokogiri (1.6.7.2)
mini_portile2 (~> 2.0.0.rc2)
oj (2.1.2)
orm_adapter (0.5.0)
paper_trail (3.0.8)
@@ -668,6 +669,7 @@ DEPENDENCIES
debugger-linecache
deface!
delayed_job_active_record
diffy
factory_girl_rails
figaro
foreigner
@@ -691,7 +693,7 @@ DEPENDENCIES
letter_opener
momentjs-rails
newrelic_rpm
nokogiri
nokogiri (>= 1.6.7.1)
oj
paper_trail (~> 3.0.8)
paperclip
@@ -730,3 +732,6 @@ DEPENDENCIES
whenever
wicked_pdf
wkhtmltopdf-binary
BUNDLED WITH
1.10.6

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 300 104"
enable-background="new 0 0 300 104"
id="svg2"
version="1.1"
inkscape:version="0.48.3.1 r9886"
sodipodi:docname="logo-black.svg"
width="100%"
height="100%">
<metadata
id="metadata24">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs22" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1280"
inkscape:window-height="741"
id="namedview20"
showgrid="false"
inkscape:zoom="1.8101934"
inkscape:cx="126.57728"
inkscape:cy="62.030566"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="g4" />
<g
id="g4"
fill="#fff">
<path
d="M142.7 15.6c.4 3.2.4 7.5 0 10.7-.5 4.6-3.8 8.4-9.1 8.4s-8.5-3.8-9.1-8.4c-.4-3.2-.4-7.5 0-10.7.5-4.6 3.8-8.4 9.1-8.4s8.6 3.8 9.1 8.4m-5.3 10.7c.4-3.2.4-7.5 0-10.7-.2-1.7-1.4-3-3.7-3-2.3 0-3.5 1.4-3.7 3-.4 3.2-.4 7.5 0 10.7.2 1.7 1.4 3 3.7 3 2.3 0 3.5-1.3 3.7-3"
id="path6"
style="fill:#000000" />
<path
d="M156.4 7.6c4.4 0 8 3.6 8 8s-3.6 8-8 8H153c-.2 0-.4.2-.4.4v9.5c0 .4-.3.8-.8.8H148c-.4 0-.8-.3-.8-.8V8.4c0-.4.3-.8.8-.8h8.4zm-3.5 5.3c-.2 0-.4.2-.4.4v4.6c0 .2.2.4.4.4h3.4c1.4 0 2.7-1.2 2.7-2.7 0-1.4-1.2-2.7-2.7-2.7h-3.4zM172.9 28.6c0 .2.2.4.4.4h9c.4 0 .8.3.8.8v3.8c0 .4-.3.8-.8.8h-14c-.4 0-.8-.3-.8-.8V8.4c0-.4.3-.8.8-.8h13.8c.4 0 .8.3.8.8v3.8c0 .4-.3.8-.8.8h-8.9c-.2 0-.4.2-.4.4V18c0 .2.2.4.4.4h8.6c.4 0 .8.3.8.8V23c0 .4-.3.8-.8.8h-8.6c-.2 0-.4.2-.4.4v4.4zM199.4 34.3c-.6 0-.9-.3-1-.6l-5.2-13.4c-.1-.2-.3-.2-.3.1v13.2c0 .4-.3.8-.8.8h-3.8c-.4 0-.8-.3-.8-.8V8.4c0-.4.3-.8.8-.8h4.5c.6 0 .8.3 1 .7l5.2 14.5c.1.2.3.2.3-.1V8.4c0-.4.3-.8.8-.8h3.8c.4 0 .8.3.8.8v25.1c0 .4-.3.8-.8.8h-4.5zM224.4 24v9.5c0 .4-.3.8-.8.8h-3.8c-.4 0-.8-.3-.8-.8V8.4c0-.4.3-.8.8-.8h13.3c.4 0 .8.3.8.8v3.8c0 .4-.3.8-.8.8h-8.4c-.2 0-.4.2-.4.4V18c0 .2.2.4.4.4h8c.4 0 .8.3.8.8V23c0 .4-.3.8-.8.8h-8c-.1-.2-.3 0-.3.2M255.6 15.6c.4 3.2.4 7.5 0 10.7-.5 4.6-3.8 8.4-9.1 8.4s-8.5-3.8-9.1-8.4c-.4-3.2-.4-7.5 0-10.7.5-4.6 3.8-8.4 9.1-8.4s8.6 3.8 9.1 8.4m-5.3 10.7c.4-3.2.4-7.5 0-10.7-.2-1.7-1.4-3-3.7-3-2.3 0-3.5 1.4-3.7 3-.4 3.2-.4 7.5 0 10.7.2 1.7 1.4 3 3.7 3 2.3 0 3.5-1.3 3.7-3M278 15.6c.4 3.2.4 7.5 0 10.7-.5 4.6-3.8 8.4-9.1 8.4s-8.5-3.8-9.1-8.4c-.4-3.2-.4-7.5 0-10.7.5-4.6 3.8-8.4 9.1-8.4s8.5 3.8 9.1 8.4m-5.4 10.7c.4-3.2.4-7.5 0-10.7-.2-1.7-1.4-3-3.7-3-2.3 0-3.5 1.4-3.7 3-.4 3.2-.4 7.5 0 10.7.2 1.7 1.4 3 3.7 3 2.3 0 3.5-1.3 3.7-3"
id="path8"
style="fill:#000000" />
<path
d="M291.2 7.6c4.7 0 8 3.8 8.5 8.4.4 3.2.4 6.6 0 9.8-.5 4.6-3.8 8.4-8.5 8.4h-8c-.4 0-.8-.3-.8-.8v-25c0-.4.3-.8.8-.8h8zm-3 5.3c-.2 0-.4.2-.4.4v15.2c0 .2.2.4.4.4h3c1.8 0 2.9-1.4 3.1-3.1.4-3.2.4-6.6 0-9.8-.2-1.7-1.4-3.1-3.1-3.1h-3zM137.5 67.9c-.4 0-.6-.1-.8-.6l-9.1-20.8c-.1-.1-.3-.1-.3.1v20.6c0 .4-.3.7-.8.7h-.8c-.4 0-.8-.3-.8-.7V43.3c0-.4.3-.7.8-.7h2c.4 0 .6.1.8.6l9.1 20.6c.1.1.3.1.3-.1V43.3c0-.4.3-.7.8-.7h.8c.4 0 .8.3.8.7v23.9c0 .4-.3.7-.8.7h-2zM146.7 65.3c0 .2.2.4.4.4h9.9c.4 0 .8.3.8.7v.9c0 .4-.3.7-.8.7h-12c-.4 0-.8-.3-.8-.7v-24c0-.4.3-.7.8-.7h12c.4 0 .8.3.8.7v.9c0 .4-.3.7-.8.7h-9.9c-.2 0-.4.1-.4.4v8.2c0 .2.2.4.4.4h9.4c.4 0 .8.3.8.7v.8c0 .4-.3.7-.8.7h-9.4c-.2 0-.4.1-.4.4v8.8zM170 45.3v21.9c0 .4-.3.7-.8.7h-1c-.4 0-.8-.3-.8-.7V45.3c0-.2-.2-.4-.4-.4h-5.6c-.4 0-.8-.3-.8-.7v-.9c0-.4.3-.7.8-.7H176c.4 0 .8.3.8.7v.9c0 .4-.3.7-.8.7h-5.6c-.2 0-.4.2-.4.4M187 67.9h-2.1c-.3 0-.6-.2-.7-.5L179 43.3c-.1-.4.2-.7.8-.7h.9c.3 0 .6.1.7.5l4.3 20.7c0 .1.1.2.2.2h.1c.1 0 .2-.1.2-.2l4-20.7c.1-.4.4-.5.7-.5h1.3c.3 0 .6.2.7.5l4.3 20.7c0 .1.1.2.2.2h.1c.1 0 .2-.1.2-.2l4.6-20.7c.1-.4.4-.5.7-.5h.9c.6 0 .8.4.8.7l-5.5 24.1c-.1.4-.4.5-.7.5h-2.1c-.3 0-.6-.2-.7-.5l-4-19.3c0-.2-.3-.2-.3 0l-3.8 19.3c0 .4-.3.5-.6.5M223.9 49.8c.4 2.9.4 7.9 0 10.9-.6 4.7-3.9 7.5-8.5 7.5s-7.8-2.9-8.5-7.5c-.4-2.9-.4-8 0-10.9.6-3.9 3.5-7.5 8.5-7.5s7.9 3.6 8.5 7.5m-2.6 10.9c.5-3 .5-7.9 0-10.9-.4-2.6-2.3-5.2-5.9-5.2-3.6 0-5.5 2.6-5.9 5.2-.5 3-.5 7.9 0 10.9.3 2.1 1.8 5.2 5.9 5.2 4.1 0 5.6-3.1 5.9-5.2"
id="path10"
style="fill:#000000" />
<path
d="m 230.9,57.8 c -0.2,0 -0.4,0.1 -0.4,0.4 l 0,9 c 0,0.4 -0.3,0.7 -0.8,0.7 l -0.9,0 c -0.4,0 -0.8,-0.3 -0.8,-0.7 l 0,-23.9 c 0,-0.4 0.3,-0.7 0.8,-0.7 l 6.5,0 c 4.4,0 8,3.4 8,7.6 0,3.3 -1.8,5.2 -4.5,6.9 -0.5,0.3 -0.6,0.6 -0.3,1.2 l 4.8,8.6 c 0.3,0.5 0,1.1 -0.7,1.1 l -0.8,0 c -0.6,0 -0.9,-0.3 -1.1,-0.7 l -4.9,-9 c -0.1,-0.2 -0.3,-0.4 -0.7,-0.4 l -4.2,0 z m 0,-12.9 c -0.2,0 -0.4,0.1 -0.4,0.4 l 0,9.8 c 0,0.2 0.2,0.4 0.4,0.4 l 4.4,0 c 3,0 5.6,-2.4 5.6,-5.3 0,-2.9 -2.5,-5.3 -5.6,-5.3 z m 18.4,12.2 C 249.2,57 249,57 249,57.2 l 0,10 c 0,0.4 -0.3,0.7 -0.8,0.7 l -0.9,0 c -0.4,0 -0.8,-0.3 -0.8,-0.7 l 0,-23.9 c 0,-0.4 0.3,-0.7 0.8,-0.7 l 0.9,0 c 0.4,0 0.8,0.3 0.8,0.7 l 0,9.6 c 0,0.2 0.2,0.2 0.3,0.1 l 8.8,-10 c 0.1,-0.1 0.4,-0.4 1,-0.4 l 0.6,0 c 0.9,0 1.1,0.8 0.6,1.3 l -9.4,10.5 c -0.2,0.3 -0.3,0.5 0,0.9 L 261,66.6 c 0.4,0.5 0.3,1.3 -0.7,1.3 l -0.8,0 c -0.6,0 -0.8,-0.3 -1,-0.5 z"
id="path12"
style="fill:#000000"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cssssssssscccssccsccsssssssscssssssssssccssccccsscc" />
<path
d="m 3,44.3 0.5,0 c 1.2,0 2.2,-0.9 2.4,-2 0.5,-2.4 1.1,-4.7 2,-7 6.6,-18.2 23.5,-30.4 42,-30.4 18.5,0 35.4,12.2 42,30.3 0.8,2.2 1.5,4.6 2,6.9 0.3,1.3 1.6,2.2 2.9,1.9 1.3,-0.3 2.2,-1.6 1.9,-2.9 C 98.2,38.5 97.4,36 96.5,33.5 89.2,13.5 70.5,0 49.9,0 29.3,0 10.6,13.5 3.3,33.6 2.4,36.1 1.7,38.7 1.1,41.3 l 0,0.2 L 3.5,42 1.1,41.5 C 0.8,42.8 1.7,44.1 3,44.3 M 99.8,53 C 99.9,52 99.4,51 98.4,50.5 95,48.9 90.7,47.4 85.9,46.3 84.4,45.9 82.8,45.6 81.2,45.3 77.9,44.7 74.3,44.1 70.5,43.7 69.8,36.3 66.6,30.9 61,27.1 54.7,22.9 46.9,22.7 40.1,26.5 33.5,30.1 29.3,36.6 29,43.8 17.9,45.2 8.2,47.6 1.4,50.9 1.3,50.9 1.3,51 1.2,51 1.1,51 1.1,51.1 1,51.1 l -0.1,0.1 c -0.1,0.1 -0.2,0.1 -0.2,0.2 -0.4,0.4 -0.6,1 -0.6,1.6 0,1.4 0.1,2.7 0.2,4.1 0,0.4 0.1,0.7 0.1,1.1 0.3,2.6 0.8,5.2 1.5,7.7 0.2,0.7 0.3,1.4 0.5,2 0.3,0.9 0.6,1.7 0.9,2.6 0.7,1.8 1.4,3.6 2.2,5.2 0.8,1.6 1.7,3.1 2.6,4.6 0.7,1.2 1.5,2.3 2.4,3.5 1,1.3 2,2.6 3.2,3.8 1.5,1.7 3.1,3.2 4.8,4.6 1.4,1.2 2.9,2.3 4.4,3.3 1.9,1.3 3.8,2.4 5.9,3.4 2,0.9 4,1.8 6.1,2.4 2.1,0.7 4.3,1.2 6.5,1.6 2.8,0.5 5.7,0.8 8.7,0.8 l 0.1,0 c 13.6,0 26.5,-5.9 35.8,-15.9 1.8,-1.9 3.4,-3.9 4.9,-6.1 0.2,-0.2 0.3,-0.5 0.5,-0.7 1.5,-2.4 2.9,-4.8 4,-7.4 0.9,-2 1.6,-4 2.2,-6 1.3,-4.5 2.1,-9.5 2.2,-14.6 0,0.1 0,0.1 0,0 M 85.7,51.3 c 3.3,0.8 6.3,1.8 8.9,2.9 -3.4,1.6 -6.7,2.9 -9.9,4.1 -1.9,0.7 -3.7,1.2 -5.5,1.7 -3.5,1 -7.1,1.6 -10.7,2 1.3,-4.4 2,-8.8 2.2,-13.4 3.6,0.4 7,1 10.2,1.6 1.7,0.4 3.3,0.7 4.8,1.1 m -13,23.1 C 69.5,74.8 66.2,75 63,75 c 1.5,-2.5 2.7,-5.2 3.8,-7.8 3.6,-0.2 7,-0.7 10.4,-1.5 -1.3,2.9 -2.8,5.8 -4.5,8.7 m -9.8,12.5 c -2.8,0 -5.6,-0.1 -8.4,-0.5 1.9,-2.1 3.7,-4.3 5.4,-6.5 1.2,0.1 2.5,0.1 3.7,0.1 1.8,0 3.7,-0.1 5.5,-0.2 -1.9,2.4 -4,4.8 -6.2,7.1 M 40.2,82.4 c 1.6,-1.7 3,-3.4 4.4,-5.2 3.2,0.9 6.4,1.6 9.5,2 -1.7,2.1 -3.5,4.1 -5.4,6 -2.8,-0.7 -5.6,-1.6 -8.5,-2.8 M 6.6,65 C 6.1,63.1 5.7,61.2 5.4,59.2 c 7.2,-1.8 16.6,-1.4 26.2,1.3 -0.9,2 -2,3.9 -3.2,5.8 C 19.3,64 11.6,64.2 6.6,65 m 50.7,9.7 c -3.2,-0.3 -6.5,-0.9 -9.8,-1.7 1.4,-2.2 2.6,-4.5 3.7,-6.9 3.5,0.6 6.8,1 10.1,1.1 -1.1,2.5 -2.4,5.1 -4,7.5 M 42.7,71.4 c -1.5,-0.5 -3,-1.1 -4.5,-1.8 -1.7,-0.7 -3.3,-1.3 -4.9,-1.9 1.1,-1.9 2.2,-3.8 3.1,-5.8 3.5,1.2 6.8,2.2 10,3 -1.1,2.3 -2.4,4.5 -3.7,6.5 M 30.5,72 c 1.8,0.6 3.8,1.3 5.7,2.2 1.2,0.5 2.3,1 3.5,1.4 -1.3,1.7 -2.8,3.3 -4.3,4.8 -3.1,-1.2 -6.2,-2.3 -9.3,-3 1.5,-1.8 3,-3.6 4.4,-5.4 m 12,-41.3 c 5.2,-2.9 11.1,-2.7 15.8,0.4 4.2,2.8 6.5,6.7 7.3,12 -5,-0.4 -10.2,-0.6 -15.5,-0.6 -5.5,0 -10.9,0.3 -16,0.7 0.4,-5.1 3.5,-9.8 8.4,-12.5 m 7.5,16.8 2.1,0 c 1,0 2,0 3.1,0.1 3.6,0.1 7.2,0.3 10.6,0.6 -0.2,4.8 -1,9.5 -2.5,14.1 -3.2,0 -6.5,-0.3 -10,-0.9 -1.6,-0.3 -3.2,-0.6 -4.9,-1 -3.2,-0.7 -6.6,-1.7 -10.2,-3 -0.1,0 -0.2,-0.1 -0.3,-0.1 C 36.4,56.8 35,56.3 33.5,55.9 23.9,53.1 14.6,52.4 6.8,53.9 17.1,49.8 32.9,47.5 50,47.5 M 8.2,69.7 c 4.2,-0.5 10.1,-0.7 17.2,0.9 -1.5,1.9 -3.2,3.8 -5,5.5 -3.3,-0.5 -6.5,-0.8 -9.6,-0.7 -1,-1.8 -1.9,-3.7 -2.6,-5.7 m 9.5,15.2 c -1.1,-1.2 -2.2,-2.4 -3.2,-3.8 -0.2,-0.3 -0.4,-0.5 -0.6,-0.8 0.5,0 1,0 1.5,0.1 2,0.2 4.2,0.4 6.5,0.9 2.8,0.6 6,1.5 9.3,2.7 -2.5,2.1 -5.3,4.1 -8.2,5.8 -1.8,-1.4 -3.6,-3.1 -5.3,-4.9 m 9.9,8 c 3.2,-2 6.1,-4.3 8.8,-6.8 2.7,1.2 5.3,2.2 8,2.9 -3,2.6 -6.3,5.1 -10,7.1 -2.4,-0.8 -4.7,-1.9 -6.8,-3.2 m 13.1,5.2 c 3.5,-2.3 6.6,-5 9.4,-7.5 2.7,0.5 5.3,0.9 7.9,1.1 -3.2,3 -6.3,5.4 -9.4,7.4 -2.6,-0.2 -5.3,-0.5 -7.9,-1 m 17.1,0.2 c 2.4,-1.9 4.7,-4.1 7.2,-6.6 3.6,-0.1 7.2,-0.6 10.8,-1.3 -5.3,4 -11.5,6.7 -18,7.9 M 86.7,79.2 c -1.1,1.6 -2.3,3.1 -3.5,4.5 -4.4,1.2 -8.8,2.2 -13.2,2.7 2.1,-2.4 3.9,-4.9 5.6,-7.5 4.4,-0.7 8.8,-1.7 13.1,-3 -0.7,1.2 -1.3,2.2 -2,3.3 m -7.8,-5.9 c 1.6,-3.1 3,-6.2 4.1,-9.3 3.7,-1.2 7.4,-2.6 11.3,-4.3 -0.5,3.5 -1.5,6.9 -2.7,10.2 -4.2,1.4 -8.4,2.5 -12.7,3.4"
id="path18"
style="fill:#000000"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csccsccsccscccccccccccccccscscscccscsccccccccsscccccccccccccccccccccccsccccccccccccccccccccccccccccccccscccscccccccccccccccccccccccccccccccccccccccccccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@@ -38,6 +38,7 @@
//= require ./products/products
//= require ./shipping_methods/shipping_methods
//= require ./side_menu/side_menu
//= require ./tag_rules/tag_rules
//= require ./taxons/taxons
//= require ./utils/utils
//= require ./users/users
@@ -45,5 +46,8 @@
//= require textAngular.min.js
//= require textAngular-sanitize.min.js
//= require ../shared/bindonce.min.js
//= require darkswarm/i18n.js
//= require darkswarm/i18n.translate.js
//= require_tree .

View File

@@ -12,25 +12,25 @@ angular.module("ofn.admin").controller "AdminOrderMgmtCtrl", [
$scope.startDate = formatDate start
$scope.endDate = formatDate end
$scope.quickSearch = ""
$scope.bulkActions = [ { name: "Delete Selected", callback: $scope.deleteLineItems } ]
$scope.bulkActions = [ { name: t("bom_actions_delete"), callback: $scope.deleteLineItems } ]
$scope.selectedBulkAction = $scope.bulkActions[0]
$scope.selectedUnitsProduct = {};
$scope.selectedUnitsVariant = {};
$scope.sharedResource = false
$scope.columns = Columns.setColumns
order_no: { name: "Order No.", visible: false }
full_name: { name: "Name", visible: true }
email: { name: "Email", visible: false }
phone: { name: "Phone", visible: false }
order_date: { name: "Order Date", visible: true }
producer: { name: "Producer", visible: true }
order_cycle: { name: "Order Cycle", visible: false }
hub: { name: "Hub", visible: false }
variant: { name: "Variant", visible: true }
quantity: { name: "Quantity", visible: true }
max: { name: "Max", visible: true }
final_weight_volume: { name: "Weight/Volume", visible: false }
price: { name: "Price", visible: false }
order_no: { name: t("bom_no"), visible: false }
full_name: { name: t("name"), visible: true }
email: { name: t("email"), visible: false }
phone: { name: t("phone"), visible: false }
order_date: { name: t("bom_date"), visible: true }
producer: { name: t("producer"), visible: true }
order_cycle: { name: t("bom_cycle"), visible: false }
hub: { name: t("bom_hub"), visible: false }
variant: { name: t("bom_variant"), visible: true }
quantity: { name: t("bom_quantity"), visible: true }
max: { name: t("bom_max"), visible: true }
final_weight_volume: { name: t("bom_final_weigth_volume"), visible: false }
price: { name: t("price"), visible: false }
$scope.initialise = ->
$scope.initialiseVariables()
authorise_api_reponse = ""

View File

@@ -4,32 +4,32 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
$scope.StatusMessage = StatusMessage
$scope.columns = Columns.setColumns
producer: {name: "Producer", visible: true}
sku: {name: "SKU", visible: false}
name: {name: "Name", visible: true}
unit: {name: "Unit", visible: true}
price: {name: "Price", visible: true}
on_hand: {name: "On Hand", visible: true}
on_demand: {name: "On Demand", visible: false}
category: {name: "Category", visible: false}
tax_category: {name: "Tax Category", visible: false}
inherits_properties: {name: "Inherits Properties?", visible: false}
available_on: {name: "Available On", visible: false}
producer: {name: t("products_producer"), visible: true}
sku: {name: t("products_sku"), visible: false}
name: {name: t("products_name"), visible: true}
unit: {name: t("products_unit"), visible: true}
price: {name: t("products_price"), visible: true}
on_hand: {name: t("products_on_hand"), visible: true}
on_demand: {name: t("products_on_demand"), visible: false}
category: {name: t("products_category"), visible: false}
tax_category: {name: t("products_tax_category"), visible: false}
inherits_properties: {name: t("products_inherits_properties"), visible: false}
available_on: {name: t("products_available_on"), visible: false}
$scope.variant_unit_options = VariantUnitManager.variantUnitOptions()
$scope.filterableColumns = [
{ name: "Producer", db_column: "producer_name" },
{ name: "Name", db_column: "name" }
{ name: t("label_producers"), db_column: "producer_name" },
{ name: t("name"), db_column: "name" }
]
$scope.filterTypes = [
{ name: "Equals", predicate: "eq" },
{ name: "Contains", predicate: "cont" }
{ name: t("equals"), predicate: "eq" },
{ name: t("contains"), predicate: "cont" }
]
$scope.optionTabs =
filters: { title: "Filter Products", visible: false }
filters: { title: t("filter_products"), visible: false }
$scope.producers = producers
@@ -105,7 +105,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
$scope.categoryFilter = "0"
$scope.editWarn = (product, variant) ->
if (DirtyProducts.count() > 0 and confirm("Unsaved changes will be lost. Continue anyway?")) or (DirtyProducts.count() == 0)
if (DirtyProducts.count() > 0 and confirm(t("unsaved_changes_confirmation"))) or (DirtyProducts.count() == 0)
window.location = "/admin/products/" + product.permalink_live + ((if variant then "/variants/" + variant.id else "")) + "/edit"
@@ -150,14 +150,14 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
if !$scope.variantSaved(variant)
$scope.removeVariant(product, variant)
else
if confirm("Are you sure?")
if confirm(t("are_you_sure"))
$http(
method: "DELETE"
url: "/api/products/" + product.permalink_live + "/variants/" + variant.id + "/soft_delete"
).success (data) ->
$scope.removeVariant(product, variant)
else
alert("The last variant cannot be deleted!")
alert(t("delete_product_variant"))
$scope.removeVariant = (product, variant) ->
product.variants.splice product.variants.indexOf(variant), 1
@@ -194,7 +194,7 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
if productsToSubmit.length > 0
$scope.updateProducts productsToSubmit # Don't submit an empty list
else
StatusMessage.display 'alert', 'No changes to save.'
StatusMessage.display 'alert', t("products_change")
$scope.updateProducts = (productsToSubmit) ->
@@ -212,10 +212,10 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
).error (data, status) ->
if status == 400 && data.errors? && data.errors.length > 0
errors = error + "\n" for error in data.errors
alert "Saving failed with the following error(s):\n" + errors
$scope.displayFailure "Save failed due to invalid data"
alert t("products_update_error") + "\n" + errors
$scope.displayFailure t("products_update_error")
else
$scope.displayFailure "Server returned with error status: " + status
$scope.displayFailure t("products_update_error_data") + status
$scope.packProduct = (product) ->
@@ -253,23 +253,23 @@ angular.module("ofn.admin").controller "AdminProductEditCtrl", ($scope, $timeout
$scope.displayUpdating = ->
StatusMessage.display 'progress', 'Saving...'
StatusMessage.display 'progress', t("saving")
$scope.displaySuccess = ->
StatusMessage.display 'success', 'Changes saved.'
StatusMessage.display 'success',t("products_changes_saved")
$scope.displayFailure = (failMessage) ->
StatusMessage.display 'failure', "Saving failed. #{failMessage}"
StatusMessage.display 'failure', t("products_update_error_msg") + "#{failMessage}"
$scope.displayDirtyProducts = ->
if DirtyProducts.count() > 0
message = if DirtyProducts.count() == 1 then "one product" else DirtyProducts.count() + " products"
StatusMessage.display 'notice', "Changes to #{message} remain unsaved."
else
StatusMessage.clear()
count = DirtyProducts.count()
switch count
when 0 then StatusMessage.clear()
when 1 then StatusMessage.display 'notice', t("one_product_unsaved")
else StatusMessage.display 'notice', t("products_unsaved", n: count)
filterSubmitProducts = (productsToFilter) ->

View File

@@ -7,7 +7,7 @@ angular.module("ofn.admin").controller "AdminEnterpriseRelationshipsCtrl", ($sco
$scope.EnterpriseRelationships.create($scope.parent_id, $scope.child_id, $scope.permissions)
$scope.delete = (enterprise_relationship) ->
if confirm("Are you sure?")
if confirm(t("are_you_sure"))
$scope.EnterpriseRelationships.delete enterprise_relationship
$scope.toggleKeyword = (string, key) ->

View File

@@ -7,5 +7,5 @@ angular.module("ofn.admin").controller "AdminEnterpriseRolesCtrl", ($scope, Ente
$scope.EnterpriseRoles.create($scope.user_id, $scope.enterprise_id)
$scope.delete = (enterprise_role) ->
if confirm("Are you sure?")
if confirm(t('are_you_sure'))
$scope.EnterpriseRoles.delete enterprise_role

View File

@@ -1,5 +1,5 @@
angular.module("admin.customers").controller "customersCtrl", ($scope, Customers, Columns, pendingChanges, shops) ->
$scope.shop = null
angular.module("admin.customers").controller "customersCtrl", ($scope, CustomerResource, Columns, pendingChanges, shops) ->
$scope.shop = {}
$scope.shops = shops
$scope.submitAll = pendingChanges.submitAll
@@ -8,10 +8,26 @@ angular.module("admin.customers").controller "customersCtrl", ($scope, Customers
code: { name: "Code", visible: true }
tags: { name: "Tags", visible: true }
$scope.$watch "shop", ->
if $scope.shop?
Customers.loaded = false
$scope.customers = Customers.index(enterprise_id: $scope.shop.id)
$scope.$watch "shop.id", ->
if $scope.shop.id?
$scope.customers = index {enterprise_id: $scope.shop.id}
$scope.loaded = ->
Customers.loaded
$scope.add = (email) ->
params =
enterprise_id: $scope.shop.id
email: email
CustomerResource.create params, (customer) =>
if customer.id
$scope.customers.push customer
$scope.quickSearch = customer.email
$scope.deleteCustomer = (customer) ->
params = id: customer.id
CustomerResource.destroy params, ->
i = $scope.customers.indexOf customer
$scope.customers.splice i, 1 unless i < 0
index = (params) ->
$scope.loaded = false
CustomerResource.index params, =>
$scope.loaded = true

View File

@@ -1 +1 @@
angular.module("admin.customers", ['ngResource', 'ngTagsInput', 'admin.indexUtils', 'admin.dropdown'])
angular.module("admin.customers", ['ngResource', 'ngTagsInput', 'admin.indexUtils', 'admin.utils', 'admin.dropdown'])

View File

@@ -1,8 +0,0 @@
angular.module("admin.customers").directive "tagsWithTranslation", ->
restrict: "E"
template: "<tags-input ng-model='object.tags'>"
scope:
object: "="
link: (scope, element, attrs) ->
scope.$watchCollection "object.tags", ->
scope.object.tag_list = (tag.text for tag in scope.object.tags).join(",")

View File

@@ -1,8 +1,17 @@
angular.module("admin.customers").factory 'CustomerResource', ($resource) ->
$resource('/admin/customers.json', {}, {
$resource('/admin/customers/:id.json', {}, {
'index':
method: 'GET'
isArray: true
params:
enterprise_id: '@enterprise_id'
'create':
method: 'POST'
params:
enterprise_id: '@enterprise_id'
email: '@email'
'destroy':
method: 'DELETE'
params:
id: '@id'
})

View File

@@ -1,16 +0,0 @@
angular.module("admin.customers").factory 'Customers', (CustomerResource) ->
new class Customers
customers: []
customers_by_id: {}
loaded: false
index: (params={}, callback=null) ->
CustomerResource.index params, (data) =>
for customer in data
@customers.push customer
@customers_by_id[customer.id] = customer
@loaded = true
(callback || angular.noop)(@customers)
@customers

View File

@@ -5,11 +5,11 @@ angular.module("admin.enterprise_groups")
$scope.menu.setItems [
{ name: 'Primary Details', icon_class: "icon-user" }
{ name: 'Users', icon_class: "icon-user" }
{ name: 'About', icon_class: "icon-pencil" }
{ name: 'Images', icon_class: "icon-picture" }
{ name: 'Contact', icon_class: "icon-phone" }
{ name: 'Web', icon_class: "icon-globe" }
{ name: (t('users')), icon_class: "icon-user" }
{ name: (t('about')), icon_class: "icon-pencil" }
{ name: (t('images')), icon_class: "icon-picture" }
{ name: (t('contact')), icon_class: "icon-phone" }
{ name: (t('web')), icon_class: "icon-globe" }
]
$scope.select(0)

View File

@@ -6,7 +6,7 @@ angular.module("admin.enterprises")
$scope.navClear = NavigationCheck.clear
$scope.pristineEmail = $scope.Enterprise.email
$scope.menu = SideMenu
$scope.newManager = { id: '', email: 'Add a manager...' }
$scope.newManager = { id: '', email: (t('add_manager')) }
# Provide a callback for generating warning messages displayed before leaving the page. This is passed in
# from a directive "nav-check" in the page - if we pass it here it will be called in the test suite,
@@ -31,4 +31,4 @@ angular.module("admin.enterprises")
if (user for user in $scope.Enterprise.users when user.id == manager.id).length == 0
$scope.Enterprise.users.push manager
else
alert "#{manager.email} is already a manager!"
alert ("#{manager.email}" + " " + t("is_already_manager"))

View File

@@ -5,20 +5,21 @@ angular.module("admin.enterprises")
$scope.select = SideMenu.select
$scope.menu.setItems [
{ name: 'Primary Details', icon_class: "icon-home" }
{ name: 'Users', icon_class: "icon-user" }
{ name: 'Address', icon_class: "icon-map-marker" }
{ name: 'Contact', icon_class: "icon-phone" }
{ name: 'Social', icon_class: "icon-twitter" }
{ name: 'About', icon_class: "icon-pencil" }
{ name: 'Business Details', icon_class: "icon-briefcase" }
{ name: 'Images', icon_class: "icon-picture" }
{ name: "Properties", icon_class: "icon-tags", show: "showProperties()" }
{ name: "Shipping Methods", icon_class: "icon-truck", show: "showShippingMethods()" }
{ name: "Payment Methods", icon_class: "icon-money", show: "showPaymentMethods()" }
{ name: "Enterprise Fees", icon_class: "icon-tasks", show: "showEnterpriseFees()" }
{ name: "Inventory Settings", icon_class: "icon-list-ol", show: "showInventorySettings()" }
{ name: "Shop Preferences", icon_class: "icon-shopping-cart", show: "showShopPreferences()" }
{ name: t('primary_details'), icon_class: "icon-home" }
{ name: t('users'), icon_class: "icon-user" }
{ name: t('address'), icon_class: "icon-map-marker" }
{ name: t('contact'), icon_class: "icon-phone" }
{ name: t('social'), icon_class: "icon-twitter" }
{ name: t('about'), icon_class: "icon-pencil" }
{ name: t('business_details'), icon_class: "icon-briefcase" }
{ name: t('images'), icon_class: "icon-picture" }
{ name: t('properties'), icon_class: "icon-tags", show: "showProperties()" }
{ name: t('shipping_methods'), icon_class: "icon-truck", show: "showShippingMethods()" }
{ name: t('payment_methods'), icon_class: "icon-money", show: "showPaymentMethods()" }
{ name: t('enterprise_fees'), icon_class: "icon-tasks", show: "showEnterpriseFees()" }
{ name: t('inventory_settings'), icon_class: "icon-list-ol", show: "enterpriseIsShop()" }
{ name: t('tag_rules'), icon_class: "icon-random", show: "enterpriseIsShop()" }
{ name: t('shop_preferences'), icon_class: "icon-shopping-cart", show: "enterpriseIsShop()" }
]
$scope.select(0)
@@ -42,8 +43,5 @@ angular.module("admin.enterprises")
$scope.showEnterpriseFees = ->
enterprisePermissions.can_manage_enterprise_fees && ($scope.Enterprise.sells != "none" || $scope.Enterprise.is_primary_producer)
$scope.showInventorySettings = ->
$scope.Enterprise.sells != "none"
$scope.showShopPreferences = ->
$scope.enterpriseIsShop = ->
$scope.Enterprise.sells != "none"

View File

@@ -1 +1 @@
angular.module("admin.enterprises", [
angular.module("admin.enterprises", [

View File

@@ -0,0 +1,7 @@
angular.module('ofn.admin').filter "translate", ->
(key, options) ->
t(key, options)
angular.module('ofn.admin').filter "t", ->
(key, options) ->
t(key, options)

View File

@@ -15,8 +15,6 @@ angular.module("admin.indexUtils").directive "ofnSelect2", ($sanitize, $timeout)
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) ->

View File

@@ -5,27 +5,27 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
$scope.confirmDelete = true
$scope.startDate = formatDate daysFromToday -7
$scope.endDate = formatDate daysFromToday 1
$scope.bulkActions = [ { name: "Delete Selected", callback: 'deleteLineItems' } ]
$scope.selectedUnitsProduct = {};
$scope.selectedUnitsVariant = {};
$scope.bulkActions = [ { name: t("bom_actions_delete"), callback: 'deleteLineItems' } ]
$scope.selectedUnitsProduct = {}
$scope.selectedUnitsVariant = {}
$scope.sharedResource = false
$scope.columns = Columns.setColumns
order_no: { name: "Order No.", visible: false }
full_name: { name: "Name", visible: true }
email: { name: "Email", visible: false }
phone: { name: "Phone", visible: false }
order_date: { name: "Order Date", visible: true }
producer: { name: "Producer", visible: true }
order_cycle: { name: "Order Cycle", visible: false }
hub: { name: "Hub", visible: false }
variant: { name: "Variant", visible: true }
quantity: { name: "Quantity", visible: true }
max: { name: "Max", visible: true }
final_weight_volume: { name: "Weight/Volume", visible: false }
price: { name: "Price", visible: false }
order_no: { name: t("bom_no"), visible: false }
full_name: { name: t("name"), visible: true }
email: { name: t("email"), visible: false }
phone: { name: t("phone"), visible: false }
order_date: { name: t("bom_date"), visible: true }
producer: { name: t("producer"), visible: true }
order_cycle: { name: t("bom_cycle"), visible: false }
hub: { name: t("bom_hub"), visible: false }
variant: { name: t("bom_variant"), visible: true }
quantity: { name: t("bom_quantity"), visible: true }
max: { name: t("bom_max"), visible: true }
final_weight_volume: { name: t("bom_final_weigth_volume"), visible: false }
price: { name: t("price"), visible: false }
$scope.confirmRefresh = ->
LineItems.allSaved() || confirm("Unsaved changes exist and will be lost if you continue.")
LineItems.allSaved() || confirm(t "unsaved_changes_warning")
$scope.resetSelectFilters = ->
$scope.distributorFilter = blankOption().id
@@ -73,12 +73,12 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
StatusMessage.display 'success', "All changes saved"
$scope.bulk_order_form.$setPristine()
).catch ->
StatusMessage.display 'failure', "Fields with red borders contain errors."
StatusMessage.display 'failure', t "unsaved_changes_error"
else
StatusMessage.display 'failure', "Fields with red borders contain errors."
StatusMessage.display 'failure', t "unsaved_changes_error"
$scope.deleteLineItem = (lineItem) ->
if ($scope.confirmDelete && confirm("Are you sure?")) || !$scope.confirmDelete
if ($scope.confirmDelete && confirm(t "are_you_sure")) || !$scope.confirmDelete
LineItems.delete lineItem, =>
$scope.lineItems.splice $scope.lineItems.indexOf(lineItem), 1

View File

@@ -1,4 +1,4 @@
angular.module("admin.payment_methods")
angular.module("admin.paymentMethods")
.controller "paymentMethodCtrl", ($scope, PaymentMethods) ->
$scope.findPaymentMethodByID = (id) ->
$scope.PaymentMethod = PaymentMethods.findByID(id)
$scope.PaymentMethod = PaymentMethods.findByID(id)

View File

@@ -1 +1 @@
angular.module("admin.payment_methods", [])
angular.module("admin.paymentMethods", [])

View File

@@ -1,4 +1,4 @@
angular.module("admin.payment_methods")
angular.module("admin.paymentMethods")
.factory "PaymentMethods", (paymentMethods) ->
new class PaymentMethods
paymentMethods: paymentMethods

View File

@@ -1,4 +1,2 @@
angular.module("admin.shipping_methods")
.controller "shippingMethodCtrl", ($scope, ShippingMethods) ->
$scope.findShippingMethodByID = (id) ->
$scope.ShippingMethod = ShippingMethods.findByID(id)
angular.module("admin.shippingMethods").controller "shippingMethodCtrl", ($scope, shippingMethod) ->
$scope.shippingMethod = shippingMethod

View File

@@ -0,0 +1,4 @@
angular.module("admin.shippingMethods")
.controller "shippingMethodsCtrl", ($scope, ShippingMethods) ->
$scope.findShippingMethodByID = (id) ->
$scope.ShippingMethod = ShippingMethods.findByID(id)

View File

@@ -1,4 +1,4 @@
angular.module("admin.shipping_methods")
angular.module("admin.shippingMethods")
.factory "ShippingMethods", (shippingMethods) ->
new class ShippingMethods
shippingMethods: shippingMethods

View File

@@ -1 +1 @@
angular.module("admin.shipping_methods", [])
angular.module("admin.shippingMethods", ["ngTagsInput", 'admin.utils'])

View File

@@ -0,0 +1,48 @@
angular.module("admin.tagRules").controller "TagRulesCtrl", ($scope, $http, enterprise) ->
$scope.tagGroups = enterprise.tag_groups
$scope.visibilityOptions = [ { id: "visible", name: "VISIBLE" }, { id: "hidden", name: "NOT VISIBLE" } ]
updateRuleCounts = ->
index = 0
for tagGroup in $scope.tagGroups
tagGroup.startIndex = index
index = index + tagGroup.rules.length
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
preferred_customer_tags: (tag.text for tag in tagGroup.tags).join(",")
type: "TagRule::#{ruleType}"
switch ruleType
when "DiscountOrder"
newRule.calculator = { preferred_flat_percent: 0 }
when "FilterShippingMethods"
newRule.peferred_shipping_method_tags = []
newRule.preferred_matched_shipping_methods_visibility = "visible"
tagGroup.rules.push(newRule)
updateRuleCounts()
$scope.addNewTag = ->
$scope.tagGroups.push { tags: [], rules: [] }
$scope.deleteTagRule = (tagGroup, tagRule) ->
index = tagGroup.rules.indexOf(tagRule)
return unless index >= 0
if tagRule.id is null
tagGroup.rules.splice(index, 1)
updateRuleCounts()
else
if confirm("Are you sure?")
$http
method: "DELETE"
url: "/admin/enterprises/#{enterprise.id}/tag_rules/#{tagRule.id}.json"
.success ->
tagGroup.rules.splice(index, 1)
updateRuleCounts()

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,34 @@
angular.module("admin.tagRules").directive 'newTagRuleDialog', ($compile, $templateCache, $window) ->
restrict: 'A'
scope: true
link: (scope, element, attr) ->
# Compile modal template
template = $compile($templateCache.get('admin/new_tag_rule_dialog.html'))(scope)
scope.ruleTypes = [
# { id: "DiscountOrder", name: 'Apply a discount to orders' }
{ id: "FilterShippingMethods", name: 'Show/Hide shipping methods' }
]
scope.ruleType = "DiscountOrder"
# Set Dialog options
template.dialog
show: { effect: "fade", duration: 400 }
hide: { effect: "fade", duration: 300 }
autoOpen: false
resizable: false
width: $window.innerWidth * 0.4;
modal: true
open: (event, ui) ->
$('.ui-widget-overlay').bind 'click', ->
$(this).siblings('.ui-dialog').find('.ui-dialog-content').dialog('close')
# Link opening of dialog to click event on element
element.bind 'click', (e) ->
template.dialog('open')
scope.addRule = (tagGroup, ruleType) ->
scope.addNewRuleTo(tagGroup, ruleType)
template.dialog('close')
return

View File

@@ -0,0 +1,4 @@
angular.module("admin.tagRules").directive "discountOrder", ->
restrict: "E"
replace: true
templateUrl: "admin/tag_rules/discount_order.html"

View File

@@ -0,0 +1,4 @@
angular.module("admin.tagRules").directive "filterShippingMethods", ->
restrict: "E"
replace: true
templateUrl: "admin/tag_rules/filter_shipping_methods.html"

View File

@@ -0,0 +1 @@
angular.module("admin.tagRules", ['ngTagsInput'])

View File

@@ -0,0 +1,15 @@
angular.module("admin.utils").directive "tagsWithTranslation", ($timeout) ->
restrict: "E"
template: "<tags-input ng-model='object[tagsAttr]'>"
scope:
object: "="
tagsAttr: "@?"
tagListAttr: "@?"
link: (scope, element, attrs) ->
$timeout ->
scope.tagsAttr ||= "tags"
scope.tagListAttr ||= "tag_list"
watchString = "object.#{scope.tagsAttr}"
scope.$watchCollection watchString, ->
scope.object[scope.tagListAttr] = (tag.text for tag in scope.object[scope.tagsAttr]).join(",")

View File

@@ -0,0 +1,9 @@
Darkswarm.controller "DistributorNodeCtrl", ($scope, HashNavigation, $anchorScroll) ->
$scope.toggle = ->
HashNavigation.toggle $scope.distributor.hash
$scope.open = ->
HashNavigation.active($scope.distributor.hash)
if $scope.open()
$anchorScroll()

View File

@@ -8,6 +8,7 @@ Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, Enterpris
$scope.show_profiles = false
$scope.filtersActive = false
$scope.distanceMatchesShown = false
$scope.filterExpression = {active: true}
$scope.$watch "query", (query)->
@@ -44,8 +45,8 @@ Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, Enterpris
$scope.filterEnterprises = ->
es = Enterprises.hubs
$scope.nameMatches = enterpriseMatchesNameQueryFilter(es, true)
$scope.distanceMatches = enterpriseMatchesNameQueryFilter(es, false)
$scope.distanceMatches = distanceWithinKmFilter($scope.distanceMatches, 50)
noNameMatches = enterpriseMatchesNameQueryFilter(es, false)
$scope.distanceMatches = distanceWithinKmFilter(noNameMatches, 50)
$scope.updateVisibleMatches = ->
@@ -65,3 +66,9 @@ Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, Enterpris
$scope.nameMatchesFiltered[0]
else
undefined
$scope.showClosedShops = ->
delete $scope.filterExpression['active']
$scope.hideClosedShops = ->
$scope.filterExpression['active'] = true

View File

@@ -0,0 +1,2 @@
Darkswarm.controller "OrdersCtrl", ($scope, $rootScope, $timeout, Orders, Search, $document, HashNavigation, FilterSelectorsService, EnterpriseModal, enterpriseMatchesNameQueryFilter, distanceWithinKmFilter) ->
$scope.Orders = Orders

View File

@@ -0,0 +1,5 @@
Darkswarm.directive 'auth', (AuthenticationService) ->
restrict: 'A'
link: (scope, elem, attrs) ->
elem.bind "click", ->
AuthenticationService.open '/' + attrs.auth

View File

@@ -0,0 +1,6 @@
Darkswarm.directive "logoFallback", () ->
restrict: "A"
link: (scope, elm, attr)->
elm.bind('error', ->
elm.replaceWith("<i class='ofn-i_059-producer'></i>")
)

View File

@@ -0,0 +1,7 @@
Darkswarm.filter "formatBalance", (localizeCurrencyFilter, tFilter)->
# Convert number to string currency using injected currency configuration.
(balance) ->
if balance < 0
tFilter('credit') + ": " + localizeCurrencyFilter(Math.abs(balance))
else
tFilter('balance_due') + ": " + localizeCurrencyFilter(Math.abs(balance))

View File

@@ -4,8 +4,13 @@ window.translate = (key, options = {}) ->
unless 'I18n' of window
console.log 'The I18n object is undefined. Cannot translate text.'
return key
return key unless key of I18n
text = I18n[key]
dict = I18n
parts = key.split '.'
while (parts.length)
part = parts.shift()
return key unless part of dict
dict = dict[part]
text = dict
for name, value of options
text = text.split("%{#{name}}").join(value)
text

View File

@@ -2,6 +2,8 @@ Darkswarm.factory "OfnMap", (Enterprises, EnterpriseModal, visibleFilter) ->
new class OfnMap
constructor: ->
@enterprises = @enterprise_markers(Enterprises.enterprises)
@enterprises = @enterprises.filter (enterprise) ->
enterprise.latitude != null || enterprise.longitude != null # Remove enterprises w/o lat or long
enterprise_markers: (enterprises) ->
@extend(enterprise) for enterprise in visibleFilter(enterprises)

View File

@@ -0,0 +1,16 @@
Darkswarm.factory 'Orders', (orders_by_distributor, currencyConfig, CurrentHub, Taxons, Dereferencer, visibleFilter, Matcher, Geo, $rootScope)->
new class Orders
constructor: ->
# Populate Orders.orders from json in page.
@orders_by_distributor = orders_by_distributor
@currency_symbol = currencyConfig.symbol
for distributor in @orders_by_distributor
@updateRunningBalance(distributor.distributed_orders)
updateRunningBalance: (orders) ->
for order, i in orders
balances = orders.slice(i,orders.length).map (o) -> parseFloat(o.outstanding_balance)
running_balance = balances.reduce (a,b) -> a+b
order.running_balance = running_balance.toFixed(2)

View File

@@ -0,0 +1,10 @@
#new-tag-rule-dialog
.text-normal.margin-bottom-30.text-center
Select a rule type:
.text-center.margin-bottom-30
-# %select.fullwidth{ 'select2-min-search' => 5, 'ng-model' => 'newRuleType', 'ng-options' => 'ruleType.id as ruleType.name for ruleType in availableRuleTypes' }
%input.ofn-select2.fullwidth{ :id => 'rule_type_selector', ng: { model: "ruleType" }, data: "ruleTypes", 'min-search' => "5" }
.text-center
%input.button.red.icon-plus{ type: 'button', value: "Add Rule", ng: { click: 'addRule(tagGroup, ruleType)' } }

View File

@@ -0,0 +1,37 @@
%div
%input{ type: "hidden",
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_id",
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][id]",
ng: { value: "rule.id" } }
%input{ type: "hidden",
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_type",
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]",
value: "TagRule::DiscountOrder" }
%input{ type: "hidden",
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_customer_tags",
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_customer_tags]",
ng: { value: "rule.preferred_customer_tags" } }
%input{ type: "hidden",
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_calculator_type",
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][calculator_type]",
value: "Spree::Calculator::FlatPercentItemTotal" }
%input{ type: "hidden",
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_calculator_attributes_id",
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][calculator_attributes][id]",
ng: { value: "rule.calculator.id" } }
%input{ type: "hidden",
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][calculator_attributes][preferred_flat_percent]",
ng: { value: "rule.calculator.preferred_flat_percent" } }
%span.text-normal {{ $index + 1 }}. Orders are discounted by
%input{ type: "number",
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_calculator_attributes_preferred_flat_percent",
min: -100,
max: 100,
ng: { model: "rule.calculator.preferred_flat_percent" }, 'invert-number' => true }
%span.text-normal %

View File

@@ -0,0 +1,27 @@
%div
%input{ type: "hidden",
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_id",
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][id]",
ng: { value: "rule.id" } }
%input{ type: "hidden",
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_type",
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][type]",
value: "TagRule::FilterShippingMethods" }
%input{ type: "hidden",
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_customer_tags",
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_customer_tags]",
ng: { value: "rule.preferred_customer_tags" } }
%input{ type: "hidden",
id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_shipping_method_tags",
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_shipping_method_tags]",
ng: { value: "rule.preferred_customer_tags" } }
%span.text-normal {{ $index + 1 }}. Shipping methods with matching tags are
%input.light.ofn-select2{ id: "enterprise_tag_rules_attributes_{{tagGroup.startIndex + $index}}_preferred_matched_shipping_methods_visibility",
name: "enterprise[tag_rules_attributes][{{tagGroup.startIndex + $index}}][preferred_matched_shipping_methods_visibility]",
ng: { model: "rule.preferred_matched_shipping_methods_visibility"},
data: 'visibilityOptions', "min-search" => 5 }
-# %tags-with-translation{ object: "rule", "tags-attr" => "shipping_method_tags", "tag-list-attr" => "preferred_shipping_method_tags" }

View File

@@ -0,0 +1,88 @@
/**
Main colors:
dark: #545454
light: #ccc
*/
.ui-dialog {
border: 2px solid #4a4a4a;
border-radius:3px;
padding:0px;
-moz-box-shadow: 3px 3px 4px #797979;
-webkit-box-shadow: 3px 3px 4px #797979;
box-shadow: 3px 3px 4px #797979;
/* For IE 8 */
-ms-filter: "progid:DXImageTransform.Microsoft.Shadow(Strength=4, Direction=135, Color='#545454')";
/* For IE 5.5 - 7 */
filter: progid:DXImageTransform.Microsoft.Shadow(Strength=4, Direction=135, Color='#545454');
}
.ui-dialog .ui-dialog-titlebar{
border-radius: 3px;
}
.ui-dialog .ui-state-hover {
&.ui-dialog-titlebar-close{
}
}
/*.ui-dialog .ui-icon-closethick{background:url(/static/assets/dialogCloseButton.png);}*/
.ui-dialog .ui-widget-header{
background-image: none;
background-color: #ffffff;
border:0px;
border-radius: 3px;
padding: 0px 5px 0px 5px;
}
.ui-dialog .ui-widget-content{
border: none;
border-radius: 3px;
padding: 0px 50px 30px 50px;
}
.ui-dialog .ui-corner-all{
border-radius:0px;
}
.ui-dialog {
.ui-state-hover, .ui-state-focus{
border: none;
background: none;
color: #545454;
}
}
.ui-state-hover, .ui-widget-header .ui-state-hover, .ui-widget-content .ui-state-hover {
background-color: #ffffff;
background: none;
}
.ui-dialog-titlebar-close {
float: right;
&:before {
color: #000000;
font-size: 2em;
font-weight: 400;
content: '\00d7';
display: inline;
}
&:hover {
&:before {
color: #da5354;
}
}
.ui-icon {
&.ui-icon-closethick {
display: none;
}
}
}
.ui-widget-overlay {
background: #e9e9e9;
opacity: 0.6;
}

View File

@@ -2,6 +2,10 @@
margin-bottom: 20px;
}
.margin-bottom-30 {
margin-bottom: 30px;
}
.margin-bottom-50 {
margin-bottom: 50px;
}

View File

@@ -69,4 +69,23 @@ div#group_buy_calculation {
text-indent:1em;
}
}
&.after {
span {
position: absolute;
transform: translate(0,-55%);
top:50%;
right: 0.5em;
pointer-events:none;
}
input {
padding-right: 1.2em;
}
}
}
th.actions {
white-space: nowrap;
}

View File

@@ -1,5 +1,8 @@
.select2-container {
.select2-choice {
.select2-search-choice-close {
display: none;
}
.select2-arrow {
width: 22px;
border: none;
@@ -7,4 +10,36 @@
background-color: transparent;
}
}
&.light {
.select2-choice{
background-color: #ffffff;
font-weight: normal;
border: 1px solid #5498da !important;
color: #5498da !important;
.select2-arrow {
&:before {
color: #5498da;
font-size: 1rem;
font-weight: 400;
content: '\25be';
display: inline;
}
}
}
&:hover, &.select2-container-active {
.select2-choice{
color: #ffffff !important;
background-color: #5498da !important;
.select2-arrow {
&:before {
color: #ffffff;
}
}
}
}
}
}

View File

@@ -0,0 +1,68 @@
.no_tags {
margin-bottom: 40px;
color: #aeaeae;
font-size: 1rem;
font-weight: bold;
}
.customer_tag {
border: 1px solid #cee1f4;
margin-bottom: 40px;
.header {
padding: 8px 10px;
background-color: #eff5fc;
border-bottom: 1px solid #cee1f4;
table {
padding: 0px;
margin: 0px 0px 0px 0px;
tr {
td {
border: none;
}
}
}
}
.no_rules {
padding: 8px 10px;
margin-bottom: 10px;
color: #aeaeae;
font-size: 1rem;
font-weight: bold;
}
table {
padding: 0px;
margin: 0px 0px 10px 0px;
tr.tag_rule {
border: none;
padding: 0px;
margin: 0px;
td {
border: none;
padding: 4px 10px 10px 10px;
margin: 0px;
input {
width: auto;
}
}
}
}
.add_rule {
padding: 8px 10px;
margin-bottom: 10px;
}
}
#new-tag-rule-dialog{
.select2-chosen, .select2-result-label{
font-size: 1rem;
font-weight: lighter;
}
}

View File

@@ -7,3 +7,12 @@
font-size: 1.2rem;
font-weight: 300;
}
.text-red {
color: #DA5354;
}
input.text-big {
font-size: 1.1rem;
}

View File

@@ -0,0 +1,63 @@
@import branding
@import mixins
.orders
@include sidepaddingSm
@include panepadding
padding-top: 10px
h3
padding-top: 2em
a
color: $clr-brick
&:hover, &:active, &:focus
color: $clr-brick-med-bright
img
display: block
width: 80px
height: auto
i.ofn-i_059-producer, i.ofn-i_060-producer-reversed
font-size: 3rem
display: inline-block
margin-right: 0.25rem
float: left
color: $clr-turquoise
.credit
color: green
.debit
color: $clr-brick
.distributor-balance.paid
visibility: hidden
.transaction-group
table
border-radius: 0.5em 0.5em 0 0
tr:nth-of-type(even)
background: transparent // clear previous
tbody.odd
tr
background-color: #f9f9f9
border: none
// Column widths for order table
.order1
width: 20%
.order2
width: 20%
.order3
width: 20%
.order4
width: 10%
.order5
width: 10%
.order6
width: 10%
.order7
width: 10%

View File

@@ -30,7 +30,7 @@ footer
background-color: transparent
border: none
padding: 0
a.big-alert
a.alert-cta
@include csstrans
width: 100%
border: 1px solid rgba($dark-grey, 0.35)

View File

@@ -7,4 +7,7 @@
@include sidepaddingSm
.name-matches, .distance-matches
margin-top: 4em
margin-top: 4em
.more-controls
text-align: center

View File

@@ -15,4 +15,4 @@
color: $clr-turquoise-bright
a.button.primary
&:hover, &:active, &:focus
color: white
color: white

View File

@@ -84,9 +84,11 @@
padding-right: 0rem
font-size: 0.8rem
.shopfront_message, .shopfront_closed_message
.shopfront_message, .shopfront_closed_message, .shopfront_hidden_message
padding: 15px
border-radius: 5px
.shopfront_message, .shopfront_closed_message
border: 2px solid #eb4c46
.shopfront_message
@@ -94,3 +96,7 @@
.shopfront_closed_message
margin: 2em 0em
.shopfront_hidden_message
border: 2px solid #db4
margin: 2em 0em

View File

@@ -0,0 +1,13 @@
require 'open_food_network/products_cache_integrity_checker'
class Admin::CacheSettingsController < Spree::Admin::BaseController
def show
@results = Exchange.cachable.map do |exchange|
checker = OpenFoodNetwork::ProductsCacheIntegrityChecker.new(exchange.receiver, exchange.order_cycle)
{distributor: exchange.receiver, order_cycle: exchange.order_cycle, status: checker.ok?, diff: checker.diff}
end
end
end

View File

@@ -7,13 +7,25 @@ module Admin
respond_to do |format|
format.html
format.json do
render json: ActiveModel::ArraySerializer.new( @collection,
each_serializer: Api::Admin::CustomerSerializer, spree_current_user: spree_current_user
).to_json
serialised = ActiveModel::ArraySerializer.new(
@collection,
each_serializer: Api::Admin::CustomerSerializer,
spree_current_user: spree_current_user)
render json: serialised.to_json
end
end
end
def create
@customer = Customer.new(params[:customer])
if user_can_create_customer?
@customer.save
render json: Api::Admin::CustomerSerializer.new(@customer).to_json
else
redirect_to '/unauthorized'
end
end
private
def collection
@@ -25,5 +37,10 @@ module Admin
def load_managed_shops
@shops = Enterprise.managed_by(spree_current_user).is_distributor
end
def user_can_create_customer?
spree_current_user.admin? ||
spree_current_user.enterprises.include?(@customer.enterprise)
end
end
end

View File

@@ -4,7 +4,7 @@ module Admin
class EnterprisesController < ResourceController
before_filter :load_enterprise_set, :only => :index
before_filter :load_countries, :except => [:index, :register, :check_permalink]
before_filter :load_methods_and_fees, :only => [:new, :edit, :update, :create]
before_filter :load_methods_and_fees, :only => [:edit, :update]
before_filter :load_groups, :only => [:new, :edit, :update, :create]
before_filter :load_taxons, :only => [:new, :edit, :update, :create]
before_filter :check_can_change_sells, only: :update
@@ -35,6 +35,8 @@ module Admin
def update
invoke_callbacks(:update, :before)
tag_rules_attributes = params[object_name].delete :tag_rules_attributes
update_tag_rules(tag_rules_attributes) if tag_rules_attributes.present?
if @object.update_attributes(params[object_name])
invoke_callbacks(:update, :after)
flash[:success] = flash_message_for(@object, :successfully_updated)
@@ -180,6 +182,27 @@ module Admin
@taxons = Spree::Taxon.order(:name)
end
def update_tag_rules(tag_rules_attributes)
# Due to the combination of trying to use nested attributes and type inheritance
# we cannot apply all attributes to tag rules in one hit because mass assignment
# methods that are specific to each class do not become available until after the
# record is persisted. This problem is compounded by the use of calculators.
@object.transaction do
tag_rules_attributes.select{ |i, attrs| attrs[:type].present? }.each do |i, attrs|
rule = @object.tag_rules.find_by_id(attrs.delete :id) || attrs[:type].constantize.new(enterprise: @object)
create_calculator_for(rule, attrs) if rule.type == "TagRule::DiscountOrder" && rule.calculator.nil?
rule.update_attributes(attrs)
end
end
end
def create_calculator_for(rule, attrs)
if attrs[:calculator_type].present? && attrs[:calculator_attributes].present?
rule.update_attributes(calculator_type: attrs[:calculator_type])
attrs[:calculator_attributes].merge!( { id: rule.calculator.id } )
end
end
def check_can_change_bulk_sells
unless spree_current_user.admin?
params[:enterprise_set][:collection_attributes].each do |i, enterprise_params|

View File

@@ -0,0 +1,10 @@
module Admin
class TagRulesController < ResourceController
respond_to :json
respond_override destroy: { json: {
success: lambda { render nothing: true, :status => 204 }
} }
end
end

View File

@@ -0,0 +1,17 @@
module Api
class StatusesController < BaseController
respond_to :json
def job_queue
render json: {alive: job_queue_alive?}
end
private
def job_queue_alive?
Spree::Config.last_job_queue_heartbeat_at.present? &&
Time.parse(Spree::Config.last_job_queue_heartbeat_at) > 6.minutes.ago
end
end
end

View File

@@ -1,4 +1,4 @@
require 'open_food_network/products_renderer'
require 'open_food_network/cached_products_renderer'
class ShopController < BaseController
layout "darkswarm"
@@ -11,11 +11,11 @@ class ShopController < BaseController
def products
begin
products_json = OpenFoodNetwork::ProductsRenderer.new(current_distributor, current_order_cycle).products
products_json = OpenFoodNetwork::CachedProductsRenderer.new(current_distributor, current_order_cycle).products_json
render json: products_json
rescue OpenFoodNetwork::ProductsRenderer::NoProducts
rescue OpenFoodNetwork::CachedProductsRenderer::NoProducts
render status: 404, json: ''
end
end

View File

@@ -20,11 +20,15 @@ module Admin
end
def admin_inject_payment_methods
admin_inject_json_ams_array "admin.payment_methods", "paymentMethods", @payment_methods, Api::Admin::IdNameSerializer
admin_inject_json_ams_array "admin.paymentMethods", "paymentMethods", @payment_methods, Api::Admin::IdNameSerializer
end
def admin_inject_shipping_methods
admin_inject_json_ams_array "admin.shipping_methods", "shippingMethods", @shipping_methods, Api::Admin::IdNameSerializer
admin_inject_json_ams_array "admin.shippingMethods", "shippingMethods", @shipping_methods, Api::Admin::IdNameSerializer
end
def admin_inject_shipping_method
admin_inject_json_ams "admin.shippingMethods", "shippingMethod", @shipping_method, Api::Admin::ShippingMethodSerializer
end
def admin_inject_shops(ngModule='admin.customers')

View File

@@ -4,7 +4,11 @@ module EnterprisesHelper
end
def available_shipping_methods
current_distributor.shipping_methods.uniq
shipping_methods = current_distributor.shipping_methods
if current_distributor.present?
current_distributor.apply_tag_rules_to(shipping_methods, customer: current_order.customer)
end
shipping_methods.uniq
end
def managed_enterprises

View File

@@ -2,7 +2,7 @@ require 'open_food_network/enterprise_injection_data'
module InjectionHelper
def inject_enterprises
inject_json_ams "enterprises", Enterprise.activated.includes(:address).all, Api::EnterpriseSerializer, enterprise_injection_data
inject_json_ams "enterprises", Enterprise.activated.includes(address: :state).all, Api::EnterpriseSerializer, enterprise_injection_data
end
def inject_group_enterprises
@@ -51,6 +51,11 @@ module InjectionHelper
render partial: "json/injection_ams", locals: {name: 'enterpriseAttributes', json: "#{@enterprise_attributes.to_json}"}
end
def inject_orders_by_distributor
data_array = spree_current_user.orders_by_distributor
inject_json_ams "orders_by_distributor", data_array, Api::OrdersByDistributorSerializer
end
def inject_json(name, partial, opts = {})
render partial: "json/injection", locals: {name: name, partial: partial}.merge(opts)
end

View File

@@ -7,4 +7,16 @@ module ShopHelper
]
end
end
def require_customer?
current_distributor.require_login? && !user_is_related_to_distributor?
end
def user_is_related_to_distributor?
spree_current_user.present? && (
spree_current_user.admin? ||
spree_current_user.enterprises.include?(current_distributor) ||
spree_current_user.customer_of(current_distributor)
)
end
end

View File

@@ -11,11 +11,17 @@ module Spree
end
def report_payment_method_options(orders)
orders.map { |o| o.payments.first.payment_method.andand.name }.uniq
orders.map do |o|
pm = o.payments.first.payment_method
[pm.andand.name, pm.andand.id]
end.uniq
end
def report_shipping_method_options(orders)
orders.map { |o| o.shipping_method.andand.name }.uniq
orders.map do |o|
sm = o.shipping_method
[sm.andand.name, sm.andand.id]
end.uniq
end
def xero_report_types

View File

@@ -0,0 +1,5 @@
class HeartbeatJob
def perform
Spree::Config.last_job_queue_heartbeat_at = Time.now
end
end

View File

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

View File

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

View File

@@ -7,14 +7,14 @@ class ContentConfiguration < Spree::Preferences::FileConfiguration
preference :logo, :file
preference :logo_mobile, :file
preference :logo_mobile_svg, :file
has_attached_file :logo
has_attached_file :logo, default_url: "/assets/ofn-logo.png"
has_attached_file :logo_mobile
has_attached_file :logo_mobile_svg
has_attached_file :logo_mobile_svg, default_url: "/assets/ofn-logo-mobile.svg"
# Home page
preference :home_hero, :file
preference :home_show_stats, :boolean, default: true
has_attached_file :home_hero
has_attached_file :home_hero, default_url: "/assets/home/home.jpg"
# Producer sign-up page
preference :producer_signup_pricing_table_html, :text, default: "(TODO: Pricing table)"
@@ -33,7 +33,7 @@ class ContentConfiguration < Spree::Preferences::FileConfiguration
# Footer
preference :footer_logo, :file
has_attached_file :footer_logo
has_attached_file :footer_logo, default_url: "/assets/ofn-logo-footer.png"
preference :footer_facebook_url, :string, default: "https://www.facebook.com/OpenFoodNet"
preference :footer_twitter_url, :string, default: "https://twitter.com/OpenFoodNet"
preference :footer_instagram_url, :string, default: ""

View File

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

View File

@@ -2,10 +2,12 @@ class Customer < ActiveRecord::Base
acts_as_taggable
belongs_to :enterprise
belongs_to :user, :class_name => Spree.user_class
belongs_to :user, class_name: Spree.user_class
before_validation :downcase_email
validates :code, uniqueness: { scope: :enterprise_id, allow_blank: true, allow_nil: true }
validates :email, presence: true, uniqueness: { scope: :enterprise_id, message: "is associated with an existing customer" }
validates :email, presence: true, uniqueness: { scope: :enterprise_id, message: I18n.t('validation_msg_is_associated_with_an_exising_customer') }
validates :enterprise_id, presence: true
scope :of, ->(enterprise) { where(enterprise_id: enterprise) }
@@ -14,6 +16,10 @@ class Customer < ActiveRecord::Base
private
def downcase_email
email.andand.downcase!
end
def associate_user
self.user = user || Spree::User.find_by_email(email)
end

View File

@@ -41,11 +41,13 @@ class Enterprise < ActiveRecord::Base
has_many :customers
has_many :billable_periods
has_many :inventory_items
has_many :tag_rules
delegate :latitude, :longitude, :city, :state_name, :to => :address
accepts_nested_attributes_for :address
accepts_nested_attributes_for :producer_properties, allow_destroy: true, reject_if: lambda { |pp| pp[:property_name].blank? }
accepts_nested_attributes_for :tag_rules, allow_destroy: true, reject_if: lambda { |tag_rule| tag_rule[:preferred_customer_tags].blank? }
has_attached_file :logo,
styles: { medium: "300x300>", small: "180x180>", thumb: "100x100>" },
@@ -175,17 +177,6 @@ class Enterprise < ActiveRecord::Base
end
}
def self.find_near(suburb)
enterprises = []
unless suburb.nil?
addresses = Spree::Address.near([suburb.latitude, suburb.longitude], ENTERPRISE_SEARCH_RADIUS, :units => :km).joins(:enterprise).limit(10)
enterprises = addresses.collect(&:enterprise)
end
enterprises
end
# Force a distinct count to work around relation count issue https://github.com/rails/rails/issues/5554
def self.distinct_count
count(distinct: true)
@@ -353,6 +344,13 @@ class Enterprise < ActiveRecord::Base
abn.present?
end
def apply_tag_rules_to(subject, context)
tag_rules.each do |rule|
rule.set_context(subject,context)
rule.apply
end
end
protected
def devise_mailer

View File

@@ -1,11 +1,18 @@
class EnterpriseFee < ActiveRecord::Base
belongs_to :enterprise
belongs_to :tax_category, class_name: 'Spree::TaxCategory', foreign_key: 'tax_category_id'
has_and_belongs_to_many :order_cycles, join_table: 'coordinator_fees'
has_many :coordinator_fees, dependent: :destroy
has_many :order_cycles, through: :coordinator_fees
has_many :exchange_fees, dependent: :destroy
has_many :exchanges, through: :exchange_fees
before_destroy { order_cycles.clear }
after_save :refresh_products_cache
# After destroy, the products cache is refreshed via the after_destroy hook for
# coordinator_fees and exchange_fees
calculated_adjustments
@@ -59,6 +66,7 @@ class EnterpriseFee < ActiveRecord::Base
:locked => true}, :without_protection => true)
end
private
def ensure_valid_tax_category_settings
@@ -72,4 +80,8 @@ class EnterpriseFee < ActiveRecord::Base
end
return true
end
def refresh_products_cache
OpenFoodNetwork::ProductsCache.enterprise_fee_changed self
end
end

View File

@@ -4,7 +4,7 @@ class EnterpriseRelationship < ActiveRecord::Base
has_many :permissions, class_name: 'EnterpriseRelationshipPermission', dependent: :destroy
validates_presence_of :parent_id, :child_id
validates_uniqueness_of :child_id, scope: :parent_id, message: "^That relationship is already established."
validates_uniqueness_of :child_id, scope: :parent_id, message: I18n.t('validation_msg_relationship_already_established')
after_save :apply_variant_override_permissions

View File

@@ -13,6 +13,9 @@ class Exchange < ActiveRecord::Base
validates_presence_of :order_cycle, :sender, :receiver
validates_uniqueness_of :sender_id, :scope => [:order_cycle_id, :receiver_id, :incoming]
after_save :refresh_products_cache
after_destroy :refresh_products_cache_from_destroy
accepts_nested_attributes_for :variants
scope :in_order_cycle, lambda { |order_cycle| where(order_cycle_id: order_cycle) }
@@ -31,6 +34,12 @@ class Exchange < ActiveRecord::Base
joins('INNER JOIN enterprises AS receiver ON (receiver.id = exchanges.receiver_id)').
order("CASE WHEN exchanges.incoming='t' THEN sender.name ELSE receiver.name END")
# Exchanges on order cycles that are dated and are upcoming or open are cached
scope :cachable, outgoing.
joins(:order_cycle).
merge(OrderCycle.dated).
merge(OrderCycle.not_closed)
scope :managed_by, lambda { |user|
if user.has_spree_role?('admin')
scoped
@@ -75,4 +84,11 @@ class Exchange < ActiveRecord::Base
end
end
def refresh_products_cache
OpenFoodNetwork::ProductsCache.exchange_changed self
end
def refresh_products_cache_from_destroy
OpenFoodNetwork::ProductsCache.exchange_destroyed self
end
end

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
class OrderCycle < ActiveRecord::Base
belongs_to :coordinator, :class_name => 'Enterprise'
has_and_belongs_to_many :coordinator_fees, :class_name => 'EnterpriseFee', :join_table => 'coordinator_fees'
has_many :coordinator_fee_refs, class_name: 'CoordinatorFee'
has_many :coordinator_fees, through: :coordinator_fee_refs, source: :enterprise_fee
has_many :exchanges, :dependent => :destroy
@@ -11,14 +13,18 @@ class OrderCycle < ActiveRecord::Base
validates_presence_of :name, :coordinator_id
after_save :refresh_products_cache
preference :product_selection_from_coordinator_inventory_only, :boolean, default: false
scope :active, lambda { where('order_cycles.orders_open_at <= ? AND order_cycles.orders_close_at >= ?', Time.zone.now, Time.zone.now) }
scope :active_or_complete, lambda { where('order_cycles.orders_open_at <= ?', Time.zone.now) }
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 :not_closed, lambda { where('order_cycles.orders_close_at > ? OR order_cycles.orders_close_at IS NULL', 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('order_cycles.orders_open_at IS NULL OR orders_close_at IS NULL')
scope :dated, where('orders_open_at IS NOT NULL AND orders_close_at IS NOT 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.
@@ -187,6 +193,10 @@ class OrderCycle < ActiveRecord::Base
self.variants.include? variant
end
def dated?
!undated?
end
def undated?
self.orders_open_at.nil? || self.orders_close_at.nil?
end
@@ -236,6 +246,10 @@ class OrderCycle < ActiveRecord::Base
coordinator.users.include? user
end
def refresh_products_cache
OpenFoodNetwork::ProductsCache.order_cycle_changed self
end
private

View File

@@ -1,8 +1,12 @@
class ProducerProperty < ActiveRecord::Base
belongs_to :producer, class_name: 'Enterprise'
belongs_to :property, class_name: 'Spree::Property'
default_scope order("#{self.table_name}.position")
after_save :refresh_products_cache
after_destroy :refresh_products_cache_from_destroy
def property_name
property.name if property
@@ -14,4 +18,16 @@ class ProducerProperty < ActiveRecord::Base
Spree::Property.create(name: name, presentation: name)
end
end
private
def refresh_products_cache
OpenFoodNetwork::ProductsCache.producer_property_changed self
end
def refresh_products_cache_from_destroy
OpenFoodNetwork::ProductsCache.producer_property_destroyed self
end
end

View File

@@ -72,6 +72,10 @@ class AbilityDecorator
can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], ProducerProperty
can [:admin, :destroy], TagRule do |tag_rule|
user.enterprises.include? tag_rule.enterprise
end
can [:admin, :index, :create], Enterprise
can [:read, :edit, :update, :bulk_update, :resend_confirmation], Enterprise do |enterprise|
OpenFoodNetwork::Permissions.new(user).editable_enterprises.include? enterprise
@@ -97,6 +101,11 @@ class AbilityDecorator
can [:print], Spree::Order do |order|
order.user == user
end
can [:create], Customer
can [:destroy], Customer do |customer|
user.enterprises.include? customer.enterprise
end
end
def add_product_management_abilities(user)

View File

@@ -22,4 +22,8 @@ Spree::AppConfiguration.class_eval do
preference :account_invoices_tax_rate, :decimal, default: 0
preference :shop_trial_length_days, :integer, default: 30
preference :minimum_billable_turnover, :integer, default: -1
# Monitoring
preference :last_job_queue_heartbeat_at, :string, default: nil
end

View File

@@ -1,6 +1,15 @@
Spree::Classification.class_eval do
belongs_to :product, :class_name => "Spree::Product", touch: true
after_save :refresh_products_cache
before_destroy :dont_destroy_if_primary_taxon
after_destroy :refresh_products_cache
private
def refresh_products_cache
product.refresh_products_cache
end
def dont_destroy_if_primary_taxon
if product.primary_taxon == taxon

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,8 @@ Spree::Order.class_eval do
attr_accessible :order_cycle_id, :distributor_id
before_validation :shipping_address_from_distributor
before_validation :associate_customer, unless: :customer_is_valid?
before_validation :associate_customer, unless: :customer_id?
before_validation :ensure_customer, unless: :customer_is_valid?
checkout_flow do
go_to_state :address
@@ -69,13 +70,6 @@ Spree::Order.class_eval do
where("state != ?", state)
}
scope :with_payment_method_name, lambda { |payment_method_name|
joins(:payments => :payment_method).
where('spree_payment_methods.name IN (?)', payment_method_name).
select('DISTINCT spree_orders.*')
}
# -- Methods
def products_available_from_new_distribution
# Check that the line_items in the current order are available from a newly selected distribution
@@ -179,6 +173,10 @@ Spree::Order.class_eval do
if order_cycle
OpenFoodNetwork::EnterpriseFeeCalculator.new.create_order_adjustments_for self
end
if distributor.present? && customer.present?
distributor.apply_tag_rules_to(self, customer: customer)
end
end
end
@@ -286,17 +284,21 @@ Spree::Order.class_eval do
def customer_is_valid?
return true unless require_customer?
customer.present? && customer.enterprise_id == distributor_id && customer.email == (user.andand.email || email)
customer.present? && customer.enterprise_id == distributor_id && customer.email == email_for_customer
end
def email_for_customer
(user.andand.email || email).andand.downcase
end
def associate_customer
email_for_customer = user.andand.email || email
existing_customer = Customer.of(distributor).find_by_email(email_for_customer)
if existing_customer
self.customer = existing_customer
else
new_customer = Customer.create(enterprise: distributor, email: email_for_customer, user: user)
self.customer = new_customer
return customer if customer.present?
self.customer = Customer.of(distributor).find_by_email(email_for_customer)
end
def ensure_customer
unless associate_customer
self.customer = Customer.create(enterprise: distributor, email: email_for_customer, user: user)
end
end
end

View File

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

View File

@@ -0,0 +1,12 @@
module Spree
Price.class_eval do
after_save :refresh_products_cache
private
def refresh_products_cache
variant.andand.refresh_products_cache
end
end
end

View File

@@ -22,12 +22,10 @@ Spree::Product.class_eval do
attr_accessible :variant_unit, :variant_unit_scale, :variant_unit_name, :unit_value
attr_accessible :inherits_properties, :sku
before_validation :sanitize_permalink
# validates_presence_of :variants, unless: :new_record?, message: "Product must have at least one variant"
validates_presence_of :supplier
validates :primary_taxon, presence: { message: "^Product Category can't be blank" }
validates :tax_category_id, presence: { message: "^Tax Category can't be blank" }, if: "Spree::Config.products_require_tax_category"
validates :primary_taxon, presence: { message: I18n.t("validation_msg_product_category_cant_be_blank") }
validates :tax_category_id, presence: { message: I18n.t("validation_msg_tax") }, if: "Spree::Config.products_require_tax_category"
validates_presence_of :variant_unit
validates_presence_of :variant_unit_scale,
@@ -35,11 +33,13 @@ Spree::Product.class_eval do
validates_presence_of :variant_unit_name,
if: -> p { p.variant_unit == 'items' }
after_save :ensure_standard_variant
after_initialize :set_available_on_to_now, :if => :new_record?
after_save :update_units
after_touch :touch_distributors
before_validation :sanitize_permalink
before_save :add_primary_taxon_to_taxons
after_touch :touch_distributors
after_save :ensure_standard_variant
after_save :update_units
after_save :refresh_products_cache
# -- Joins
@@ -198,6 +198,11 @@ Spree::Product.class_eval do
alias_method_chain :delete, :delete_from_order_cycles
def refresh_products_cache
OpenFoodNetwork::ProductsCache.product_changed self
end
private
def set_available_on_to_now

View File

@@ -0,0 +1,10 @@
module Spree
ProductProperty.class_eval do
after_save :refresh_products_cache
after_destroy :refresh_products_cache
def refresh_products_cache
product.refresh_products_cache
end
end
end

View File

@@ -0,0 +1,15 @@
module Spree
Property.class_eval do
after_save :refresh_products_cache
# When a Property is destroyed, dependent-destroy will destroy all ProductProperties,
# which will take care of refreshing the products cache
private
def refresh_products_cache
product_properties(:reload).each &:refresh_products_cache
end
end
end

View File

@@ -1,10 +1,12 @@
Spree::ShippingMethod.class_eval do
acts_as_taggable
has_many :distributor_shipping_methods
has_many :distributors, through: :distributor_shipping_methods, class_name: 'Enterprise', foreign_key: 'distributor_id'
after_save :touch_distributors
attr_accessible :distributor_ids, :description
attr_accessible :require_ship_address
attr_accessible :require_ship_address, :tag_list
validates :distributors, presence: { message: "^At least one hub must be selected" }

View File

@@ -1,7 +1,12 @@
Spree::Taxon.class_eval do
has_many :classifications, :dependent => :destroy
self.attachment_definitions[:icon][:path] = 'public/images/spree/taxons/:id/:style/:basename.:extension'
self.attachment_definitions[:icon][:url] = '/images/spree/taxons/:id/:style/:basename.:extension'
after_save :refresh_products_cache
# Indicate which filters should be used for this taxon
def applicable_filters
@@ -45,4 +50,11 @@ Spree::Taxon.class_eval do
taxons
end
private
def refresh_products_cache
products(:reload).each &:refresh_products_cache
end
end

View File

@@ -15,6 +15,7 @@ Spree.user_class.class_eval do
accepts_nested_attributes_for :enterprise_roles, :allow_destroy => true
attr_accessible :enterprise_ids, :enterprise_roles_attributes, :enterprise_limit
after_create :associate_customers
after_create :send_signup_confirmation
validate :limit_owned_enterprises
@@ -41,6 +42,10 @@ Spree.user_class.class_eval do
customers.of(enterprise).first
end
def associate_customers
Customer.update_all({ user_id: id }, { user_id: nil, email: email })
end
def send_signup_confirmation
Delayed::Job.enqueue ConfirmSignupJob.new(id)
end
@@ -49,6 +54,27 @@ Spree.user_class.class_eval do
owned_enterprises(:reload).size < enterprise_limit
end
# Returns Enterprise IDs for distributors that the user has shopped at
def enterprises_ordered_from
orders.where(state: :complete).map(&:distributor_id).uniq
end
# Returns orders and their associated payments for all distributors that have been ordered from
def compelete_orders_by_distributor
Enterprise
.includes(distributed_orders: { payments: :payment_method })
.where(enterprises: { id: enterprises_ordered_from },
spree_orders: { state: 'complete', user_id: id })
.order('spree_orders.completed_at DESC')
end
def orders_by_distributor
# Remove uncompleted payments as these will not be reflected in order balance
data_array = compelete_orders_by_distributor.to_a
remove_uncompleted_payments(data_array)
data_array.sort! { |a, b| b.distributed_orders.length <=> a.distributed_orders.length }
end
private
def limit_owned_enterprises
@@ -56,4 +82,12 @@ Spree.user_class.class_eval do
errors.add(:owned_enterprises, "^#{email} is not permitted to own any more enterprises (limit is #{enterprise_limit}).")
end
end
def remove_uncompleted_payments(enterprises)
enterprises.each do |enterprise|
enterprise.distributed_orders.each do |order|
order.payments.keep_if { |payment| payment.state == "completed" }
end
end
end
end

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