Merge pull request #5810 from pacodelaluna/improve-dfc-standard-integration

Improve DFC Provider engine to support version 1.3
This commit is contained in:
François Turbelin
2020-09-30 11:13:03 +02:00
committed by GitHub
20 changed files with 679 additions and 124 deletions

View File

@@ -64,6 +64,7 @@ PATH
remote: engines/dfc_provider
specs:
dfc_provider (0.0.1)
active_model_serializers (~> 0.8.4)
jwt (~> 2.2)
rspec (~> 3.9)

View File

@@ -0,0 +1,63 @@
# frozen_string_literal: true
# Controller used to provide the API products for the DFC application
module DfcProvider
module Api
class BaseController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :not_found
before_action :check_authorization,
:check_user,
:check_enterprise
respond_to :json
private
def check_authorization
return if access_token.present?
head :unprocessable_entity
end
def check_user
return if current_user.present?
head :unauthorized
end
def check_enterprise
current_enterprise
end
def current_enterprise
@current_enterprise ||=
if params[enterprise_id_param_name] == 'default'
current_user.enterprises.first!
else
current_user.enterprises.find(params[enterprise_id_param_name])
end
end
def enterprise_id_param_name
:enterprise_id
end
def current_user
@current_user ||= authorization_control.process
end
def access_token
request.headers['Authorization'].to_s.split(' ').last
end
def authorization_control
DfcProvider::AuthorizationControl.new(access_token)
end
def not_found
head :not_found
end
end
end
end

View File

@@ -0,0 +1,26 @@
# frozen_string_literal: true
# Controller used to provide the API products for the DFC application
module DfcProvider
module Api
class CatalogItemsController < DfcProvider::Api::BaseController
def index
render json: current_user, serializer: DfcProvider::PersonSerializer
end
def show
render json: variant, serializer: DfcProvider::CatalogItemSerializer
end
private
def variant
@variant ||=
Spree::Variant.
joins(product: :supplier).
where('enterprises.id' => current_enterprise.id).
find(params[:id])
end
end
end
end

View File

@@ -0,0 +1,18 @@
# frozen_string_literal: true
# Controller used to provide the CatalogItem API for the DFC application
module DfcProvider
module Api
class EnterprisesController < DfcProvider::Api::BaseController
def show
render json: current_enterprise, serializer: DfcProvider::EnterpriseSerializer
end
private
def enterprise_id_param_name
:id
end
end
end
end

View File

@@ -0,0 +1,28 @@
# frozen_string_literal: true
# Controller used to provide the Persons API for the DFC application
module DfcProvider
module Api
class PersonsController < DfcProvider::Api::BaseController
skip_before_action :check_enterprise
before_action :check_user_accessibility
def show
render json: user, serializer: DfcProvider::PersonSerializer
end
private
def user
@user ||= Spree::User.find(params[:id])
end
def check_user_accessibility
return if current_user == user
not_found
end
end
end
end

View File

@@ -1,70 +0,0 @@
# frozen_string_literal: true
# Controller used to provide the API products for the DFC application
module DfcProvider
module Api
class ProductsController < ::ActionController::Base
# To access 'base_url' helper
include Rails.application.routes.url_helpers
before_filter :check_authorization,
:check_user,
:check_enterprise
respond_to :json
def index
products = @enterprise.
inventory_variants.
includes(:product, :inventory_items)
serialized_data = ::DfcProvider::ProductSerializer.
new(products, base_url).
serialized_data
render json: serialized_data
end
private
def check_enterprise
@enterprise =
if params[:enterprise_id] == 'default'
@user.enterprises.first
else
@user.enterprises.where(id: params[:enterprise_id]).first
end
return if @enterprise.present?
head :not_found
end
def check_authorization
return if access_token.present?
head :unprocessable_entity
end
def check_user
@user = authorization_control.process
return if @user.present?
head :unauthorized
end
def base_url
"#{root_url}api/dfc_provider"
end
def access_token
request.headers['Authorization'].to_s.split(' ').last
end
def authorization_control
DfcProvider::AuthorizationControl.new(access_token)
end
end
end
end

View File

@@ -0,0 +1,22 @@
# frozen_string_literal: true
# Controller used to provide the SuppliedProducts API for the DFC application
module DfcProvider
module Api
class SuppliedProductsController < DfcProvider::Api::BaseController
def show
render json: variant, serializer: DfcProvider::SuppliedProductSerializer
end
private
def variant
@variant ||=
Spree::Variant.
joins(product: :supplier).
where('enterprises.id' => current_enterprise.id).
find(params[:id])
end
end
end
end

View File

