Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DFC] match taxons to dfc product types #11817

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
83beafc
Add missing translation on taxon admin form
rioug Oct 27, 2023
652c7a5
Add dfc_name field on Spree::Taxons
rioug Oct 27, 2023
403386e
Add basic match on the root level Product Types
rioug Oct 31, 2023
8014aa4
Update test to check for Product Type
rioug Oct 31, 2023
42e2141
Match product taxon with DFC Product type
rioug Dec 8, 2023
4d52e16
Update taxon to not be a top concept product type
rioug Dec 11, 2023
a2cb1f4
Update swagger documentation
rioug Dec 14, 2023
2be1aea
SKOS parser, user shorten version for product types URI
rioug Dec 14, 2023
476f3b8
Fix DFC importer to support Product types
rioug Dec 15, 2023
e917b26
Support non "underscore" name for dfc_name
rioug Dec 15, 2023
47cea0a
Add matching DFC product type to taxon when importing product
rioug Dec 15, 2023
a35e896
Add taxon to request spec
rioug Dec 15, 2023
25a820f
Allow updating of product type
rioug Dec 15, 2023
162fd4b
update documentation
rioug Dec 15, 2023
8013fac
Per review, match on the product type URI instead of name
rioug Dec 18, 2023
d8b56d5
Per review, match on the product type URI instead of name part 2
rioug Dec 18, 2023
c40ccb8
Refactor, move product type matching to DfcProductTypeFactory
rioug Dec 22, 2023
c1e7aa6
Small improvment for Importer
rioug Dec 22, 2023
9607739
Per review, store product type instead of path
rioug Dec 22, 2023
2aa0ab1
Simplify traversing of Product Types
rioug Dec 22, 2023
35da321
Fix skos parser to not replace Product type URI by dfc-pt
rioug Jan 4, 2024
28e17af
Fix DFC context in spec to resolve product types
mkllnk Jan 5, 2024
96a0100
Fix admin view to use dfc_id
rioug Jan 15, 2024
36b3b3c
Switch to comparing product type `semanticId`
rioug Jan 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/controllers/spree/admin/taxons_controller.rb
Expand Up @@ -117,8 +117,8 @@ def destroy

def taxon_params
params.require(:taxon).permit(
:name, :parent_id, :position, :icon, :description, :permalink,
:taxonomy_id, :meta_description, :meta_keywords, :meta_title
:name, :parent_id, :position, :icon, :description, :permalink, :taxonomy_id,
:meta_description, :meta_keywords, :meta_title, :dfc_id
)
end
end
Expand Down
16 changes: 10 additions & 6 deletions app/views/spree/admin/taxons/_form.html.haml
@@ -1,31 +1,35 @@
.row
.alpha.five.columns
= f.field_container :name do
= f.label :name, t(:name)
= f.label :name, t(".name")
%span.required *
%br/
= error_message_on :taxon, :name, class: 'fullwidth title'
= text_field :taxon, :name, class: 'fullwidth'
= f.field_container :permalink_part do
= f.label :permalink_part, t(:permalink)
= f.label :permalink_part, t(".permalink")
%span.required *
%br/
= @taxon.permalink.split("/")[0...-1].join("/") + "/"
= text_field_tag :permalink_part, @permalink_part
= f.field_container :meta_title do
= f.label :meta_title, t(:meta_title)
= f.label :meta_title, t(".meta_title")
%br/
= f.text_field :meta_title, class: 'fullwidth', rows: 6
= f.field_container :meta_description do
= f.label :meta_description, t(:meta_description)
= f.label :meta_description, t(".meta_description")
%br/
= f.text_field :meta_description, class: 'fullwidth', rows: 6
= f.field_container :meta_description do
= f.label :meta_keywords, t(:meta_keywords)
= f.label :meta_keywords, t(".meta_keywords")
%br/
= f.text_field :meta_keywords, class: 'fullwidth', rows: 6
= f.field_container :dfc_id do
= f.label :dfc_id, t(".dfc_id")
%br/
= f.text_field :dfc_id, class: 'fullwidth', rows: 6
.omega.seven.columns
= f.field_container :description do
= f.label :description, t(:description)
= f.label :description, t(".description")
%br/
= f.text_area :description, class: 'fullwidth', rows: 6
9 changes: 9 additions & 0 deletions config/locales/en.yml
Expand Up @@ -4479,6 +4479,15 @@ See the %{link} to find out more about %{sitename}'s features and to start using
email: "Email"
total: "Total"
billing_address_name: "Name"
taxons:
form:
name: Name
permalink: Permalink
meta_title: Meta Title
meta_description: Meta Description
meta_keywords: Meta Keywords
description: Description
dfc_id: DFC URI
general_settings:
edit:
legal_settings: "Legal Settings"
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20231027041224_add_dfc_name_to_spree_taxons.rb
@@ -0,0 +1,5 @@
class AddDfcNameToSpreeTaxons < ActiveRecord::Migration[7.0]
def change
add_column :spree_taxons, :dfc_id, :string
end
end
1 change: 1 addition & 0 deletions db/schema.rb
Expand Up @@ -881,6 +881,7 @@
t.string "meta_title", limit: 255
t.string "meta_description", limit: 255
t.string "meta_keywords", limit: 255
t.string "dfc_id"
t.index ["parent_id"], name: "index_taxons_on_parent_id"
t.index ["permalink"], name: "index_taxons_on_permalink"
t.index ["taxonomy_id"], name: "index_taxons_on_taxonomy_id"
Expand Down
47 changes: 47 additions & 0 deletions engines/dfc_provider/app/services/dfc_product_type_factory.rb
@@ -0,0 +1,47 @@
# frozen_string_literal: true

