dugway 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (125) hide show
  1. data/.gitignore +7 -0
  2. data/.travis.yml +3 -0
  3. data/Gemfile +2 -0
  4. data/README.md +151 -0
  5. data/Rakefile +12 -0
  6. data/bin/dugway +7 -0
  7. data/dugway.gemspec +43 -0
  8. data/lib/dugway.rb +66 -0
  9. data/lib/dugway/application.rb +147 -0
  10. data/lib/dugway/cart.rb +144 -0
  11. data/lib/dugway/cli.rb +22 -0
  12. data/lib/dugway/cli/build.rb +56 -0
  13. data/lib/dugway/cli/create.rb +71 -0
  14. data/lib/dugway/cli/server.rb +37 -0
  15. data/lib/dugway/cli/templates/Gemfile.tt +2 -0
  16. data/lib/dugway/cli/templates/config.tt +24 -0
  17. data/lib/dugway/cli/templates/gitignore.tt +9 -0
  18. data/lib/dugway/cli/templates/source/cart.html +40 -0
  19. data/lib/dugway/cli/templates/source/checkout.html +33 -0
  20. data/lib/dugway/cli/templates/source/contact.html +37 -0
  21. data/lib/dugway/cli/templates/source/home.html +25 -0
  22. data/lib/dugway/cli/templates/source/images/badge.png +0 -0
  23. data/lib/dugway/cli/templates/source/javascripts/cart.js.coffee +7 -0
  24. data/lib/dugway/cli/templates/source/javascripts/product.js.coffee +7 -0
  25. data/lib/dugway/cli/templates/source/layout.html +101 -0
  26. data/lib/dugway/cli/templates/source/maintenance.html +20 -0
  27. data/lib/dugway/cli/templates/source/product.html +65 -0
  28. data/lib/dugway/cli/templates/source/products.html +25 -0
  29. data/lib/dugway/cli/templates/source/screenshot.jpg +0 -0
  30. data/lib/dugway/cli/templates/source/scripts.js +19 -0
  31. data/lib/dugway/cli/templates/source/settings.json +71 -0
  32. data/lib/dugway/cli/templates/source/styles.css +27 -0
  33. data/lib/dugway/cli/templates/source/stylesheets/cart.css.sass +38 -0
  34. data/lib/dugway/cli/templates/source/stylesheets/layout.css.sass +103 -0
  35. data/lib/dugway/cli/templates/source/stylesheets/product.css.sass +19 -0
  36. data/lib/dugway/cli/templates/source/stylesheets/products.css.sass +20 -0
  37. data/lib/dugway/cli/templates/source/stylesheets/vendor/normalize.css +396 -0
  38. data/lib/dugway/cli/templates/source/success.html +5 -0
  39. data/lib/dugway/controller.rb +148 -0
  40. data/lib/dugway/data/locales/cs.yml +26 -0
  41. data/lib/dugway/data/locales/da.yml +26 -0
  42. data/lib/dugway/data/locales/en-AU.yml +26 -0
  43. data/lib/dugway/data/locales/en-GB.yml +26 -0
  44. data/lib/dugway/data/locales/en-PH.yml +27 -0
  45. data/lib/dugway/data/locales/en-US.yml +26 -0
  46. data/lib/dugway/data/locales/es-MX.yml +26 -0
  47. data/lib/dugway/data/locales/eu.yml +26 -0
  48. data/lib/dugway/data/locales/gsw-CH.yml +26 -0
  49. data/lib/dugway/data/locales/hu.yml +26 -0
  50. data/lib/dugway/data/locales/il.yml +26 -0
  51. data/lib/dugway/data/locales/ja.yml +30 -0
  52. data/lib/dugway/data/locales/ms-MY.yml +26 -0
  53. data/lib/dugway/data/locales/nb.yml +22 -0
  54. data/lib/dugway/data/locales/pl.yml +27 -0
  55. data/lib/dugway/data/locales/pt-BR.yml +26 -0
  56. data/lib/dugway/data/locales/sv-SE.yml +26 -0
  57. data/lib/dugway/data/locales/th.yml +26 -0
  58. data/lib/dugway/data/locales/tr.yml +29 -0
  59. data/lib/dugway/data/locales/zh-TW.yml +30 -0
  60. data/lib/dugway/data/theme_fonts.yml +151 -0
  61. data/lib/dugway/extensions/time.rb +15 -0
  62. data/lib/dugway/liquid/drops/account_drop.rb +17 -0
  63. data/lib/dugway/liquid/drops/artist_drop.rb +9 -0
  64. data/lib/dugway/liquid/drops/artists_drop.rb +13 -0
  65. data/lib/dugway/liquid/drops/base_drop.rb +59 -0
  66. data/lib/dugway/liquid/drops/cart_drop.rb +13 -0
  67. data/lib/dugway/liquid/drops/cart_item_drop.rb +21 -0
  68. data/lib/dugway/liquid/drops/categories_drop.rb +13 -0
  69. data/lib/dugway/liquid/drops/category_drop.rb +9 -0
  70. data/lib/dugway/liquid/drops/contact_drop.rb +29 -0
  71. data/lib/dugway/liquid/drops/country_drop.rb +6 -0
  72. data/lib/dugway/liquid/drops/currency_drop.rb +6 -0
  73. data/lib/dugway/liquid/drops/image_drop.rb +6 -0
  74. data/lib/dugway/liquid/drops/page_drop.rb +13 -0
  75. data/lib/dugway/liquid/drops/pages_drop.rb +9 -0
  76. data/lib/dugway/liquid/drops/product_drop.rb +101 -0
  77. data/lib/dugway/liquid/drops/product_option_drop.rb +27 -0
  78. data/lib/dugway/liquid/drops/products_drop.rb +75 -0
  79. data/lib/dugway/liquid/drops/shipping_option_drop.rb +13 -0
  80. data/lib/dugway/liquid/drops/theme_drop.rb +23 -0
  81. data/lib/dugway/liquid/filters/comparison_filters.rb +48 -0
  82. data/lib/dugway/liquid/filters/core_filters.rb +138 -0
  83. data/lib/dugway/liquid/filters/default_pagination.rb +35 -0
  84. data/lib/dugway/liquid/filters/font_filters.rb +9 -0
  85. data/lib/dugway/liquid/filters/url_filters.rb +66 -0
  86. data/lib/dugway/liquid/filters/util_filters.rb +119 -0
  87. data/lib/dugway/liquid/tags/checkout_form.rb +9 -0
  88. data/lib/dugway/liquid/tags/get.rb +58 -0
  89. data/lib/dugway/liquid/tags/paginate.rb +129 -0
  90. data/lib/dugway/liquifier.rb +122 -0
  91. data/lib/dugway/logger.rb +16 -0
  92. data/lib/dugway/request.rb +36 -0
  93. data/lib/dugway/store.rb +166 -0
  94. data/lib/dugway/template.rb +44 -0
  95. data/lib/dugway/theme.rb +145 -0
  96. data/lib/dugway/theme_font.rb +68 -0
  97. data/lib/dugway/version.rb +3 -0
  98. data/spec/fixtures/store/page/about-us.js +1 -0
  99. data/spec/fixtures/store/products.js +1 -0
  100. data/spec/fixtures/store/store.js +1 -0
  101. data/spec/fixtures/theme/cart.html +33 -0
  102. data/spec/fixtures/theme/checkout.html +31 -0
  103. data/spec/fixtures/theme/contact.html +37 -0
  104. data/spec/fixtures/theme/home.html +38 -0
  105. data/spec/fixtures/theme/images/bc_badge.png +0 -0
  106. data/spec/fixtures/theme/javascripts/one.js +3 -0
  107. data/spec/fixtures/theme/javascripts/two.js.coffee +2 -0
  108. data/spec/fixtures/theme/layout.html +77 -0
  109. data/spec/fixtures/theme/maintenance.html +17 -0
  110. data/spec/fixtures/theme/product.html +73 -0
  111. data/spec/fixtures/theme/products.html +41 -0
  112. data/spec/fixtures/theme/screenshot.jpg +0 -0
  113. data/spec/fixtures/theme/scripts.js +2 -0
  114. data/spec/fixtures/theme/settings.json +55 -0
  115. data/spec/fixtures/theme/styles.css +4 -0
  116. data/spec/fixtures/theme/stylesheets/one.css +3 -0
  117. data/spec/fixtures/theme/stylesheets/two.css.sass +4 -0
  118. data/spec/fixtures/theme/success.html +5 -0
  119. data/spec/spec_helper.rb +24 -0
  120. data/spec/units/dugway/request_spec.rb +206 -0
  121. data/spec/units/dugway/store_spec.rb +194 -0
  122. data/spec/units/dugway/template_spec.rb +145 -0
  123. data/spec/units/dugway/theme_font_spec.rb +136 -0
  124. data/spec/units/dugway_spec.rb +9 -0
  125. metadata +549 -0