@@ -0,0 +1,25 @@
# frozen_string_literal: true
# Serializer used to render the DFC Address from an OFN User
# into JSON-LD format based on DFC ontology
module DfcProvider
class AddressSerializer < ActiveModel::Serializer
attribute :type, key: '@type'
attribute :city, key: 'dfc:city'
attribute :country, key: 'dfc:country'
attribute :postcode, key: 'dfc:postcode'
attribute :street, key: 'dfc:street'
def type
'dfc:Address'
end
def city; end
def country; end
def postcode; end
def street; end
end
end

View File

@@ -0,0 +1,55 @@
# frozen_string_literal: true
# Serializer used to render a DFC CatalogItem from an OFN Product
# into JSON-LD format based on DFC ontology
module DfcProvider
class CatalogItemSerializer < ActiveModel::Serializer
attribute :id, key: '@id'
attribute :type, key: '@type'
attribute :references, key: 'dfc:references'
attribute :sku, key: 'dfc:sku'
attribute :stock_limitation, key: 'dfc:stockLimitation'
has_many :offered_through,
serializer: DfcProvider::OfferSerializer,
key: 'dfc:offeredThrough'
def id
dfc_provider_routes.api_dfc_provider_enterprise_catalog_item_url(
enterprise_id: object.product.supplier_id,
id: object.id,
host: root_url
)
end
def type
'dfc:CatalogItem'
end
def references
{
'@type' => '@id',
'@id' => "/supplied_products/#{object.product_id}"
}
end
def stock_limitation; end
def offered_through
[object]
end
private
def reference_id
dfc_provider_routes.api_dfc_provider_enterprise_supplied_product_url(
enterprise_id: object.product.supplier_id,
id: object.product_id,
host: root_url
)
end
def dfc_provider_routes
DfcProvider::Engine.routes.url_helpers
end
end
end

View File

@@ -0,0 +1,53 @@
# frozen_string_literal: true
# Serializer used to render a DFC Enterprise from an OFN Enterprise
# into JSON-LD format based on DFC ontology
module DfcProvider
class EnterpriseSerializer < ActiveModel::Serializer
attribute :id, key: '@id'
attribute :type, key: '@type'
attribute :vat_number, key: 'dfc:VATnumber'
has_many :defines, key: 'dfc:defines'
has_many :supplies,
key: 'dfc:supplies',
serializer: DfcProvider::SuppliedProductSerializer
has_many :manages,
key: 'dfc:manages',
serializer: DfcProvider::CatalogItemSerializer
def id
dfc_provider_routes.api_dfc_provider_enterprise_url(
id: object.id,
host: root_url
)
end
def type
'dfc:Entreprise'
end
def vat_number; end
def defines
[]
end
def supplies
Spree::Variant.
joins(product: :supplier).
where('enterprises.id' => object.id)
end
def manages
Spree::Variant.
joins(product: :supplier).
where('enterprises.id' => object.id)
end
private
def dfc_provider_routes
DfcProvider::Engine.routes.url_helpers
end
end
end

View File

@@ -0,0 +1,32 @@
# frozen_string_literal: true
# Serializer used to render the DFC Offer from an OFN Product
# into JSON-LD format based on DFC ontology
module DfcProvider
class OfferSerializer < ActiveModel::Serializer
attribute :id, key: '@id'
attribute :type, key: '@type'
attribute :offeres_to, key: 'dfc:offeres_to'
attribute :price, key: 'dfc:price'
attribute :stock_limitation, key: 'dfc:stockLimitation'
def id
"/offers/#{object.id}"
end
def type
'dfc:Offer'
end
def offeres_to
{
'@type' => '@id',
'@id' => nil
}
end
def stock_limitation
object.on_hand
end
end
end

View File

@@ -0,0 +1,55 @@
# frozen_string_literal: true
# Serializer used to render the DFC Person from an OFN User
# into JSON-LD format based on DFC ontology
module DfcProvider
class PersonSerializer < ActiveModel::Serializer
attribute :context, key: '@context'
attribute :id, key: '@id'
attribute :type, key: '@type'
attribute :family_name, key: 'dfc:familyName'
attribute :first_name, key: 'dfc:firstName'
has_one :address,
key: 'dfc:hasAddress',
serializer: DfcProvider::AddressSerializer
has_many :affiliates,
key: 'dfc:affiliates',
serializer: DfcProvider::EnterpriseSerializer
# Context should be provided inside the controller,
# but AMS doesn't not supported `meta` and `meta_key` with `root` to nil...
def context
{
'dfc' => 'http://datafoodconsortium.org/ontologies/DFC_FullModel.owl#',
'@base' => "#{root_url}api/dfc_provider"
}
end
def id
dfc_provider_routes.api_dfc_provider_person_url(
id: object.id,
host: root_url
)
end
def type
'dfc:Person'
end
def family_name; end
def first_name; end
def address; end
def affiliates
object.enterprises
end
private
def dfc_provider_routes
DfcProvider::Engine.routes.url_helpers
end
end
end