require 'singleton'

class DfcProductTypeFactory
include Singleton

def self.for(dfc_id)
instance.for(dfc_id)
end

def initialize
@product_types = {}

populate_product_types
end

def for(dfc_id)
@product_types[dfc_id]
end

private

def populate_product_types
DfcLoader.connector.PRODUCT_TYPES.topConcepts.each do |product_type|
record_type(DfcLoader.connector.PRODUCT_TYPES, product_type.to_s)
end
end

def record_type(product_type_object, product_type)
current_product_type = product_type_object.public_send(product_type.to_s)

id = current_product_type.semanticId
@product_types[id] = current_product_type

# Narrower product types are defined as class method on the current product type object
narrowers = current_product_type.methods(false).sort

# Leaf node
return if narrowers.empty?

narrowers.each do |narrower|
# recursive call
record_type(current_product_type, narrower)
end
end
end
24 changes: 16 additions & 8 deletions engines/dfc_provider/app/services/supplied_product_builder.rb
Expand Up @@ -11,19 +11,13 @@ def self.supplied_product(variant)
id,
name: variant.product_and_full_name,
description: variant.description,
productType: product_type,
productType: product_type(variant),
quantity: QuantitativeValueBuilder.quantity(variant),
spree_product_id: variant.product.id,
image_url: variant.product&.image&.url(:product)
)
end

# OFN product categories (taxons) are currently not standardised.
# This is just a dummy value for demos.
def self.product_type
DfcLoader.connector.PRODUCT_TYPES.VEGETABLE.NON_LOCAL_VEGETABLE
end

def self.import_variant(supplied_product)
product_id = supplied_product.spree_product_id

Expand All @@ -47,7 +41,7 @@ def self.import_product(supplied_product)
name: supplied_product.name,
description: supplied_product.description,
price: 0, # will be in DFC Offer
primary_taxon: Spree::Taxon.first, # dummy value until we have a mapping
primary_taxon: taxon(supplied_product)
).tap do |product|
QuantitativeValueBuilder.apply(supplied_product.quantity, product)
end
Expand All @@ -56,10 +50,24 @@ def self.import_product(supplied_product)
def self.apply(supplied_product, variant)
variant.product.assign_attributes(
description: supplied_product.description,
primary_taxon: taxon(supplied_product)
)

