mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
Merge pull request #11378 from mkllnk/dfc-ofn-product-group-id
[DFC API] Add custom OFN product id to DFC SuppliedProduct
This commit is contained in:
@@ -39,6 +39,7 @@ Metrics/BlockLength:
|
||||
"put",
|
||||
"resource",
|
||||
"resources",
|
||||
"response",
|
||||
"scenario",
|
||||
"shared_examples",
|
||||
"shared_examples_for",
|
||||
|
||||
@@ -272,18 +272,6 @@ module Spree
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_units
|
||||
return unless saved_change_to_variant_unit? || saved_change_to_variant_unit_name?
|
||||
|
||||
variants.each(&:update_units)
|
||||
end
|
||||
|
||||
def touch_distributors
|
||||
Enterprise.distributing_products(id).each(&:touch)
|
||||
end
|
||||
|
||||
def ensure_standard_variant
|
||||
return unless variants.empty?
|
||||
|
||||
@@ -298,6 +286,18 @@ module Spree
|
||||
variants << variant
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_units
|
||||
return unless saved_change_to_variant_unit? || saved_change_to_variant_unit_name?
|
||||
|
||||
variants.each(&:update_units)
|
||||
end
|
||||
|
||||
def touch_distributors
|
||||
Enterprise.distributing_products(id).each(&:touch)
|
||||
end
|
||||
|
||||
def validate_image
|
||||
return if image.blank? || image.valid?
|
||||
|
||||
|
||||
@@ -14,13 +14,19 @@ module DfcProvider
|
||||
|
||||
return head :bad_request unless supplied_product
|
||||
|
||||
product = SuppliedProductBuilder.import(supplied_product)
|
||||
product.supplier = current_enterprise
|
||||
product.save!
|
||||
variant = SuppliedProductBuilder.import_variant(supplied_product)
|
||||
product = variant.product
|
||||
|
||||
supplied_product = SuppliedProductBuilder.supplied_product(
|
||||
product.variants.first
|
||||
)
|
||||
if product.new_record?
|
||||
product.supplier = current_enterprise
|
||||
product.save!
|
||||
end
|
||||
|
||||
if variant.new_record?
|
||||
variant.save!
|
||||
end
|
||||
|
||||
supplied_product = SuppliedProductBuilder.supplied_product(variant)
|
||||
render json: DfcIo.export(supplied_product)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Load our monkey-patches:
|
||||
require "data_food_consortium/connector/connector"
|
||||
|
||||
# Our interface to the DFC Connector library.
|
||||
module DfcIo
|
||||
# Serialise DFC Connector subjects as JSON-LD string.
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "data_food_consortium/connector/connector"
|
||||
|
||||
class DfcLoader
|
||||
def self.connector
|
||||
@connector ||= load_vocabularies
|
||||
|
||||
@@ -7,16 +7,35 @@ class SuppliedProductBuilder < DfcBuilder
|
||||
id: variant.id,
|
||||
)
|
||||
|
||||
DataFoodConsortium::Connector::SuppliedProduct.new(
|
||||
DfcProvider::SuppliedProduct.new(
|
||||
id,
|
||||
name: variant.name_to_display,
|
||||
description: variant.description,
|
||||
productType: product_type,
|
||||
quantity: QuantitativeValueBuilder.quantity(variant),
|
||||
spree_product_id: variant.product.id,
|
||||
)
|
||||
end
|
||||
|
||||
def self.import(supplied_product)
|
||||
def self.import_variant(supplied_product)
|
||||
product_id = supplied_product.spree_product_id
|
||||
|
||||
if product_id.present?
|
||||
product = Spree::Product.find(product_id)
|
||||
Spree::Variant.new(
|
||||
product:,
|
||||
price: 0,
|
||||
).tap do |variant|
|
||||
apply(supplied_product, variant)
|
||||
end
|
||||
else
|
||||
product = import_product(supplied_product)
|
||||
product.ensure_standard_variant
|
||||
product.variants.first
|
||||
end
|
||||
end
|
||||
|
||||
def self.import_product(supplied_product)
|
||||
Spree::Product.new(
|
||||
name: supplied_product.name,
|
||||
description: supplied_product.description,
|
||||
|
||||
@@ -15,21 +15,28 @@ module DataFoodConsortium
|
||||
].freeze
|
||||
|
||||
def self.type_map
|
||||
@type_map ||= TYPES.each_with_object({}) do |clazz, result|
|
||||
# Methods with variable arguments have a negative arity of -n-1
|
||||
# where n is the number of required arguments.
|
||||
number_of_required_args = -1 * (clazz.instance_method(:initialize).arity + 1)
|
||||
args = Array.new(number_of_required_args)
|
||||
type_uri = clazz.new(*args).semanticType
|
||||
result[type_uri] = clazz
|
||||
|
||||
# Add support for the old DFC v1.7 URLs:
|
||||
new_type_uri = type_uri.gsub(
|
||||
"https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_BusinessOntology.owl#",
|
||||
"http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl#"
|
||||
)
|
||||
result[new_type_uri] = clazz
|
||||
unless @type_map
|
||||
@type_map = {}
|
||||
TYPES.each(&method(:register_type))
|
||||
end
|
||||
|
||||
@type_map
|
||||
end
|
||||
|
||||
def self.register_type(clazz)
|
||||
# Methods with variable arguments have a negative arity of -n-1
|
||||
# where n is the number of required arguments.
|
||||
number_of_required_args = -1 * (clazz.instance_method(:initialize).arity + 1)
|
||||
args = Array.new(number_of_required_args)
|
||||
type_uri = clazz.new(*args).semanticType
|
||||
type_map[type_uri] = clazz
|
||||
|
||||
# Add support for the old DFC v1.7 URLs:
|
||||
new_type_uri = type_uri.gsub(
|
||||
"https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_BusinessOntology.owl#",
|
||||
"http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl#"
|
||||
)
|
||||
type_map[new_type_uri] = clazz
|
||||
end
|
||||
|
||||
def import(json_string_or_io)
|
||||
@@ -120,11 +127,12 @@ module DataFoodConsortium
|
||||
end
|
||||
|
||||
def guess_setter_name(predicate)
|
||||
fragment = predicate.fragment
|
||||
|
||||
# Some predicates are named like `hasQuantity`
|
||||
# but the attribute name would be `quantity`.
|
||||
name = fragment.sub(/^has/, "").camelize(:lower)
|
||||
name =
|
||||
# Some predicates are named like `hasQuantity`
|
||||
# but the attribute name would be `quantity`.
|
||||
predicate.fragment&.sub(/^has/, "")&.camelize(:lower) ||
|
||||
# And sometimes the URI looks like `ofn:spree_product_id`.
|
||||
predicate.to_s.split(":").last
|
||||
|
||||
"#{name}="
|
||||
end
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Load our monkey-patches of the DFC Connector:
|
||||
require "data_food_consortium/connector/connector"
|
||||
|
||||
# Our Rails engine
|
||||
require "dfc_provider/engine"
|
||||
|
||||
# Custom data types
|
||||
require "dfc_provider/supplied_product"
|
||||
|
||||
module DfcProvider
|
||||
DataFoodConsortium::Connector::Importer.register_type(SuppliedProduct)
|
||||
end
|
||||
|
||||
17
engines/dfc_provider/lib/dfc_provider/supplied_product.rb
Normal file
17
engines/dfc_provider/lib/dfc_provider/supplied_product.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DfcProvider
|
||||
class SuppliedProduct < DataFoodConsortium::Connector::SuppliedProduct
|
||||
attr_accessor :spree_product_id
|
||||
|
||||
def initialize(semantic_id, spree_product_id: nil, **properties)
|
||||
super(semantic_id, **properties)
|
||||
|
||||
self.spree_product_id = spree_product_id
|
||||
|
||||
registerSemanticProperty("ofn:spree_product_id") do
|
||||
self.spree_product_id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -21,7 +21,7 @@ describe DataFoodConsortium::Connector::Connector, vcr: true do
|
||||
it "imports" do
|
||||
json = connector.export(product)
|
||||
result = connector.import(json)
|
||||
expect(result.class).to eq product.class
|
||||
expect(result).to be_a product.class
|
||||
expect(result.semanticType).to eq product.semanticType
|
||||
expect(result.semanticId).to eq "https://example.net/tomato"
|
||||
expect(result.name).to eq "Tomato"
|
||||
@@ -32,7 +32,7 @@ describe DataFoodConsortium::Connector::Connector, vcr: true do
|
||||
io = StringIO.new(json)
|
||||
result = connector.import(io)
|
||||
|
||||
expect(result.class).to eq product.class
|
||||
expect(result).to be_a product.class
|
||||
expect(result.semanticType).to eq product.semanticType
|
||||
expect(result.semanticId).to eq "https://example.net/tomato"
|
||||
expect(result.name).to eq "Tomato"
|
||||
|
||||
@@ -100,7 +100,7 @@ describe DataFoodConsortium::Connector::Importer do
|
||||
it "imports a single object with simple properties" do
|
||||
result = import(product)
|
||||
|
||||
expect(result.class).to eq product.class
|
||||
expect(result).to be_a product.class
|
||||
expect(result.semanticType).to eq product.semanticType
|
||||
expect(result.semanticId).to eq "https://example.net/tomato"
|
||||
expect(result.name).to eq "Tomato"
|
||||
@@ -111,7 +111,7 @@ describe DataFoodConsortium::Connector::Importer do
|
||||
it "imports an object with referenced context" do
|
||||
result = connector.import(product_data)
|
||||
|
||||
expect(result.class).to eq DataFoodConsortium::Connector::SuppliedProduct
|
||||
expect(result).to be_a DataFoodConsortium::Connector::SuppliedProduct
|
||||
expect(result.semanticType).to eq "https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_BusinessOntology.owl#SuppliedProduct"
|
||||
expect(result.semanticId).to eq "https://example.net/tomato"
|
||||
expect(result.name).to eq "Tomato"
|
||||
@@ -122,7 +122,7 @@ describe DataFoodConsortium::Connector::Importer do
|
||||
it "imports an object with included context" do
|
||||
result = connector.import(product_data_with_context)
|
||||
|
||||
expect(result.class).to eq DataFoodConsortium::Connector::SuppliedProduct
|
||||
expect(result).to be_a DataFoodConsortium::Connector::SuppliedProduct
|
||||
expect(result.semanticType).to eq "https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_BusinessOntology.owl#SuppliedProduct"
|
||||
expect(result.semanticId).to eq "https://example.net/tomato"
|
||||
expect(result.name).to eq "Tomato"
|
||||
@@ -133,7 +133,7 @@ describe DataFoodConsortium::Connector::Importer do
|
||||
it "imports an object with DFC v1.8 context" do
|
||||
result = connector.import(product_data_with_context_v1_8)
|
||||
|
||||
expect(result.class).to eq DataFoodConsortium::Connector::SuppliedProduct
|
||||
expect(result).to be_a DataFoodConsortium::Connector::SuppliedProduct
|
||||
expect(result.semanticType).to eq "https://github.com/datafoodconsortium/ontology/releases/latest/download/DFC_BusinessOntology.owl#SuppliedProduct"
|
||||
expect(result.semanticId).to eq "https://example.net/tomato"
|
||||
expect(result.name).to eq "Tomato"
|
||||
@@ -149,7 +149,7 @@ describe DataFoodConsortium::Connector::Importer do
|
||||
|
||||
item, tomato = result
|
||||
|
||||
expect(item.class).to eq catalog_item.class
|
||||
expect(item).to be_a catalog_item.class
|
||||
expect(item.semanticType).to eq catalog_item.semanticType
|
||||
expect(item.semanticId).to eq "https://example.net/tomatoItem"
|
||||
expect(tomato.name).to eq "Tomato"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require DfcProvider::Engine.root.join("spec/swagger_helper")
|
||||
require_relative "../swagger_helper"
|
||||
|
||||
describe "Addresses", type: :request, swagger_doc: "dfc.yaml", rswag_autodoc: true do
|
||||
let(:user) { create(:oidc_user) }
|
||||
|
||||
@@ -15,7 +15,7 @@ describe "CatalogItems", type: :request, swagger_doc: "dfc.yaml",
|
||||
let(:product) {
|
||||
create(
|
||||
:base_product,
|
||||
supplier: enterprise, name: "Apple", description: "Red",
|
||||
id: 90_000, supplier: enterprise, name: "Apple", description: "Red",
|
||||
variants: [variant],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ describe "Enterprises", type: :request, swagger_doc: "dfc.yaml", rswag_autodoc:
|
||||
let!(:product) {
|
||||
create(
|
||||
:base_product,
|
||||
supplier: enterprise, name: "Apple", description: "Round",
|
||||
id: 90_000, supplier: enterprise, name: "Apple", description: "Round",
|
||||
variants: [variant],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml",
|
||||
let!(:product) {
|
||||
create(
|
||||
:base_product,
|
||||
id: 90_000,
|
||||
supplier: enterprise, name: "Pesto", description: "Basil Pesto",
|
||||
variants: [variant],
|
||||
)
|
||||
@@ -78,7 +79,7 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml",
|
||||
example.metadata[:operation][:parameters].first[:schema][:example]
|
||||
end
|
||||
|
||||
it "creates a variant" do |example|
|
||||
it "creates a product and variant" do |example|
|
||||
expect { submit_request(example.metadata) }
|
||||
.to change { enterprise.supplied_products.count }.by(1)
|
||||
|
||||
@@ -87,16 +88,44 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml",
|
||||
%r|^http://test\.host/api/dfc/enterprises/10000/supplied_products/[0-9]+$|
|
||||
)
|
||||
|
||||
spree_product_id = json_response["ofn:spree_product_id"].to_i
|
||||
|
||||
variant_id = dfc_id.split("/").last.to_i
|
||||
variant = Spree::Variant.find(variant_id)
|
||||
expect(variant.name).to eq "Apple"
|
||||
expect(variant.unit_value).to eq 3
|
||||
expect(variant.product_id).to eq spree_product_id
|
||||
|
||||
# References the associated Spree::Product
|
||||
product_id = json_response["ofn:spree_product_id"]
|
||||
product = Spree::Product.find(product_id)
|
||||
expect(product.name).to eq "Apple"
|
||||
expect(product.variants).to eq [variant]
|
||||
|
||||
# Creates a variant for existing product
|
||||
supplied_product[:'ofn:spree_product_id'] = product_id
|
||||
supplied_product[:'dfc-b:hasQuantity'][:'dfc-b:value'] = 6
|
||||
|
||||
expect {
|
||||
submit_request(example.metadata)
|
||||
product.variants.reload
|
||||
}
|
||||
.to change { product.variants.count }.by(1)
|
||||
|
||||
variant_id = json_response["@id"].split("/").last.to_i
|
||||
second_variant = Spree::Variant.find(variant_id)
|
||||
expect(product.variants).to match_array [variant, second_variant]
|
||||
expect(second_variant.unit_value).to eq 6
|
||||
|
||||
# Insert static value to keep documentation deterministic:
|
||||
response.body.gsub!(
|
||||
"supplied_products/#{variant_id}",
|
||||
"supplied_products/10001"
|
||||
)
|
||||
.gsub!(
|
||||
"\"ofn:spree_product_id\":#{spree_product_id}",
|
||||
'"ofn:spree_product_id":90000'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -116,6 +145,7 @@ describe "SuppliedProducts", type: :request, swagger_doc: "dfc.yaml",
|
||||
|
||||
run_test! do
|
||||
expect(response.body).to include variant.name
|
||||
expect(json_response["ofn:spree_product_id"]).to eq 90_000
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require DfcProvider::Engine.root.join("spec/spec_helper")
|
||||
require_relative "../spec_helper"
|
||||
|
||||
describe AddressBuilder do
|
||||
subject(:result) { described_class.address(address) }
|
||||
|
||||
@@ -118,6 +118,7 @@ paths:
|
||||
dfc-b:lifetime: ''
|
||||
dfc-b:usageOrStorageCondition: ''
|
||||
dfc-b:totalTheoreticalStock: 0.0
|
||||
ofn:spree_product_id: 90000
|
||||
- "@id": http://test.host/api/dfc/enterprises/10000/offers/10001
|
||||
"@type": dfc-b:Offer
|
||||
dfc-b:price: 19.99
|
||||
@@ -341,6 +342,7 @@ paths:
|
||||
dfc-b:lifetime: ''
|
||||
dfc-b:usageOrStorageCondition: ''
|
||||
dfc-b:totalTheoreticalStock: 0.0
|
||||
ofn:spree_product_id: 90000
|
||||
- "@id": http://test.host/api/dfc/enterprises/10000/catalog_items/10001
|
||||
"@type": dfc-b:CatalogItem
|
||||
dfc-b:references: http://test.host/api/dfc/enterprises/10000/supplied_products/10001
|
||||
@@ -409,6 +411,7 @@ paths:
|
||||
dfc-b:lifetime: ''
|
||||
dfc-b:usageOrStorageCondition: ''
|
||||
dfc-b:totalTheoreticalStock: 0.0
|
||||
ofn:spree_product_id: 90000
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
@@ -471,6 +474,7 @@ paths:
|
||||
dfc-b:lifetime: ''
|
||||
dfc-b:usageOrStorageCondition: ''
|
||||
dfc-b:totalTheoreticalStock: 0.0
|
||||
ofn:spree_product_id: 90000
|
||||
'404':
|
||||
description: not found
|
||||
put:
|
||||
|
||||
Reference in New Issue
Block a user