View File

@@ -1,37 +0,0 @@
# frozen_string_literal: true
# Serializer used to render the products passed
# into JSON-LD format based on DFC ontology
module DfcProvider
class ProductSerializer
def initialize(products, base_url)
@products = products
@base_url = base_url
end
def serialized_data
{
"@context" =>
{
"DFC" => "http://datafoodconsortium.org/ontologies/DFC_FullModel.owl#",
"@base" => @base_url
},
"@id" => "/enterprise/products",
"DFC:supplies" => serialized_products
}
end
private
def serialized_products
@products.map do |variant|
{
"DFC:description" => variant.name,
"DFC:quantity" => variant.total_on_hand,
"@id" => variant.id,
"DFC:hasUnit" => { "@id" => "/unit/#{variant.unit_description.presence || 'piece'}" }
}
end
end
end
end

View File

@@ -0,0 +1,72 @@
# frozen_string_literal: true
# Serializer used to render a DFC SuppliedProduct from an OFN Variant
# into JSON-LD format based on DFC ontology
module DfcProvider
class SuppliedProductSerializer < ActiveModel::Serializer
attribute :id, key: '@id'
attribute :type, key: '@type'
attribute :unit, key: 'dfc:hasUnit'
attribute :quantity, key: 'dfc:quantity'
attribute :description, key: 'dfc:description'
attribute :total_theoritical_stock, key: 'dfc:totalTheoriticalStock'
attribute :brand, key: 'dfc:brand'
attribute :claim, key: 'dfc:claim'
attribute :image, key: 'dfc:image'
attribute :life_time, key: 'lifeTime'
has_many :physical_characteristics, key: 'dfc:physicalCharacterisctics'
def id
dfc_provider_routes.api_dfc_provider_enterprise_supplied_product_url(
enterprise_id: object.product.supplier_id,
id: object.id,
host: root_url
)
end
def type
'dfc:SuppliedProduct'
end
def unit
{
'@id' => "/unit/#{unit_name}",
'rdfs:label' => unit_name
}
end
def quantity
object.on_hand
end
def description
object.name
end
def total_theoritical_stock; end
def brand; end
def claim; end
def image
object.images.first.try(:attachment, :url)
end
def life_time; end
def physical_characteristics
[]
end
private
def unit_name
object.unit_description.presence || 'piece'
end
def dfc_provider_routes
DfcProvider::Engine.routes.url_helpers
end
end
end

View File

@@ -3,9 +3,11 @@
DfcProvider::Engine.routes.draw do
namespace :api do
scope :dfc_provider, as: :dfc_provider, path: '/dfc_provider' do
resources :enterprises, only: :none do
resources :products, only: [:index]
resources :enterprises, only: [:show] do
resources :catalog_items, only: [:index, :show]
resources :supplied_products, only: [:show]
end
resources :persons, only: [:show]
end
end
end

View File

@@ -16,6 +16,7 @@ Gem::Specification.new do |spec|
spec.files = Dir["{app,config,lib}/**/*"] + ['README.md']
spec.test_files = Dir['spec/**/*']
spec.add_dependency 'active_model_serializers', '~> 0.8.4'
spec.add_dependency 'jwt', '~> 2.2'
spec.add_dependency 'rspec', '~> 3.9'
end

View File

