Files
openfoodnetwork/app/services/sets/model_set.rb
Pau Perez 060530cda8 Do not fetch VOs with deleted variant
In the line below we filter them out in Ruby so it's a waste of
resources. The fundamental difference is that `#includes` and
`#references` results in LEFT JOINs, whereas `#joins` results in INNER
JOIN, and because there's a default scope on `deleted_at IS NULL`, these
are not included in the result set.

This however, requires us to move away from the current algorithm but
unfortunately we can't refactor it completely yet.

Before:

```sql
SELECT *
  FROM "variant_overrides"
  LEFT OUTER
  JOIN "spree_variants"
    ON "spree_variants"."id"              =  "variant_overrides"."variant_id"
   AND "spree_variants"."deleted_at" IS NULL
  LEFT OUTER
  JOIN "spree_products"
    ON "spree_products"."id"              =  "spree_variants"."product_id"
   AND "spree_products"."deleted_at" IS NULL
 WHERE "variant_overrides"."permission_revoked_at" IS NULL
   AND "variant_overrides"."hub_id" IN (
    SELECT "enterprises"."id"
      FROM "enterprises"
    INNER
      JOIN "enterprise_roles"
        ON "enterprise_roles"."enterprise_id" =  "enterprises"."id"
    WHERE (enterprise_roles.user_id          =  ?)
      AND (sells                             != 'none')
 ORDER BY name)
```

After:

```sql
SELECT "variant_overrides".*
  FROM "variant_overrides"
 INNER
  JOIN "spree_variants"
    ON "spree_variants"."id"              =  "variant_overrides"."variant_id"
   AND "spree_variants"."deleted_at" IS NULL
 INNER
  JOIN "spree_products"
    ON "spree_products"."id"              =  "spree_variants"."product_id"
   AND "spree_products"."deleted_at" IS NULL
 WHERE "variant_overrides"."permission_revoked_at" IS NULL
   AND "variant_overrides"."hub_id" IN (
    SELECT "enterprises"."id"
      FROM "enterprises"
    INNER
      JOIN "enterprise_roles"
        ON "enterprise_roles"."enterprise_id" =  "enterprises"."id"
    WHERE (enterprise_roles.user_id          =  ?)
      AND (sells                             != 'none')
 ORDER BY name)
```

This is covered in the test suite by
spec/controllers/admin/variant_overrides_controller_spec.rb:72. It keeps
passing so we're good to go.
2021-02-01 14:47:51 +01:00

79 lines
2.4 KiB
Ruby

# frozen_string_literal: true
# Tableless model to handle updating multiple models at once from a single form
module Sets
class ModelSet
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :collection
def initialize(klass, collection, attributes = {}, reject_if = nil, delete_if = nil)
@klass, @collection, @reject_if, @delete_if = klass, collection, reject_if, delete_if
# Set here first, to ensure that we apply collection_attributes to the right collection
@collection = attributes[:collection] if attributes[:collection]
attributes.each do |name, value|
public_send("#{name}=", value)
end
end
def collection_attributes=(collection_attributes)
collection_attributes.each do |_k, attributes|
# attributes == {:id => 123, :next_collection_at => '...'}
found_element = @collection.detect do |element|
element.id.to_s == attributes[:id].to_s && !element.id.nil?
end
if found_element.nil?
@collection << @klass.new(attributes) unless @reject_if.andand.call(attributes)
else
process(found_element, attributes)
end
end
end
# The request params are assigned to the fetched record here, becoming dirty and ready to
# be persisted in DB as an UPDATE.
#
# This enables having different process strategies depending on the concrete model set class, if
# we choose to. This will be the default implementation.
def process(found_element, attributes)
found_element.assign_attributes(attributes.except(:id))
end
def errors
errors = ActiveModel::Errors.new self
full_messages = @collection
.map { |model| model.errors.full_messages }
.flatten
full_messages.each { |message| errors.add(:base, message) }
errors
end
def save
collection_to_delete.each(&:destroy)
collection_to_keep.all?(&:save)
end
def collection_to_delete
# Remove all elements to be deleted from collection and return them
# Allows us to render @model_set.collection without deleted elements
deleted = []
@collection = collection.to_a
collection.delete_if { |e| deleted << e if @delete_if.andand.call(e.attributes) }
deleted
end
def collection_to_keep
collection.reject { |e| @delete_if.andand.call(e.attributes) }
end
def persisted?
false
end
end
end