Merge pull request #11817 from rioug/10809-match-taxons-to-DFC-product-types

[DFC] match taxons to dfc product types
This commit is contained in:
Konrad
2024-01-25 00:05:31 +01:00
committed by GitHub
16 changed files with 298 additions and 74 deletions

View File

@@ -117,8 +117,8 @@ module Spree
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

View File

@@ -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

View File

@@ -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"

View File

@@ -0,0 +1,5 @@
class AddDfcNameToSpreeTaxons < ActiveRecord::Migration[7.0]
def change
add_column :spree_taxons, :dfc_id, :string
end
end

View File

@@ -881,6 +881,7 @@ ActiveRecord::Schema[7.0].define(version: 20231003000823494) do
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"

View File

@@ -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

View File

@@ -11,19 +11,13 @@ class SuppliedProductBuilder < DfcBuilder
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
@@ -47,7 +41,7 @@ class SuppliedProductBuilder < DfcBuilder
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
@@ -56,10 +50,24 @@ class SuppliedProductBuilder < DfcBuilder
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

View File

@@ -4,7 +4,7 @@ require_relative "skos_parser"
module DataFoodConsortium
module Connector
class Importer
class Importer # rubocop:disable Metrics/ClassLength
TYPES = [
DataFoodConsortium::Connector::CatalogItem,
DataFoodConsortium::Connector::Enterprise,
@@ -106,7 +106,7 @@ module DataFoodConsortium
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
@@ -128,10 +128,17 @@ module DataFoodConsortium
"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`.
@@ -141,6 +148,14 @@ module DataFoodConsortium
"#{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

View File

@@ -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,

View File

@@ -23,6 +23,7 @@ describe DataFoodConsortium::Connector::Importer do
name: "Tomato",
description: "Awesome tomato",
totalTheoreticalStock: 3,
productType: non_local_vegetable,
)
end
let(:product_data) do
@@ -36,26 +37,8 @@ describe DataFoodConsortium::Connector::Importer do
"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
@@ -65,7 +48,8 @@ describe DataFoodConsortium::Connector::Importer do
"@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",
@@ -74,7 +58,8 @@ describe DataFoodConsortium::Connector::Importer do
"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
@@ -96,6 +81,11 @@ describe DataFoodConsortium::Connector::Importer do
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)
@@ -105,6 +95,7 @@ describe DataFoodConsortium::Connector::Importer do
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
@@ -116,17 +107,7 @@ describe DataFoodConsortium::Connector::Importer do
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
@@ -138,6 +119,7 @@ describe DataFoodConsortium::Connector::Importer do
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
@@ -154,6 +136,7 @@ describe DataFoodConsortium::Connector::Importer do
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
@@ -164,6 +147,7 @@ describe DataFoodConsortium::Connector::Importer do
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

View File

@@ -17,6 +17,14 @@ describe "CatalogItems", type: :request, swagger_doc: "dfc.yaml",
: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") }

View File

@@ -29,6 +29,14 @@ describe "Enterprises", type: :request, swagger_doc: "dfc.yaml", rswag_autodoc:
: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") }

View File

