openfoodfacts 0.3.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/README.md +13 -6
- data/lib/openfoodfacts.rb +8 -1
- data/lib/openfoodfacts/additive.rb +6 -6
- data/lib/openfoodfacts/allergen.rb +3 -3
- data/lib/openfoodfacts/brand.rb +2 -2
- data/lib/openfoodfacts/category.rb +3 -3
- data/lib/openfoodfacts/city.rb +3 -3
- data/lib/openfoodfacts/contributor.rb +3 -3
- data/lib/openfoodfacts/country.rb +3 -3
- data/lib/openfoodfacts/entry_date.rb +33 -0
- data/lib/openfoodfacts/faq.rb +5 -5
- data/lib/openfoodfacts/ingredient_that_may_be_from_palm_oil.rb +3 -3
- data/lib/openfoodfacts/label.rb +5 -10
- data/lib/openfoodfacts/language.rb +33 -0
- data/lib/openfoodfacts/last_edit_date.rb +33 -0
- data/lib/openfoodfacts/locale.rb +22 -8
- data/lib/openfoodfacts/manufacturing_place.rb +3 -3
- data/lib/openfoodfacts/mission.rb +72 -0
- data/lib/openfoodfacts/number_of_ingredients.rb +33 -0
- data/lib/openfoodfacts/nutrition_grade.rb +33 -0
- data/lib/openfoodfacts/origin.rb +3 -3
- data/lib/openfoodfacts/packager_code.rb +3 -3
- data/lib/openfoodfacts/packaging.rb +3 -3
- data/lib/openfoodfacts/press.rb +16 -26
- data/lib/openfoodfacts/product.rb +26 -30
- data/lib/openfoodfacts/product_state.rb +3 -3
- data/lib/openfoodfacts/purchase_place.rb +3 -3
- data/lib/openfoodfacts/store.rb +3 -3
- data/lib/openfoodfacts/trace.rb +3 -3
- data/lib/openfoodfacts/user.rb +5 -5
- data/lib/openfoodfacts/version.rb +1 -1
- data/test/minitest_helper.rb +7 -1
- data/test/test_openfoodfacts.rb +151 -25
- metadata +36 -57
- data/test/fixtures/additives.yml +0 -1963
- data/test/fixtures/additives_locale.yml +0 -1963
- data/test/fixtures/brands.yml +0 -31542
- data/test/fixtures/brands_locale.yml +0 -17498
- data/test/fixtures/faq.yml +0 -1078
- data/test/fixtures/fetch_product_3029330003533.yml +0 -280
- data/test/fixtures/index.yml +0 -510
- data/test/fixtures/labels.yml +0 -9091
- data/test/fixtures/login_user.yml +0 -38
- data/test/fixtures/press.yml +0 -1284
- data/test/fixtures/product_3029330003533.yml +0 -280
- data/test/fixtures/product_states.yml +0 -910
- data/test/fixtures/product_states_locale.yml +0 -1002
- data/test/fixtures/products_for_brand.yml +0 -5481
- data/test/fixtures/products_for_state.yml +0 -2178
- data/test/fixtures/products_with_additive.yml +0 -3130
- data/test/fixtures/search_Chocolat.yml +0 -586
- data/test/fixtures/search_Chocolat_1_000_000.yml +0 -200
@@ -15,9 +15,9 @@ module Openfoodfacts
|
|
15
15
|
|
16
16
|
# Get manufacturing places
|
17
17
|
#
|
18
|
-
def all(locale:
|
18
|
+
def all(locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
|
19
19
|
if path = LOCALE_PATHS[locale]
|
20
|
-
Product.tags_from_page(self, "
|
20
|
+
Product.tags_from_page(self, "https://#{locale}.#{domain}/#{path}")
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
@@ -30,4 +30,4 @@ module Openfoodfacts
|
|
30
30
|
end
|
31
31
|
|
32
32
|
end
|
33
|
-
end
|
33
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'hashie'
|
2
|
+
require 'open-uri'
|
3
|
+
|
4
|
+
module Openfoodfacts
|
5
|
+
class Mission < Hashie::Mash
|
6
|
+
|
7
|
+
# TODO: Add more locales
|
8
|
+
LOCALE_PATHS = {
|
9
|
+
'fr' => 'missions',
|
10
|
+
'uk' => 'missions',
|
11
|
+
'us' => 'missions',
|
12
|
+
'world' => 'missions'
|
13
|
+
}
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def all(locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
|
17
|
+
if path = LOCALE_PATHS[locale]
|
18
|
+
url = "https://#{locale}.#{domain}/#{path}"
|
19
|
+
html = URI.open(url).read
|
20
|
+
dom = Nokogiri::HTML.fragment(html)
|
21
|
+
|
22
|
+
dom.css('#missions li').map do |mission_dom|
|
23
|
+
links = mission_dom.css('a')
|
24
|
+
|
25
|
+
attributes = {
|
26
|
+
"title" => links.first.text.strip,
|
27
|
+
"url" => URI.join(url, links.first.attr('href')).to_s,
|
28
|
+
"description" => mission_dom.css('div').first.children[2].text.gsub('→', '').strip,
|
29
|
+
"users_count" => links.last.text[/(\d+)/, 1].to_i
|
30
|
+
}
|
31
|
+
|
32
|
+
new(attributes)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Fetch mission
|
39
|
+
#
|
40
|
+
def fetch
|
41
|
+
if (self.url)
|
42
|
+
html = URI.open(self.url).read
|
43
|
+
dom = Nokogiri::HTML.fragment(html)
|
44
|
+
|
45
|
+
description = dom.css('#description').first
|
46
|
+
|
47
|
+
# Remove "All missions" link
|
48
|
+
users = dom.css('#main_column a')[0..-2].map do |user_link|
|
49
|
+
User.new(
|
50
|
+
"user_id" => user_link.text.strip,
|
51
|
+
"url" => URI.join(self.url, user_link.attr('href')).to_s,
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
mission = {
|
56
|
+
"title" => dom.css('h1').first.text.strip,
|
57
|
+
"description" => description.text.strip,
|
58
|
+
"description_long" => description.next.text.strip,
|
59
|
+
|
60
|
+
"users" => users,
|
61
|
+
"users_count" => users.count
|
62
|
+
}
|
63
|
+
|
64
|
+
self.merge!(mission)
|
65
|
+
end
|
66
|
+
|
67
|
+
self
|
68
|
+
end
|
69
|
+
alias_method :reload, :fetch
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'hashie'
|
2
|
+
|
3
|
+
module Openfoodfacts
|
4
|
+
class NumberOfIngredients < Hashie::Mash
|
5
|
+
|
6
|
+
# TODO: Add more locales
|
7
|
+
LOCALE_PATHS = {
|
8
|
+
'fr' => 'nombres-d-ingredients',
|
9
|
+
'uk' => 'numbers-of-ingredients',
|
10
|
+
'us' => 'numbers-of-ingredients',
|
11
|
+
'world' => 'numbers-of-ingredients'
|
12
|
+
}
|
13
|
+
|
14
|
+
class << self
|
15
|
+
|
16
|
+
# Get last edit dates
|
17
|
+
#
|
18
|
+
def all(locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
|
19
|
+
if path = LOCALE_PATHS[locale]
|
20
|
+
Product.tags_from_page(self, "https://#{locale}.#{domain}/#{path}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
# Get products with last edit date
|
27
|
+
#
|
28
|
+
def products(page: -1)
|
29
|
+
Product.from_website_page(url, page: page, products_count: products_count) if url
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'hashie'
|
2
|
+
|
3
|
+
module Openfoodfacts
|
4
|
+
class NutritionGrade < Hashie::Mash
|
5
|
+
|
6
|
+
# TODO: Add more locales
|
7
|
+
LOCALE_PATHS = {
|
8
|
+
'fr' => 'notes-nutritionnelles',
|
9
|
+
'uk' => 'nutrition-grades',
|
10
|
+
'us' => 'nutrition-grades',
|
11
|
+
'world' => 'nutrition-grades'
|
12
|
+
}
|
13
|
+
|
14
|
+
class << self
|
15
|
+
|
16
|
+
# Get nutrition grades
|
17
|
+
#
|
18
|
+
def all(locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
|
19
|
+
if path = LOCALE_PATHS[locale]
|
20
|
+
Product.tags_from_page(self, "https://#{locale}.#{domain}/#{path}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
# Get products with nutrition grade
|
27
|
+
#
|
28
|
+
def products(page: -1)
|
29
|
+
Product.from_website_page(url, page: page, products_count: products_count) if url
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
data/lib/openfoodfacts/origin.rb
CHANGED
@@ -15,9 +15,9 @@ module Openfoodfacts
|
|
15
15
|
|
16
16
|
# Get origins
|
17
17
|
#
|
18
|
-
def all(locale:
|
18
|
+
def all(locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
|
19
19
|
if path = LOCALE_PATHS[locale]
|
20
|
-
Product.tags_from_page(self, "
|
20
|
+
Product.tags_from_page(self, "https://#{locale}.#{domain}/#{path}")
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
@@ -30,4 +30,4 @@ module Openfoodfacts
|
|
30
30
|
end
|
31
31
|
|
32
32
|
end
|
33
|
-
end
|
33
|
+
end
|
@@ -15,9 +15,9 @@ module Openfoodfacts
|
|
15
15
|
|
16
16
|
# Get packager codes
|
17
17
|
#
|
18
|
-
def all(locale:
|
18
|
+
def all(locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
|
19
19
|
if path = LOCALE_PATHS[locale]
|
20
|
-
Product.tags_from_page(self, "
|
20
|
+
Product.tags_from_page(self, "https://#{locale}.#{domain}/#{path}")
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
@@ -30,4 +30,4 @@ module Openfoodfacts
|
|
30
30
|
end
|
31
31
|
|
32
32
|
end
|
33
|
-
end
|
33
|
+
end
|
@@ -15,9 +15,9 @@ module Openfoodfacts
|
|
15
15
|
|
16
16
|
# Get packagings
|
17
17
|
#
|
18
|
-
def all(locale:
|
18
|
+
def all(locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
|
19
19
|
if path = LOCALE_PATHS[locale]
|
20
|
-
Product.tags_from_page(self, "
|
20
|
+
Product.tags_from_page(self, "https://#{locale}.#{domain}/#{path}")
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
@@ -30,4 +30,4 @@ module Openfoodfacts
|
|
30
30
|
end
|
31
31
|
|
32
32
|
end
|
33
|
-
end
|
33
|
+
end
|
data/lib/openfoodfacts/press.rb
CHANGED
@@ -8,44 +8,34 @@ module Openfoodfacts
|
|
8
8
|
|
9
9
|
# TODO: Add more locales
|
10
10
|
LOCALE_PATHS = {
|
11
|
-
'fr' => 'presse'
|
12
|
-
'uk' => 'press',
|
13
|
-
'us' => 'press',
|
14
|
-
'world' => 'press'
|
11
|
+
'fr' => 'revue-de-presse-fr'
|
15
12
|
}
|
16
13
|
|
17
14
|
LOCALE_DATE_FORMATS = {
|
18
|
-
'fr' => '%d/%m/%Y'
|
19
|
-
'uk' => '%m/%d/%Y',
|
20
|
-
'us' => '%m/%d/%Y',
|
21
|
-
'world' => '%m/%d/%Y'
|
15
|
+
'fr' => '%d/%m/%Y'
|
22
16
|
}
|
23
17
|
|
24
18
|
class << self
|
25
|
-
def items(locale:
|
19
|
+
def items(locale: 'fr', domain: DEFAULT_DOMAIN)
|
26
20
|
if path = LOCALE_PATHS[locale]
|
27
|
-
|
21
|
+
date_format = LOCALE_DATE_FORMATS[locale]
|
22
|
+
|
23
|
+
html = URI.open("https://#{locale}.#{domain}/#{path}").read
|
28
24
|
dom = Nokogiri::HTML.fragment(html)
|
29
|
-
|
30
|
-
titles = dom.css('#
|
25
|
+
|
26
|
+
titles = dom.css('#press_table tbody tr')
|
31
27
|
titles.each_with_index.map do |item, index|
|
32
|
-
|
28
|
+
colums = item.css('td')
|
33
29
|
|
34
|
-
link =
|
30
|
+
link = colums[1].css('a')
|
35
31
|
attributes = {
|
36
|
-
"
|
37
|
-
"
|
32
|
+
"type" => colums[0].text,
|
33
|
+
"title" => colums[1].text.strip,
|
34
|
+
"url" => link && link.attr('href') && link.attr('href').value,
|
35
|
+
"source" => colums[2].text.strip,
|
36
|
+
"date" => (DateTime.strptime(colums[3].text, date_format) rescue nil)
|
38
37
|
}
|
39
38
|
|
40
|
-
last = Nokogiri::HTML.fragment(data.last)
|
41
|
-
if date_format = LOCALE_DATE_FORMATS[locale] and date = last.text.strip[/\d+\/\d+\/\d+\z/, 0]
|
42
|
-
attributes["date"] = DateTime.strptime(date, date_format)
|
43
|
-
end
|
44
|
-
|
45
|
-
if data.length >= 3
|
46
|
-
attributes["source"] = Nokogiri::HTML.fragment(data[-2]).text.strip
|
47
|
-
end
|
48
|
-
|
49
39
|
new(attributes)
|
50
40
|
end
|
51
41
|
end
|
@@ -53,4 +43,4 @@ module Openfoodfacts
|
|
53
43
|
end
|
54
44
|
|
55
45
|
end
|
56
|
-
end
|
46
|
+
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'cgi'
|
1
2
|
require 'hashie'
|
2
3
|
require 'net/http'
|
3
4
|
require 'nokogiri'
|
@@ -16,23 +17,12 @@ module Openfoodfacts
|
|
16
17
|
|
17
18
|
class << self
|
18
19
|
|
19
|
-
# Products from API
|
20
|
-
#
|
21
|
-
def from_api(api_url, page: -1)
|
22
|
-
if url
|
23
|
-
body = open(api_url).read
|
24
|
-
json = JSON.parse(body)
|
25
|
-
json_products = json['products']
|
26
|
-
json_products.map(&method(:new))
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
20
|
# Get product
|
31
21
|
#
|
32
|
-
def get(code, locale:
|
22
|
+
def get(code, locale: DEFAULT_LOCALE)
|
33
23
|
if code
|
34
24
|
product_url = url(code, locale: locale)
|
35
|
-
json = open(product_url).read
|
25
|
+
json = URI.open(product_url).read
|
36
26
|
hash = JSON.parse(json)
|
37
27
|
|
38
28
|
new(hash["product"]) if !hash["status"].nil? && hash["status"] == 1
|
@@ -42,16 +32,20 @@ module Openfoodfacts
|
|
42
32
|
|
43
33
|
# Return product API URL
|
44
34
|
#
|
45
|
-
def url(code, locale:
|
46
|
-
|
35
|
+
def url(code, locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
|
36
|
+
if code
|
37
|
+
path = "api/v0/produit/#{code}.json"
|
38
|
+
"https://#{locale}.#{domain}/#{path}"
|
39
|
+
end
|
47
40
|
end
|
48
41
|
|
49
42
|
# Search products
|
50
43
|
#
|
51
|
-
def search(terms, locale:
|
52
|
-
terms =
|
53
|
-
|
54
|
-
|
44
|
+
def search(terms, locale: DEFAULT_LOCALE, page: 1, page_size: 20, sort_by: 'unique_scans_n', domain: DEFAULT_DOMAIN)
|
45
|
+
terms = CGI.escape(terms)
|
46
|
+
path = "cgi/search.pl?search_terms=#{terms}&jqm=1&page=#{page}&page_size=#{page_size}&sort_by=#{sort_by}"
|
47
|
+
url = "https://#{locale}.#{domain}/#{path}"
|
48
|
+
json = URI.open(url).read
|
55
49
|
hash = JSON.parse(json)
|
56
50
|
html = hash["jqm"]
|
57
51
|
|
@@ -85,11 +79,11 @@ module Openfoodfacts
|
|
85
79
|
end
|
86
80
|
|
87
81
|
def from_jquery_mobile_list(jqm_html)
|
88
|
-
from_html_list(jqm_html, 'ul li:not(#loadmore)', /code=(\d+)\Z/i)
|
82
|
+
from_html_list(jqm_html, 'ul#search_results_list li:not(#loadmore)', /code=(\d+)\Z/i)
|
89
83
|
end
|
90
84
|
|
91
85
|
def from_website_list(html, locale: 'world')
|
92
|
-
from_html_list(html, 'ul.products li', /\/(\d+)
|
86
|
+
from_html_list(html, 'ul.products li', /\/(\d+)\/?/i, locale: 'world')
|
93
87
|
end
|
94
88
|
|
95
89
|
# page -1 to fetch all pages
|
@@ -97,7 +91,7 @@ module Openfoodfacts
|
|
97
91
|
if page == -1
|
98
92
|
if products_count # Avoid one call
|
99
93
|
pages_count = (products_count.to_f / 20).ceil
|
100
|
-
(1..pages_count).map { |
|
94
|
+
(1..pages_count).map { |page_i| from_website_page(page_url, page: page_i) }.flatten
|
101
95
|
else
|
102
96
|
products = []
|
103
97
|
|
@@ -111,13 +105,13 @@ module Openfoodfacts
|
|
111
105
|
products
|
112
106
|
end
|
113
107
|
else
|
114
|
-
html = open("#{page_url}/#{page}").read
|
108
|
+
html = URI.open("#{page_url}/#{page}").read
|
115
109
|
from_website_list(html, locale: Locale.locale_from_link(page_url))
|
116
110
|
end
|
117
111
|
end
|
118
112
|
|
119
113
|
def tags_from_page(_klass, page_url, &custom_tag_parsing)
|
120
|
-
html = open(page_url).read
|
114
|
+
html = URI.open(page_url).read
|
121
115
|
dom = Nokogiri::HTML.fragment(html)
|
122
116
|
|
123
117
|
dom.css('table#tagstable tbody tr').map do |tag|
|
@@ -154,10 +148,11 @@ module Openfoodfacts
|
|
154
148
|
# User can be nil
|
155
149
|
# Tested not updatable fields: countries, ingredients_text, purchase_places, purchase_places_tag, purchase_places_tags
|
156
150
|
#
|
157
|
-
def update(user: nil)
|
151
|
+
def update(user: nil, domain: DEFAULT_DOMAIN)
|
158
152
|
if self.code && self.lc
|
159
153
|
subdomain = self.lc == 'world' ? 'world' : "world-#{self.lc}"
|
160
|
-
|
154
|
+
path = 'cgi/product_jqm.pl'
|
155
|
+
uri = URI("https://#{subdomain}.#{domain}/#{path}")
|
161
156
|
params = self.to_hash
|
162
157
|
params.merge!("user_id" => user.user_id, "password" => user.password) if user
|
163
158
|
response = Net::HTTP.post_form(uri, params)
|
@@ -172,17 +167,18 @@ module Openfoodfacts
|
|
172
167
|
|
173
168
|
# Return Product API URL
|
174
169
|
#
|
175
|
-
def url(locale:
|
170
|
+
def url(locale: DEFAULT_LOCALE)
|
176
171
|
self.class.url(self.code, locale: locale)
|
177
172
|
end
|
178
173
|
|
179
174
|
# Return Product web URL according to locale
|
180
175
|
#
|
181
|
-
def weburl(locale: nil)
|
182
|
-
locale ||= self.lc ||
|
176
|
+
def weburl(locale: nil, domain: DEFAULT_DOMAIN)
|
177
|
+
locale ||= self.lc || DEFAULT_LOCALE
|
183
178
|
|
184
179
|
if self.code && prefix = LOCALE_WEBURL_PREFIXES[locale]
|
185
|
-
"
|
180
|
+
path = "#{prefix}/#{self.code}"
|
181
|
+
"https://#{locale}.#{domain}/#{path}"
|
186
182
|
end
|
187
183
|
end
|
188
184
|
|
@@ -15,9 +15,9 @@ module Openfoodfacts
|
|
15
15
|
|
16
16
|
# Get product states
|
17
17
|
#
|
18
|
-
def all(locale:
|
18
|
+
def all(locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
|
19
19
|
if path = LOCALE_PATHS[locale]
|
20
|
-
Product.tags_from_page(self, "
|
20
|
+
Product.tags_from_page(self, "https://#{locale}.#{domain}/#{path}")
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
@@ -30,4 +30,4 @@ module Openfoodfacts
|
|
30
30
|
end
|
31
31
|
|
32
32
|
end
|
33
|
-
end
|
33
|
+
end
|
@@ -15,9 +15,9 @@ module Openfoodfacts
|
|
15
15
|
|
16
16
|
# Get purchase places
|
17
17
|
#
|
18
|
-
def all(locale:
|
18
|
+
def all(locale: DEFAULT_LOCALE, domain: DEFAULT_DOMAIN)
|
19
19
|
if path = LOCALE_PATHS[locale]
|
20
|
-
Product.tags_from_page(self, "
|
20
|
+
Product.tags_from_page(self, "https://#{locale}.#{domain}/#{path}")
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
@@ -30,4 +30,4 @@ module Openfoodfacts
|
|
30
30
|
end
|
31
31
|
|
32
32
|
end
|
33
|
-
end
|
33
|
+
end
|