variant.display_name = supplied_product.name
QuantitativeValueBuilder.apply(supplied_product.quantity, variant.product)
variant.unit_value = variant.product.unit_value
end

def self.product_type(variant)
taxon_dfc_id = variant.product.primary_taxon&.dfc_id

DfcProductTypeFactory.for(taxon_dfc_id)
end

def self.taxon(supplied_product)
dfc_id = supplied_product.productType.semanticId
Spree::Taxon.find_by(dfc_id: )
end

private_class_method :product_type, :taxon
end
Expand Up @@ -4,7 +4,7 @@

module DataFoodConsortium
module Connector
class Importer
class Importer # rubocop:disable Metrics/ClassLength
TYPES = [
DataFoodConsortium::Connector::CatalogItem,
DataFoodConsortium::Connector::Enterprise,
Expand Down Expand Up @@ -106,7 +106,7 @@ def apply_statement(statement)
if property.value.is_a?(Enumerable)
property.value << value
else
setter = guess_setter_name(statement.predicate)
setter = guess_setter_name(statement)
subject.try(setter, value) if setter
end
end
Expand All @@ -128,10 +128,17 @@ def skos_concept(object)
"https://github.com/datafoodconsortium/taxonomies/releases/latest/download/measures.rdf#",
"dfc-m:"
)

SKOSParser.concepts[id]
end

def guess_setter_name(predicate)
def guess_setter_name(statement)
predicate = statement.predicate

# Ideally the product models would be consitent with the rule below and use "type"
# instead of "productType" but alast they are not so we need this exception
return "productType=" if predicate.fragment == "hasType" && product_type?(statement)

name =
# Some predicates are named like `hasQuantity`
# but the attribute name would be `quantity`.
Expand All @@ -141,6 +148,14 @@ def guess_setter_name(predicate)

"#{name}="
end

def product_type?(statement)
return true if statement.object.literal? && statement.object.value.match("dfc-pt")

return true if statement.object.path.match("productTypes")

false
end
end
end
end
Expand Up @@ -93,7 +93,7 @@
"dfc-b:hasUnit": "dfc-m:Piece",
"dfc-b:value": 17
},
"dfc-b:hasType": "dfc-pt:non-local-vegetable",
"dfc-b:hasType": "dfc-pt:drink",
"dfc-b:lifetime": "",
"dfc-b:name": "Pesto novo",
"dfc-b:totalTheoreticalStock": 0,
Expand Down
Expand Up @@ -23,6 +23,7 @@
name: "Tomato",
description: "Awesome tomato",
totalTheoreticalStock: 3,
productType: non_local_vegetable,
)
end
let(:product_data) do
Expand All @@ -36,26 +37,8 @@
"dfc-b:alcoholPercentage":0.0,
"dfc-b:lifetime":"",
"dfc-b:usageOrStorageCondition":"",
"dfc-b:totalTheoreticalStock":3
}
JSON
end
let(:product_data_with_context) do
<<~JSON
{
"@context": {
"dfc-b": "http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl#",
"dfc-m": "http://static.datafoodconsortium.org/data/measures.rdf#",
"dfc-pt": "http://static.datafoodconsortium.org/data/productTypes.rdf#"
},
"@id":"https://example.net/tomato",
"@type":"dfc-b:SuppliedProduct",
"dfc-b:name":"Tomato",
"dfc-b:description":"Awesome tomato",
"dfc-b:alcoholPercentage":0.0,
"dfc-b:lifetime":"",
"dfc-b:usageOrStorageCondition":"",
"dfc-b:totalTheoreticalStock":3
"dfc-b:totalTheoreticalStock":3,
"dfc-b:hasType": "dfc-pt:non-local-vegetable"
}
JSON
end
Expand All @@ -65,7 +48,8 @@
"@context": {
"dfc-b": "https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_BusinessOntology.owl#",
"dfc-m": "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/measures.rdf#",
"dfc-pt": "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#"
"dfc-pt": "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#",
"dfc-b:hasType":{"@type":"@id"}
},
"@id":"https://example.net/tomato",
"@type":"dfc-b:SuppliedProduct",
Expand All @@ -74,7 +58,8 @@
"dfc-b:alcoholPercentage":0.0,
"dfc-b:lifetime":"",
"dfc-b:usageOrStorageCondition":"",
"dfc-b:totalTheoreticalStock":3
"dfc-b:totalTheoreticalStock":3,
"dfc-b:hasType": "dfc-pt:non-local-vegetable"
}
JSON
end
Expand All @@ -96,6 +81,11 @@
end
connector.MEASURES.PIECE
end
let(:non_local_vegetable) do
connector.PRODUCT_TYPES.VEGETABLE.NON_LOCAL_VEGETABLE
end

