Skip to content

Commit

Permalink
Add product_deleted webhook (#6794)
Browse files Browse the repository at this point in the history
* Add product_deleted webhook

* update changelog
  • Loading branch information
d-wysocki committed Feb 1, 2021
1 parent 7a24146 commit 97071ce
Show file tree
Hide file tree
Showing 12 changed files with 187 additions and 24 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ All notable, unreleased changes to this project will be documented in this file.
- Introduce product reference attributes - #6711 by @IKarbowiak
- Add metadata to warehouse - #6727 by @d-wysocki
- Add page webhooks: `PAGE_CREATED`, `PAGE_UPDATED` and `PAGE_DELETED` - #6787 by @d-wysocki
- Add `PRODUCT_DELETED` webhook - #6794 by @d-wysocki
- Fix `product_updated` and `product_created` webhooks - #6798 by @d-wysocki

# 2.11.1
Expand Down
32 changes: 28 additions & 4 deletions saleor/graphql/product/bulk_mutations/products.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,21 +87,45 @@ class Meta:
@classmethod
def perform_mutation(cls, _root, info, ids, **data):
_, pks = resolve_global_ids_to_primary_keys(ids, Product)
variants = models.ProductVariant.objects.filter(product__pk__in=pks)
product_to_variant = list(
models.ProductVariant.objects.filter(product__pk__in=pks).values_list(
"product_id", "id"
)
)
variants_ids = [variant_id for _, variant_id in product_to_variant]

# get draft order lines for products
order_line_pks = list(
order_models.OrderLine.objects.filter(
variant__in=variants, order__status=OrderStatus.DRAFT
variant_id__in=variants_ids, order__status=OrderStatus.DRAFT
).values_list("pk", flat=True)
)

response = super().perform_mutation(_root, info, ids, **data)
response = super().perform_mutation(
_root,
info,
ids,
manager=info.context.plugins,
product_to_variant=product_to_variant,
**data,
)

# delete order lines for deleted variants
order_models.OrderLine.objects.filter(pk__in=order_line_pks).delete()

return response

@classmethod
def bulk_action(cls, queryset, manager, product_to_variant):
product_variant_map = defaultdict(list)
for product, variant in product_to_variant:
product_variant_map[product].append(variant)

products = [product for product in queryset]
queryset.delete()
for product in products:
variants = product_variant_map.get(product.id)
manager.product_deleted(product, variants)


class BulkAttributeValueInput(InputObjectType):
id = graphene.ID(description="ID of the selected attribute.")
Expand Down
8 changes: 4 additions & 4 deletions saleor/graphql/product/mutations/products.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,19 +681,19 @@ def success_response(cls, instance):
@classmethod
def perform_mutation(cls, _root, info, **data):
node_id = data.get("id")
instance = cls.get_node_or_error(info, node_id, only_type=Product)

instance = cls.get_node_or_error(info, node_id, only_type=Product)
variants_id = list(instance.variants.all().values_list("id", flat=True))
# get draft order lines for variant
line_pks = list(
order_models.OrderLine.objects.filter(
variant__in=instance.variants.all(), order__status=OrderStatus.DRAFT
variant_id__in=variants_id, order__status=OrderStatus.DRAFT
).values_list("pk", flat=True)
)

response = super().perform_mutation(_root, info, **data)

# delete order lines for deleted variant
order_models.OrderLine.objects.filter(pk__in=line_pks).delete()
info.context.plugins.product_deleted(instance, variants_id)

return response

Expand Down
28 changes: 28 additions & 0 deletions saleor/graphql/product/tests/test_bulk_delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,34 @@ def test_delete_products(
assert OrderLine.objects.filter(pk__in=not_draft_order_lines_pks).exists()


@patch("saleor.plugins.webhook.plugin.trigger_webhooks_for_event.delay")
def test_delete_products_trigger_webhook(
mocked_webhook_trigger,
staff_api_client,
product_list,
permission_manage_products,
channel_USD,
settings,
):
# given
settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]

query = DELETE_PRODUCTS_MUTATION
variables = {
"ids": [
graphene.Node.to_global_id("Product", product.id)
for product in product_list
]
}
response = staff_api_client.post_graphql(
query, variables, permissions=[permission_manage_products]
)
content = get_graphql_content(response)

assert content["data"]["productBulkDelete"]["count"] == 3
assert mocked_webhook_trigger.called


def test_delete_products_variants_in_draft_order(
staff_api_client, product_list, permission_manage_products
):
Expand Down
33 changes: 33 additions & 0 deletions saleor/graphql/product/tests/test_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
from ....product.utils.costs import get_product_costs_data
from ....tests.utils import dummy_editorjs
from ....warehouse.models import Allocation, Stock, Warehouse
from ....webhook.event_types import WebhookEventType
from ....webhook.payloads import generate_product_deleted_payload
from ...core.enums import AttributeErrorCode, ReportingPeriod
from ...tests.utils import (
assert_no_permission,
Expand Down Expand Up @@ -4906,6 +4908,37 @@ def test_delete_product(staff_api_client, product, permission_manage_products):
assert node_id == data["product"]["id"]


@patch("saleor.plugins.webhook.plugin.trigger_webhooks_for_event.delay")
def test_delete_product_trigger_webhook(
mocked_webhook_trigger,
staff_api_client,
product,
permission_manage_products,
settings,
):
settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]

query = DELETE_PRODUCT_MUTATION
node_id = graphene.Node.to_global_id("Product", product.id)
variants_id = list(product.variants.all().values_list("id", flat=True))
variables = {"id": node_id}
response = staff_api_client.post_graphql(
query, variables, permissions=[permission_manage_products]
)
content = get_graphql_content(response)
data = content["data"]["productDelete"]
assert data["product"]["name"] == product.name
with pytest.raises(product._meta.model.DoesNotExist):
product.refresh_from_db()
assert node_id == data["product"]["id"]

expected_data = generate_product_deleted_payload(product, variants_id)

mocked_webhook_trigger.assert_called_once_with(
WebhookEventType.PRODUCT_DELETED, expected_data
)


def test_delete_product_variant_in_draft_order(
staff_api_client,
product_with_two_variants,
Expand Down
2 changes: 2 additions & 0 deletions saleor/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -5785,6 +5785,7 @@ enum WebhookEventTypeEnum {
CUSTOMER_CREATED
PRODUCT_CREATED
PRODUCT_UPDATED
PRODUCT_DELETED
CHECKOUT_CREATED
CHECKOUT_UPDATED
FULFILLMENT_CREATED
Expand All @@ -5806,6 +5807,7 @@ enum WebhookSampleEventTypeEnum {
CUSTOMER_CREATED
PRODUCT_CREATED
PRODUCT_UPDATED
PRODUCT_DELETED
CHECKOUT_CREATED
CHECKOUT_UPDATED
FULFILLMENT_CREATED
Expand Down
10 changes: 10 additions & 0 deletions saleor/plugins/base_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,16 @@ def product_updated(self, product: "Product", previous_value: Any) -> Any:
"""
return NotImplemented

def product_deleted(
self, product: "Product", variants: List[int], previous_value: Any
) -> Any:
"""Trigger when product is deleted.
Overwrite this method if you need to trigger specific logic after a product is
deleted.
"""
return NotImplemented

def order_fully_paid(self, order: "Order", previous_value: Any) -> Any:
"""Trigger when order is fully paid.
Expand Down
6 changes: 6 additions & 0 deletions saleor/plugins/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,12 @@ def product_updated(self, product: "Product"):
default_value = None
return self.__run_method_on_plugins("product_updated", default_value, product)

def product_deleted(self, product: "Product", variants: List[int]):
default_value = None
return self.__run_method_on_plugins(
"product_deleted", default_value, product, variants
)

def order_created(self, order: "Order"):
default_value = None
return self.__run_method_on_plugins("order_created", default_value, order)
Expand Down
11 changes: 10 additions & 1 deletion saleor/plugins/webhook/plugin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Any, Optional
from typing import TYPE_CHECKING, Any, List, Optional

from ...webhook.event_types import WebhookEventType
from ...webhook.payloads import (
Expand All @@ -8,6 +8,7 @@
generate_invoice_payload,
generate_order_payload,
generate_page_payload,
generate_product_deleted_payload,
generate_product_payload,
)
from ..base_plugin import BasePlugin
Expand Down Expand Up @@ -121,6 +122,14 @@ def product_updated(self, product: "Product", previous_value: Any) -> Any:
product_data = generate_product_payload(product)
trigger_webhooks_for_event.delay(WebhookEventType.PRODUCT_UPDATED, product_data)

def product_deleted(
self, product: "Product", variants: List[int], previous_value: Any
) -> Any:
if not self.active:
return previous_value
product_data = generate_product_deleted_payload(product, variants)
trigger_webhooks_for_event.delay(WebhookEventType.PRODUCT_DELETED, product_data)

def checkout_created(self, checkout: "Checkout", previous_value: Any) -> Any:
if not self.active:
return previous_value
Expand Down
30 changes: 30 additions & 0 deletions saleor/plugins/webhook/tests/test_webhook.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import json
from unittest import mock

import graphene
import pytest

from ....app.models import App
Expand All @@ -10,6 +12,7 @@
generate_invoice_payload,
generate_order_payload,
generate_page_payload,
generate_product_deleted_payload,
generate_product_payload,
)
from ...manager import get_plugins_manager
Expand Down Expand Up @@ -148,6 +151,33 @@ def test_product_updated(mocked_webhook_trigger, settings, product):
)


@mock.patch("saleor.plugins.webhook.plugin.trigger_webhooks_for_event.delay")
def test_product_deleted(mocked_webhook_trigger, settings, product):
settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
manager = get_plugins_manager()

product = product
variants_id = list(product.variants.all().values_list("id", flat=True))
product_id = product.id
product.delete()
product.id = product_id
variant_global_ids = [
graphene.Node.to_global_id("ProductVariant", pk) for pk in variants_id
]
manager.product_deleted(product, variants_id)

expected_data = generate_product_deleted_payload(product, variants_id)

expected_data_dict = json.loads(expected_data)[0]
assert expected_data_dict["id"] is not None
assert expected_data_dict["variants"] is not None
assert variant_global_ids == expected_data_dict["variants"]

mocked_webhook_trigger.assert_called_once_with(
WebhookEventType.PRODUCT_DELETED, expected_data
)


@mock.patch("saleor.plugins.webhook.plugin.trigger_webhooks_for_event.delay")
def test_order_updated(mocked_webhook_trigger, settings, order_with_lines):
settings.PLUGINS = ["saleor.plugins.webhook.plugin.WebhookPlugin"]
Expand Down
4 changes: 4 additions & 0 deletions saleor/webhook/event_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class WebhookEventType:

PRODUCT_CREATED = "product_created"
PRODUCT_UPDATED = "product_updated"
PRODUCT_DELETED = "product_deleted"

CHECKOUT_CREATED = "checkout_created"
CHECKOUT_UPADTED = "checkout_updated"
Expand All @@ -48,6 +49,7 @@ class WebhookEventType:
CUSTOMER_CREATED: "Customer created",
PRODUCT_CREATED: "Product created",
PRODUCT_UPDATED: "Product updated",
PRODUCT_DELETED: "Product deleted",
CHECKOUT_CREATED: "Checkout created",
CHECKOUT_UPADTED: "Checkout updated",
FULFILLMENT_CREATED: "Fulfillment_created",
Expand All @@ -70,6 +72,7 @@ class WebhookEventType:
(CUSTOMER_CREATED, DISPLAY_LABELS[CUSTOMER_CREATED]),
(PRODUCT_CREATED, DISPLAY_LABELS[PRODUCT_CREATED]),
(PRODUCT_UPDATED, DISPLAY_LABELS[PRODUCT_UPDATED]),
(PRODUCT_DELETED, DISPLAY_LABELS[PRODUCT_DELETED]),
(CHECKOUT_CREATED, DISPLAY_LABELS[CHECKOUT_CREATED]),
(CHECKOUT_UPADTED, DISPLAY_LABELS[CHECKOUT_UPADTED]),
(FULFILLMENT_CREATED, DISPLAY_LABELS[FULFILLMENT_CREATED]),
Expand All @@ -91,6 +94,7 @@ class WebhookEventType:
CUSTOMER_CREATED: AccountPermissions.MANAGE_USERS,
PRODUCT_CREATED: ProductPermissions.MANAGE_PRODUCTS,
PRODUCT_UPDATED: ProductPermissions.MANAGE_PRODUCTS,
PRODUCT_DELETED: ProductPermissions.MANAGE_PRODUCTS,
CHECKOUT_CREATED: CheckoutPermissions.MANAGE_CHECKOUTS,
CHECKOUT_UPADTED: CheckoutPermissions.MANAGE_CHECKOUTS,
FULFILLMENT_CREATED: OrderPermissions.MANAGE_ORDERS,
Expand Down
46 changes: 31 additions & 15 deletions saleor/webhook/payloads.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
from typing import Iterable, Optional

import graphene
from django.db.models import QuerySet

from ..account.models import User
Expand Down Expand Up @@ -200,24 +201,25 @@ def generate_customer_payload(customer: "User"):
return data


PRODUCT_FIELDS = (
"name",
"description",
"currency",
"attributes",
"updated_at",
"charge_taxes",
"weight",
"publication_date",
"is_published",
"private_metadata",
"metadata",
)


def generate_product_payload(product: "Product"):
serializer = PayloadSerializer(
extra_model_fields={"ProductVariant": ("quantity", "quantity_allocated")}
)

product_fields = (
"name",
"description",
"currency",
"attributes",
"updated_at",
"charge_taxes",
"weight",
"publication_date",
"is_published",
"private_metadata",
"metadata",
)
product_variant_fields = (
"sku",
"name",
Expand All @@ -230,7 +232,7 @@ def generate_product_payload(product: "Product"):
)
product_payload = serializer.serialize(
[product],
fields=product_fields,
fields=PRODUCT_FIELDS,
additional_fields={
"category": (lambda p: p.category, ("name", "slug")),
"collections": (lambda p: p.collections.all(), ("name", "slug")),
Expand All @@ -243,6 +245,20 @@ def generate_product_payload(product: "Product"):
return product_payload


def generate_product_deleted_payload(product: "Product", variants_id):
serializer = PayloadSerializer()
product_fields = PRODUCT_FIELDS
variant_global_ids = [
graphene.Node.to_global_id("ProductVariant", pk) for pk in variants_id
]
product_payload = serializer.serialize(
[product],
fields=product_fields,
extra_dict_data={"variants": list(variant_global_ids)},
)
return product_payload


def generate_fulfillment_lines_payload(fulfillment: Fulfillment):
serializer = PayloadSerializer()
lines = FulfillmentLine.objects.prefetch_related(
Expand Down

0 comments on commit 97071ce

Please sign in to comment.