@@ -2,18 +2,13 @@
require 'spec_helper'
describe DfcProvider::Api::ProductsController, type: :controller do
describe DfcProvider::Api::CatalogItemsController, type: :controller do
render_views
let(:user) { create(:user) }
let(:enterprise) { create(:distributor_enterprise, owner: user) }
let(:product) { create(:simple_product, supplier: enterprise ) }
let!(:visible_inventory_item) do
create(:inventory_item,
enterprise: enterprise,
variant: product.variants.first,
visible: true)
end
let!(:user) { create(:user) }
let!(:enterprise) { create(:distributor_enterprise, owner: user) }
let!(:product) { create(:simple_product, supplier: enterprise ) }
let!(:variant) { product.variants.first }
describe('.index') do
context 'with authorization token' do
@@ -37,9 +32,13 @@ describe DfcProvider::Api::ProductsController, type: :controller do
expect(response.status).to eq 200
end
it 'renders the related product' do
it 'renders the required content' do
expect(response.body)
.to include(product.variants.first.name)
.to include(variant.name)
expect(response.body)
.to include(variant.sku)
expect(response.body)
.to include("offers/#{variant.id}")
end
end
@@ -60,9 +59,13 @@ describe DfcProvider::Api::ProductsController, type: :controller do
expect(response.status).to eq 200
end
it 'renders the related product' do
it 'renders the required content' do
expect(response.body)
.to include(product.variants.first.name)
.to include(variant.name)
expect(response.body)
.to include(variant.sku)
expect(response.body)
.to include("offers/#{variant.id}")
end
end
end
@@ -70,7 +73,7 @@ describe DfcProvider::Api::ProductsController, type: :controller do
context 'without a recorded enterprise' do
let(:enterprise) { create(:enterprise) }
it 'returns not_found head' do
it 'is not found' do
api_get :index, enterprise_id: 'default'
expect(response.status).to eq 404
end
@@ -96,4 +99,53 @@ describe DfcProvider::Api::ProductsController, type: :controller do
end
end
end
describe('.show') do
context 'with authorization token' do
before do
request.headers['Authorization'] = 'Bearer 123456.abcdef.123456'
end
context 'with an authenticated user' do
before do
allow_any_instance_of(DfcProvider::AuthorizationControl)
.to receive(:process)
.and_return(user)
end
context 'with an enterprise' do
context 'given with an id' do
before do
api_get :show,
enterprise_id: enterprise.id,
id: variant.id
end
it 'is successful' do
expect(response.status).to eq 200
end
it 'renders the required content' do
expect(response.body)
.to include('dfc:CatalogItem')
expect(response.body)
.to include("offers/#{variant.id}")
end
end
context 'with a variant not linked to the enterprise' do
before do
api_get :show,
enterprise_id: enterprise.id,
id: create(:simple_product).variants.first.id
end
it 'is not found' do
expect(response.status).to eq 404
end
end
end
end
end
end
end

View File

@@ -0,0 +1,54 @@
# frozen_string_literal: true
require 'spec_helper'
describe DfcProvider::Api::EnterprisesController, type: :controller do
render_views
let!(:user) { create(:user) }
let!(:enterprise) { create(:distributor_enterprise, owner: user) }
let!(:product) { create(:simple_product, supplier: enterprise ) }
describe('.show') do
context 'with authorization token' do
before do
request.headers['Authorization'] = 'Bearer 123456.abcdef.123456'
end
context 'with an authenticated user' do
before do
allow_any_instance_of(DfcProvider::AuthorizationControl)
.to receive(:process)
.and_return(user)
end
context 'with an enterprise' do
context 'given with an id' do
before { api_get :show, id: 'default' }
it 'is successful' do
expect(response.status).to eq 200
end
it 'renders the required content' do
expect(response.body)
.to include(product.name)
expect(response.body)
.to include(product.sku)
expect(response.body)
.to include("offers/#{product.variants.first.id}")
end
end
context 'given with a wrong id' do
before { api_get :show, id: 999 }
it 'is not found' do
expect(response.status).to eq 404
end
end
end
end
end
end
end

View File

@@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'spec_helper'
describe DfcProvider::Api::PersonsController, type: :controller do
render_views
let!(:user) { create(:user) }
describe('.show') do
context 'with authorization token' do
before do
request.headers['Authorization'] = 'Bearer 123456.abcdef.123456'
end
context 'with an authenticated user' do
before do
allow_any_instance_of(DfcProvider::AuthorizationControl)
.to receive(:process)
.and_return(user)
end
context 'given with an accessible id' do
before do
api_get :show,
id: user.id
end
it 'is successful' do
expect(response.status).to eq 200
end
it 'renders the required content' do
expect(response.body).to include('dfc:Person')
end
end
context 'with an other user id' do
before { api_get :show, id: create(:user).id }
it 'is not found' do
expect(response.status).to eq 404
end
end
end
end
end
end

View File

@@ -0,0 +1,55 @@
# frozen_string_literal: true
require 'spec_helper'
describe DfcProvider::Api::SuppliedProductsController, type: :controller do
render_views
let!(:user) { create(:user) }
let!(:enterprise) { create(:distributor_enterprise, owner: user) }
let!(:product) { create(:simple_product, supplier: enterprise ) }
let!(:variant) { product.variants.first }
describe('.show') do
context 'with authorization token' do
before do
request.headers['Authorization'] = 'Bearer 123456.abcdef.123456'
end
context 'with an authenticated user' do
before do
allow_any_instance_of(DfcProvider::AuthorizationControl)
.to receive(:process)
.and_return(user)
end
context 'with an enterprise' do
context 'given with an id' do
before do
api_get :show,
enterprise_id: 'default',
id: variant.id
end
it 'is successful' do
expect(response.status).to eq 200
end
it 'renders the required content' do
expect(response.body)
.to include(variant.name)
end
end
context 'given with a wrong id' do
before { api_get :show, id: 999 }
it 'is not found' do
expect(response.status).to eq 404
end
end
end
end
end
end
end