before { connector.loadProductTypes(read_file("productTypes")) }

it "imports a single object with simple properties" do
result = import(product)
Expand All @@ -105,6 +95,7 @@
expect(result.semanticId).to eq "https://example.net/tomato"
expect(result.name).to eq "Tomato"
expect(result.description).to eq "Awesome tomato"
expect(result.productType).to eq non_local_vegetable
expect(result.totalTheoreticalStock).to eq 3
end

Expand All @@ -116,17 +107,7 @@
expect(result.semanticId).to eq "https://example.net/tomato"
expect(result.name).to eq "Tomato"
expect(result.description).to eq "Awesome tomato"
expect(result.totalTheoreticalStock).to eq 3
end

it "imports an object with included context" do
result = connector.import(product_data_with_context)

expect(result).to be_a DataFoodConsortium::Connector::SuppliedProduct
expect(result.semanticType).to eq "dfc-b:SuppliedProduct"
expect(result.semanticId).to eq "https://example.net/tomato"
expect(result.name).to eq "Tomato"
expect(result.description).to eq "Awesome tomato"
expect(result.productType).to eq non_local_vegetable
expect(result.totalTheoreticalStock).to eq 3
end

Expand All @@ -138,6 +119,7 @@
expect(result.semanticId).to eq "https://example.net/tomato"
expect(result.name).to eq "Tomato"
expect(result.description).to eq "Awesome tomato"
expect(result.productType).to eq non_local_vegetable
expect(result.totalTheoreticalStock).to eq 3
end

Expand All @@ -154,6 +136,7 @@
expect(item.semanticId).to eq "https://example.net/tomatoItem"
expect(tomato.name).to eq "Tomato"
expect(tomato.description).to eq "Awesome tomato"
expect(tomato.productType).to eq non_local_vegetable
expect(tomato.totalTheoreticalStock).to eq 3
end

Expand All @@ -164,6 +147,7 @@

expect(tomato.name).to eq "Tomato"
expect(tomato.quantity).to eq items
expect(tomato.productType).to eq non_local_vegetable
expect(items.value).to eq 5
expect(items.unit).to eq piece
end
Expand Down
8 changes: 8 additions & 0 deletions engines/dfc_provider/spec/requests/catalog_items_spec.rb
Expand Up @@ -17,6 +17,14 @@
:base_product,
id: 90_000, supplier: enterprise, name: "Apple", description: "Red",
variants: [variant],
primary_taxon: non_local_vegetable
)
}
let(:non_local_vegetable) {
build(
:taxon,
name: "Non Local Vegetable",
dfc_id: "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#non-local-vegetable"
)
}
let(:variant) { build(:base_variant, id: 10_001, unit_value: 1, sku: "AR") }
Expand Down
8 changes: 8 additions & 0 deletions engines/dfc_provider/spec/requests/enterprises_spec.rb
Expand Up @@ -29,6 +29,14 @@
:product_with_image,
id: 90_000, supplier: enterprise, name: "Apple", description: "Round",
variants: [variant],
primary_taxon: non_local_vegetable
)
}
let(:non_local_vegetable) {
build(
:taxon,
name: "Non Local Vegetable",
dfc_id: "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#non-local-vegetable"
)
}
let(:variant) { build(:base_variant, id: 10_001, unit_value: 1, sku: "APP") }
Expand Down