@@ -0,0 +1,129 @@
1
+ module Dugway
2
+ module Tags
3
+ class Paginate < ::Liquid::Block
4
+ Syntax = /(\w+)\s+from\s+(#{ Liquid::QuotedFragment })\s*(by\s*(#{ Liquid::QuotedFragment }))?/
5
+
6
+ def initialize(tag_name, markup, tokens)
7
+ if markup =~ Syntax
8
+ @variable_name = $1
9
+ @collection_name = $2
10
+ @per_page = $3.present? ? $4 : nil
11
+
12
+ @attributes = { 'inner_window' => 3, 'outer_window' => 1 }
13
+
14
+ markup.scan(Liquid::TagAttributes) { |key, value|
15
+ @attributes[key] = value
16
+ }
17
+
18
+ @limit = @attributes['limit']
19
+ else
20
+ raise SyntaxError.new("Syntax Error in tag 'paginate' - Valid syntax: paginate [variable] from [collection] by [number]")
21
+ end
22
+
23
+ super
24
+ end
25
+
26
+ def render(context)
27
+ @context = context
28
+ @limit = context[@limit].present? ? context[@limit] : (@limit.present? ? @limit.to_i : nil)
29
+ @per_page = context[@per_page].present? ? context[@per_page] : (@per_page.present? ? @per_page.to_i : nil)
30
+ @order = context[@attributes['order']].present? ? context[@attributes['order']] : @attributes['order']
31
+
32
+ context.stack do
33
+ context['internal'] = {
34
+ 'per_page' => @per_page,
35
+ 'order' => @order,
36
+ 'page' => @context.registers[:params][:page] && @context.registers[:params][:page].to_i,
37
+ 'limit' => @limit
38
+ }
39
+
40
+ collection = context[@collection_name]
41
+ context[@variable_name] = collection
42
+ current_page = collection.current_page
43
+
44
+ pagination = {
45
+ 'page_size' => collection.per_page,
46
+ 'current_page' => current_page,
47
+ 'current_offset' => collection.offset
48
+ }
49
+
50
+ context['paginate'] = pagination
51
+
52
+ collection_size = collection.total_entries
53
+
54
+ raise ArgumentError.new("Cannot paginate array '#{ @collection_name }'. Not found.") if collection_size.nil?
55
+
56
+ page_count = collection.total_pages
57
+
58
+ pagination['items'] = collection_size
59
+ pagination['pages'] = page_count
60
+
61
+ pagination['previous'] = collection.previous_page.blank? ? no_link('&laquo; Previous') : link('&laquo; Previous', collection.previous_page)
62
+
63
+ pagination['next'] = collection.next_page.blank? ? no_link('Next &raquo;') : link('Next &raquo;', collection.next_page)
64
+
65
+ pagination['parts'] = []
66
+
67
+ if page_count > 1
68
+ inner_window = @attributes['inner_window'].to_i
69
+ outer_window = @attributes['outer_window'].to_i
70
+
71
+ min = current_page - inner_window
72
+ max = current_page + inner_window
73
+
74
+ # Adjust lower or upper limit if other is out of bounds
75
+ if max > page_count
76
+ min -= max - page_count
77
+ elsif min < 1
78
+ max += 1 - min
79
+ end
80
+
81
+ current = min..max
82
+ beginning = 1..(1 + outer_window)
83
+ tail = (page_count - outer_window)..page_count
84
+ visible = [current, beginning, tail].map(&:to_a).flatten
85
+ visible &= (1..page_count).to_a
86
+
87
+ hellip_break = false
88
+
89
+ 1.upto(page_count) { |page|
90
+ if page == current_page
91
+ pagination['parts'] << no_link(page)
92
+ elsif visible.include?(page)
93
+ pagination['parts'] << link(page, page)
94
+ elsif page == beginning.last + 1 || page == tail.first - 1
95
+ next if hellip_break
96
+
97
+ pagination['parts'] << no_link('&hellip;')
98
+ hellip_break = true
99
+ next
100
+ end
101
+
102
+ hellip_break = false
103
+ }
104
+ end
105
+
106
+ render_all @nodelist, context
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ def no_link(title)
113
+ { 'title' => title.to_s, 'is_link' => false }
114
+ end
115
+
116
+ def link(title, page)
117
+ { 'title' => title.to_s, 'url' => current_url + "?page=#{ page }#{ search }", 'is_link' => true }
118
+ end
119
+
120
+ def search
121
+ @context.registers[:params]['search'].present? ? "&search=#{ @context.registers[:params]['search'] }" : nil
122
+ end
123
+
124
+ def current_url
125
+ @context.registers[:path]
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,122 @@
1
+ require 'liquid'
2
+ require "#{ File.dirname(__FILE__) }/liquid/drops/base_drop"
3
+ Dir.glob("#{ File.dirname(__FILE__) }/liquid/**/*.rb").each { |file| require file }
4
+
5
+ Liquid::Template.register_filter(Dugway::Filters::UtilFilters)
6
+ Liquid::Template.register_filter(Dugway::Filters::CoreFilters)
7
+ Liquid::Template.register_filter(Dugway::Filters::DefaultPagination)
8
+ Liquid::Template.register_filter(Dugway::Filters::UrlFilters)
9
+ Liquid::Template.register_filter(Dugway::Filters::FontFilters)
10
+
11
+ Liquid::Template.register_tag(:checkoutform, Dugway::Tags::CheckoutForm)
12
+ Liquid::Template.register_tag(:get, Dugway::Tags::Get)
13
+ Liquid::Template.register_tag(:paginate, Dugway::Tags::Paginate)
14
+
15
+ module Dugway
16
+ class Liquifier
17
+ ESCAPE_CSS = {
18
+ '{{' => '"<<',
19
+ '}}' => '>>"',
20
+ '{%' => '"<',
21
+ '%}' => '">'
22
+ }
23
+
24
+ def initialize(request)
25
+ @request = request
26
+ end
27
+
28
+ def render(content, variables={})
29
+ variables.symbolize_keys!
30
+
31
+ assigns = shared_assigns
32
+ assigns['page_content'] = variables[:page_content]
33
+ assigns['page'] = Drops::PageDrop.new(variables[:page])
34
+ assigns['product'] = Drops::ProductDrop.new(variables[:product])
35
+
36
+ registers = shared_registers
37
+ registers[:category] = variables[:category]
38
+ registers[:artist] = variables[:artist]
39
+
40
+ if errors = variables.delete(:errors)
41
+ shared_context['errors'] << errors
42
+ end
43
+
44
+ context = Liquid::Context.new([ assigns, shared_context ], {}, registers)
45
+ Liquid::Template.parse(content).render!(context)
46
+ end
47
+
48
+ def self.render_styles(css)
49
+ Liquid::Template.parse(css).render!(
50
+ { 'theme' => Drops::ThemeDrop.new(Dugway.theme.customization) },
51
+ :registers => { :settings => Dugway.theme.settings }
52
+ )
53
+ end
54
+
55
+ def self.escape_styles(css)
56
+ ESCAPE_CSS.each_pair { |k,v| css.gsub!(k,v) }
57
+ css
58
+ end
59
+
60
+ def self.unescape_styles(css)
61
+ ESCAPE_CSS.each_pair { |k,v| css.gsub!(v,k) }
62
+ css
63
+ end
64
+
65
+ private
66
+
67
+ def store
68
+ Dugway.store
69
+ end
70
+
71
+ def theme
72
+ Dugway.theme
73
+ end
74
+
75
+ def cart
76
+ Dugway.cart
77
+ end
78
+
79
+ def shared_context
80
+ @shared_context ||= { 'errors' => [] }
81
+ end
82
+
83
+ def shared_assigns
84
+ {
85
+ 'store' => Drops::AccountDrop.new(store.account),
86
+ 'cart' => Drops::CartDrop.new(cart),
87
+ 'theme' => Drops::ThemeDrop.new(theme.customization),
88
+ 'pages' => Drops::PagesDrop.new(store.pages.map { |p| Drops::PageDrop.new(p) }),
89
+ 'categories' => Drops::CategoriesDrop.new(store.categories.map { |c| Drops::CategoryDrop.new(c) }),
90
+ 'artists' => Drops::ArtistsDrop.new(store.artists.map { |a| Drops::ArtistDrop.new(a) }),
91
+ 'products' => Drops::ProductsDrop.new(store.products.map { |p| Drops::ProductDrop.new(p) }),
92
+ 'contact' => Drops::ContactDrop.new,
93
+ 'head_content' => head_content,
94
+ 'bigcartel_credit' => bigcartel_credit
95
+ }
96
+ end
97
+
98
+ def shared_registers
99
+ {
100
+ :request => @request,
101
+ :path => @request.path,
102
+ :params => @request.params.with_indifferent_access,
103
+ :currency => store.currency,
104
+ :settings => theme.settings
105
+ }
106
+ end
107
+
108
+ def head_content
109
+ content = %{<meta name="generator" content="Big Cartel">}
110
+
111
+ if google_font_url = ThemeFont.google_font_url_for_theme
112
+ content << %{\n<link rel="stylesheet" type="text/css" href="#{ google_font_url }">}
113
+ end
114
+
115
+ content
116
+ end
117
+
118
+ def bigcartel_credit
119
+ '<a href="http://bigcartel.com/" title="Start your own store at Big Cartel now">Online Store by Big Cartel</a>'
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,16 @@
1
+ require 'logger'
2
+
3
+ module Dugway
4
+ class Logger < ::Logger
5
+ def initialize
6
+ Dir.mkdir(dir) unless File.exists?(dir)
7
+ super(File.join(dir, 'dugway.log'))
8
+ end
9
+
10
+ def dir
11
+ File.join(Dir.pwd, 'log')
12
+ end
13
+
14
+ alias write <<
15
+ end
16
+ end
@@ -0,0 +1,36 @@
1
+ module Dugway
2
+ class Request < Rack::Request
3
+ def params
4
+ super.update(env['rack.routing_args']).symbolize_keys
5
+ end
6
+
7
+ def page_permalink
8
+ case path
9
+ when %r{^/$}
10
+ 'home'
11
+ when %r{^/(products|category|artist)/}
12
+ 'products'
13
+ when %r{^/product/}
14
+ 'product'
15
+ else
16
+ File.basename(path[1..-1], '.*')
17
+ end
18
+ end
19
+
20
+ def extension
21
+ File.extname(path).present? ? File.extname(path) : '.html'
22
+ end
23
+
24
+ def format
25
+ params[:format] || extension[1..-1]
26
+ end
27
+
28
+ def html?
29
+ format == 'html'
30
+ end
31
+
32
+ def js?
33
+ format == 'js'
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,166 @@
1
+ require 'httparty'
2
+
3
+ module Dugway
4
+ class Store
5
+ include HTTParty
6
+
7
+ format :json
8
+ default_timeout 5
9
+ headers 'User-Agent' => "Dugway #{ Dugway::VERSION }"
10
+
11
+ def initialize(subdomain)
12
+ self.class.base_uri "http://api.bigcartel.com/#{ subdomain }"
13
+ end
14
+
15
+ def account
16
+ @account ||= get('/store.js')
17
+ end
18
+
19
+ def theme_pages
20
+ [
21
+ { 'name' => 'Home', 'permalink' => 'home', 'url' => '/', 'category' => 'theme' },
22
+ { 'name' => 'Products', 'permalink' => 'products', 'url' => '/products', 'category' => 'theme' },
23
+ { 'name' => 'Product', 'permalink' => 'product', 'url' => '/product', 'category' => 'theme' },
24
+ { 'name' => 'Cart', 'permalink' => 'cart', 'url' => '/cart', 'category' => 'theme' },
25
+ { 'name' => 'Checkout', 'permalink' => 'checkout', 'url' => '/checkout', 'category' => 'theme' },
26
+ { 'name' => 'Success', 'permalink' => 'success', 'url' => '/success', 'category' => 'theme' },
27
+ { 'name' => 'Contact', 'permalink' => 'contact', 'url' => '/contact', 'category' => 'theme' },
28
+ { 'name' => 'Maintenance', 'permalink' => 'maintenance', 'url' => '/maintenance', 'category' => 'theme' }
29
+ ]
30
+ end
31
+
32
+ def custom_pages
33
+ @custom_pages ||= begin
34
+ custom_pages = account.has_key?('pages') ? account['pages'] : []
35
+ custom_pages = custom_pages.map { |page| get("/page/#{ page['permalink'] }.js") }
36
+ end
37
+ end
38
+
39
+ def pages
40
+ @pages ||= theme_pages + custom_pages
41
+ end
42
+
43
+ def page(permalink)
44
+ lookup(permalink, pages)
45
+ end
46
+
47
+ def categories
48
+ account.has_key?('categories') ? account['categories'] : []
49
+ end
50
+
51
+ def category(permalink)
52
+ lookup(permalink, categories)
53
+ end
54
+
55
+ def category_products(permalink)
56
+ lookup_products(permalink, 'categories')
57
+ end
58
+
59
+ def artists
60
+ account.has_key?('artists') ? account['artists'] : []
61
+ end
62
+
63
+ def artist(permalink)
64
+ lookup(permalink, artists)
65
+ end
66
+
67
+ def artist_products(permalink)
68
+ lookup_products(permalink, 'artists')
69
+ end
70
+
71
+ def products
72
+ @products ||= get('/products.js')
73
+ end
74
+
75
+ def product(permalink)
76
+ lookup(permalink, products)
77
+ end
78
+
79
+ def product_and_option(option_id)
80
+ products.each { |product|
81
+ product['options'].each { |option|
82
+ if option['id'] == option_id
83
+ return product, option
84
+ end
85
+ }
86
+ }
87
+
88
+ nil
89
+ end
90
+
91
+ def previous_product(permalink)
92
+ products.each_with_index { |product, index|
93
+ if product['permalink'] == permalink && index > 0 && previous_product = products[index - 1]
94
+ return previous_product
95
+ end
96
+ }
97
+
98
+ nil
99
+ end
100
+
101
+ def next_product(permalink)
102
+ products.each_with_index { |product, index|
103
+ if product['permalink'] == permalink && (index + 1) < products.size && next_product = products[index + 1]
104
+ return next_product
105
+ end
106
+ }
107
+
108
+ nil
109
+ end
110
+
111
+ def search_products(search_terms)
112
+ products.select { |p| p['name'].downcase.include?(search_terms.downcase) || p['description'].downcase.include?(search_terms.downcase) }
113
+ end
114
+
115
+ def country
116
+ account['country']
117
+ end
118
+
119
+ def currency
120
+ account['currency']
121
+ end
122
+
123
+ def locale
124
+ case currency['code']
125
+ when 'AUD' then 'en-AU'
126
+ when 'BRL' then 'pt-BR'
127
+ when 'CAD' then 'en-US'
128
+ when 'CZK' then 'cs'
129
+ when 'DKK' then 'da'
130
+ when 'EUR' then 'eu'
131
+ when 'HKD' then 'en-US'
132
+ when 'HUF' then 'hu'
133
+ when 'ILS' then 'il'
134
+ when 'JPY' then 'ja'
135
+ when 'MYR' then 'ms-MY'
136
+ when 'MXN' then 'es-MX'
137
+ when 'TWD' then 'zh-TW'
138
+ when 'NZD' then 'en-US'
139
+ when 'NOK' then 'nb'
140
+ when 'PHP' then 'en-PH'
141
+ when 'PLN' then 'pl'
142
+ when 'GBP' then 'en-GB'
143
+ when 'SGD' then 'en-US'
144
+ when 'SEK' then 'sv-SE'
145
+ when 'CHF' then 'gsw-CH'
146
+ when 'THB' then 'th'
147
+ when 'TRY' then 'tr'
148
+ else 'en-US'
149
+ end
150
+ end
151
+
152
+ private
153
+
154
+ def get(path)
155
+ self.class.get(path).parsed_response
156
+ end
157
+
158
+ def lookup(permalink, array)
159
+ array.find { |item| item['permalink'] == permalink }.try(:dup)
160
+ end
161
+
162
+ def lookup_products(permalink, type)
163
+ products.select { |p| p[type].any? { |c| c['permalink'] == permalink }}
164
+ end
165
+ end
166
+ end