@@ -11,9 +11,25 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml", rswag_auto
id: 90_000,
supplier: enterprise, name: "Pesto", description: "Basil Pesto",
variants: [variant],
primary_taxon: taxon
)
}
let(:variant) { build(:base_variant, id: 10_001, unit_value: 1) }
let(:taxon) {
build(
:taxon,
name: "Processed Vegetable",
dfc_id: "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#processed-vegetable"
)
}
let!(:non_local_vegetable) {
create(
:taxon,
name: "Non Local Vegetable",
dfc_id: "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#non-local-vegetable"
)
}
before { login_as user }
@@ -28,14 +44,7 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml", rswag_auto
parameter name: :supplied_product, in: :body, schema: {
example: {
'@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#",
'dfc-b:hasUnit': {
'@type': "@id"
},
},
'@context': "https://www.datafoodconsortium.org",
'@id': "http://test.host/api/dfc/enterprises/6201/supplied_products/0",
'@type': "dfc-b:SuppliedProduct",
'dfc-b:name': "Apple",
@@ -105,6 +114,7 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml", rswag_auto
product = Spree::Product.find(product_id)
expect(product.name).to eq "Apple"
expect(product.variants).to eq [variant]
expect(product.primary_taxon).to eq(non_local_vegetable)
# Creates a variant for existing product
supplied_product[:'ofn:spree_product_id'] = product_id
@@ -151,6 +161,7 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml", rswag_auto
run_test! do
expect(response.body).to include variant.name
expect(json_response["ofn:spree_product_id"]).to eq 90_000
expect(json_response["dfc-b:hasType"]).to eq("dfc-pt:processed-vegetable")
expect(json_response["ofn:image"]).to include("logo-white.png")
# Insert static value to keep documentation deterministic:
@@ -170,6 +181,14 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml", rswag_auto
end
put "Update SuppliedProduct" do
let!(:drink_taxon) {
create(
:taxon,
name: "Drink",
dfc_id: "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#drink"
)
}
consumes "application/json"
parameter name: :supplied_product, in: :body, schema: {
@@ -195,6 +214,7 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml", rswag_auto
}.to change { variant.description }.to("DFC-Pesto updated")
.and change { variant.display_name }.to("Pesto novo")
.and change { variant.unit_value }.to(17)
.and change { variant.product.primary_taxon }.to(drink_taxon)
end
end
end

View File

@@ -0,0 +1,49 @@
# frozen_string_literal: true
require_relative "../spec_helper"
describe DfcProductTypeFactory do
describe ".for" do
let(:dfc_id) {
"https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#drink"
}
it "assigns a top level product type" do
drink = DfcLoader.connector.PRODUCT_TYPES.DRINK
expect(described_class.for(dfc_id).semanticId).to eq drink.semanticId
end
context "with second level product type" do
let(:dfc_id) {
"https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#soft-drink"
}
it "assigns a second level product type" do
soft_drink = DfcLoader.connector.PRODUCT_TYPES.DRINK.SOFT_DRINK
expect(described_class.for(dfc_id).semanticId).to eq soft_drink.semanticId
end
end
context "with leaf level product type" do
let(:dfc_id) {
"https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#lemonade"
}
it "assigns a leaf level product type" do
lemonade = DfcLoader.connector.PRODUCT_TYPES.DRINK.SOFT_DRINK.LEMONADE
expect(described_class.for(dfc_id).semanticId).to eq lemonade.semanticId
end
end
context "with non existing product type" do
let(:dfc_id) { "other" }
it "returns nil" do
expect(described_class.for(dfc_id)).to be_nil
end
end
end
end

View File

@@ -7,7 +7,17 @@ describe SuppliedProductBuilder do
subject(:builder) { described_class }
let(:variant) {
build(:variant, id: 5).tap { |v| v.product.supplier_id = 7 }
build(:variant, id: 5).tap do |v|
v.product.supplier_id = 7
v.product.primary_taxon = taxon
end
}
let(:taxon) {
build(
:taxon,
name: "Soft Drink",
dfc_id: "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#soft-drink"
)
}
describe ".supplied_product" do
@@ -41,11 +51,22 @@ describe SuppliedProductBuilder do
expect(product.name).to eq "Apple - Granny Smith"
end
it "assigns a product type" do
product = builder.supplied_product(variant)
vegetable = DfcLoader.connector.PRODUCT_TYPES.VEGETABLE.NON_LOCAL_VEGETABLE
context "product_type mapping" do
subject(:product) { builder.supplied_product(variant) }
expect(product.productType).to eq vegetable
it "assigns a product type" do
soft_drink = DfcLoader.connector.PRODUCT_TYPES.DRINK.SOFT_DRINK
expect(product.productType).to eq soft_drink
end
context "when no taxon set" do
let(:taxon) { nil }
it "returns nil" do
expect(product.productType).to be_nil
end
end
end
it "assigns an image_url type" do
@@ -59,4 +80,54 @@ describe SuppliedProductBuilder do
expect(product.image).to eq variant.product.image.url(:product)
end
end
describe ".import_product" do
let(:supplied_product) do
DataFoodConsortium::Connector::SuppliedProduct.new(
"https://example.net/tomato",
name: "Tomato",
description: "Awesome tomato",
quantity: DataFoodConsortium::Connector::QuantitativeValue.new(
unit: DfcLoader.connector.MEASURES.KILOGRAM,
value: 2,
),
productType: product_type,
)
end
let(:product_type) { DfcLoader.connector.PRODUCT_TYPES.VEGETABLE.NON_LOCAL_VEGETABLE }
let!(:taxon) {
create(
:taxon,
name: "Non local vegetable",
dfc_id: "https://github.com/datafoodconsortium/taxonomies/releases/latest/download/productTypes.rdf#non-local-vegetable"
)
}
it "creates a new Spree::Product" do
product = builder.import_product(supplied_product)
expect(product).to be_a(Spree::Product)
expect(product.name).to eq("Tomato")
expect(product.description).to eq("Awesome tomato")
expect(product.variant_unit).to eq("weight")
end
describe "taxon" do
it "assigns the taxon matching the DFC product type" do
product = builder.import_product(supplied_product)
expect(product.primary_taxon).to eq(taxon)
end
describe "when no matching taxon" do
let(:product_type) { DfcLoader.connector.PRODUCT_TYPES.DRINK }
it "set the taxon to nil" do
product = builder.import_product(supplied_product)
expect(product.primary_taxon).to be_nil
end
end
end
end
end

View File

@@ -553,12 +553,7 @@ paths:
application/json:
schema:
example:
"@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#
dfc-b:hasUnit:
"@type": "@id"
"@context": https://www.datafoodconsortium.org
"@id": http://test.host/api/dfc/enterprises/6201/supplied_products/0
"@type": dfc-b:SuppliedProduct
dfc-b:name: Apple
@@ -602,7 +597,7 @@ paths:
"@type": dfc-b:SuppliedProduct
dfc-b:name: Pesto - 1g
dfc-b:description: Basil Pesto
dfc-b:hasType: dfc-pt:non-local-vegetable
dfc-b:hasType: dfc-pt:processed-vegetable
dfc-b:hasQuantity:
"@type": dfc-b:QuantitativeValue
dfc-b:hasUnit: dfc-m:Gram
@@ -698,7 +693,7 @@ paths:
"@type": dfc-b:QuantitativeValue
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