Skip to content

Commit

Permalink
Add review mutations
Browse files Browse the repository at this point in the history
  • Loading branch information
MahirSalahin committed Jul 26, 2024
1 parent f16347d commit ee2d6a1
Show file tree
Hide file tree
Showing 18 changed files with 572 additions and 179 deletions.
6 changes: 3 additions & 3 deletions saleor/graphql/core/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,9 @@ def error_policy_enum_description(enum):
CollectionErrorCode = graphene.Enum.from_enum(product_error_codes.CollectionErrorCode)
CollectionErrorCode.doc_category = DOC_CATEGORY_PRODUCTS

ReviewErrorCode = graphene.Enum.from_enum(review_error_codes.ReviewErrorCode)
ReviewErrorCode.doc_category = DOC_CATEGORY_PRODUCTS

SendConfirmationEmailErrorCode = graphene.Enum.from_enum(
account_error_codes.SendConfirmationEmailErrorCode
)
Expand Down Expand Up @@ -379,6 +382,3 @@ def error_policy_enum_description(enum):
webhook_error_codes.WebhookTriggerErrorCode
)
WebhookTriggerErrorCode.doc_category = DOC_CATEGORY_WEBHOOKS

ReviewErrorCode = graphene.Enum.from_enum(review_error_codes.ReviewErrorCode)
ReviewErrorCode.doc_category = DOC_CATEGORY_PRODUCTS
2 changes: 2 additions & 0 deletions saleor/graphql/core/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
ProductVariantBulkError,
ProductVariantBulkTranslateError,
ProductWithoutVariantError,
ReviewError,
SendConfirmationEmailError,
SeoInput,
ShippingError,
Expand Down Expand Up @@ -147,6 +148,7 @@
"ProductVariantBulkError",
"ProductVariantBulkTranslateError",
"ReducedRate",
"ReviewError",
"SendConfirmationEmailError",
"SeoInput",
"ShippingError",
Expand Down
6 changes: 6 additions & 0 deletions saleor/graphql/core/types/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
ProductTranslateErrorCode,
ProductVariantBulkErrorCode,
ProductVariantTranslateErrorCode,
ReviewErrorCode,
SendConfirmationEmailErrorCode,
ShippingErrorCode,
ShopErrorCode,
Expand Down Expand Up @@ -571,6 +572,11 @@ class Meta:
doc_category = DOC_CATEGORY_PRODUCTS


class ReviewError(Error):
code = ReviewErrorCode(description="The error code", required=True)
class Meta:
doc_category = DOC_CATEGORY_PRODUCTS

class ShopError(Error):
code = ShopErrorCode(description="The error code.", required=True)

Expand Down
6 changes: 6 additions & 0 deletions saleor/graphql/review/dataloaders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from ..core.dataloaders import BaseThumbnailBySizeAndFormatLoader


class ThumbnailByReviewMediaIdSizeAndFormatLoader(BaseThumbnailBySizeAndFormatLoader):
context_key = "thumbnail_by_reviewmedia_size_and_format"
model_name = "review_media"
6 changes: 6 additions & 0 deletions saleor/graphql/review/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from ..core.enums import to_enum
from ...review import ReviewMediaTypes
from ..core.doc_category import DOC_CATEGORY_PRODUCTS

ReviewMediaType = to_enum(ReviewMediaTypes, type_name="ReviewMediaType")
ReviewMediaType.doc_category = DOC_CATEGORY_PRODUCTS
7 changes: 0 additions & 7 deletions saleor/graphql/review/errors.py

This file was deleted.

11 changes: 11 additions & 0 deletions saleor/graphql/review/mutations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .review_submit import SubmitProductReview
from .review_delete import DeleteProductReview
from .review_update import UpdateProductReview
from .review_media_create import ReviewMediaCreate

__all__ = [
"SubmitProductReview",
"DeleteProductReview",
"UpdateProductReview",
"ReviewMediaCreate",
]
47 changes: 47 additions & 0 deletions saleor/graphql/review/mutations/review_delete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import graphene
from graphql import GraphQLError
from saleor.graphql.core import ResolveInfo
from ....review import models
from ....permission.enums import ProductPermissions
from ....core.exceptions import PermissionDenied
from ...core.mutations import ModelDeleteMutation
from ...core.types.common import ReviewError
from ..types import Review


class DeleteProductReview(ModelDeleteMutation):
class Arguments:
id = graphene.ID(required=True, description="ID of a review to delete")

class Meta:
description = "Deletes a product review"
model = models.Review
object_type = Review
error_type_class = ReviewError
error_type_field = "review_errors"
permissions = (ProductPermissions.MANAGE_PRODUCTS,)

@classmethod
def clean_instance(cls, info, instance):
if not instance:
raise GraphQLError("Review not found.")

@classmethod
def get_instance(cls, info, **data):
review_id = data.get("id")
try:
instance = models.Review.objects.get(pk=review_id)
except models.Review.DoesNotExist:
raise GraphQLError(f"Review with id {review_id} does not exist.")
return instance

@classmethod
def perform_mutation(cls, _root, info, **data):
instance = cls.get_instance(info, **data)
cls.clean_instance(info, instance)
db_id = instance.id
instance.delete()

instance.id = db_id
cls.post_save_action(info, instance, None)
return cls.success_response(instance)
145 changes: 145 additions & 0 deletions saleor/graphql/review/mutations/review_media_create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import graphene
from django.core.exceptions import ValidationError
from django.core.files import File
from ....core.http_client import HTTPClient
from ....review import ReviewMediaTypes, models
from ....review.error_codes import ReviewErrorCode
from ....core.utils.validators import get_oembed_data
from ....thumbnail.utils import get_filename_from_url
from ...core import ResolveInfo
from ...core.types import BaseInputObjectType, Upload
from ...core.doc_category import DOC_CATEGORY_PRODUCTS
from ...core.mutations import BaseMutation
from ...channel import ChannelContext
from ...core.types import ReviewError
from ...plugins.dataloaders import get_plugin_manager_promise
from ...core.validators.file import clean_image_file, is_image_url, validate_image_url
from ..types import Review, ReviewMedia

ALT_CHAR_LIMIT = 250


class ReviewMediaCreateInput(BaseInputObjectType):
alt = graphene.String(description="Alt text for a review media.")
image = Upload(
required=False, description="Represents an image file in a multipart request."
)
review = graphene.ID(required=True, description="ID of a review.", name="review")
media_url = graphene.String(
required=False, description="Represents an URL to an external media."
)

class Meta:
doc_category = DOC_CATEGORY_PRODUCTS


class ReviewMediaCreate(BaseMutation):
review = graphene.Field(Review)
media = graphene.Field(ReviewMedia)

class Arguments:
input = ReviewMediaCreateInput(
required=True, description="Fields required to create a review media."
)

class Meta:
description = (
"Create a media object (image or video URL) associated with review. "
"For image, this mutation must be sent as a `multipart` request. "
"More detailed specs of the upload format can be found here: "
"https://github.com/jaydenseric/graphql-multipart-request-spec"
)
doc_category = DOC_CATEGORY_PRODUCTS
# permissions = (ProductPermissions.MANAGE_PRODUCTS,)
error_type_class = ReviewError
error_type_field = "review_errors"

@classmethod
def validate_input(cls, data):
image = data.get("image")
media_url = data.get("media_url")
alt = data.get("alt")

if not image and not media_url:
raise ValidationError(
{
"input": ValidationError(
"Image or external URL is required.",
code=ReviewErrorCode.REQUIRED.value,
)
}
)
if image and media_url:
raise ValidationError(
{
"input": ValidationError(
"Either image or external URL is required.",
code=ReviewErrorCode.DUPLICATED_INPUT_ITEM.value,
)
}
)

if alt and len(alt) > ALT_CHAR_LIMIT:
raise ValidationError(
{
"input": ValidationError(
f"Alt field exceeds the character "
f"limit of {ALT_CHAR_LIMIT}.",
code=ReviewErrorCode.INVALID.value,
)
}
)

@classmethod
def perform_mutation( # type: ignore[override]
cls, _root, info: ResolveInfo, /, *, input
):
cls.validate_input(input)
review = cls.get_node_or_error(
info,
input["review"],
field="review",
only_type=Review,
qs=models.Review.objects.all(),
)

alt = input.get("alt", "")
media_url = input.get("media_url")
media = None
if img_data := input.get("image"):
input["image"] = info.context.FILES.get(img_data)
image_data = clean_image_file(input, "image", ReviewErrorCode)
media = review.media.create(
image=image_data, alt=alt, type=ReviewMediaTypes.IMAGE
)
if media_url:
# Remote URLs can point to the images or oembed data.
# In case of images, file is downloaded. Otherwise we keep only
# URL to remote media.
if is_image_url(media_url):
validate_image_url(
media_url, "media_url", ReviewErrorCode.INVALID.value
)
filename = get_filename_from_url(media_url)
image_data = HTTPClient.send_request(
"GET", media_url, stream=True, allow_redirects=False
)
image_file = File(image_data.raw, filename)
media = review.media.create(
image=image_file,
alt=alt,
type=ReviewMediaTypes.IMAGE,
)
else:
oembed_data, media_type = get_oembed_data(media_url, "media_url")
media = review.media.create(
external_url=oembed_data["url"],
alt=oembed_data.get("title", alt),
type=media_type,
oembed_data=oembed_data,
)
manager = get_plugin_manager_promise(info.context).get()
cls.call_event(manager.review_updated, review)
cls.call_event(manager.review_media_created, media)
review = ChannelContext(node=review, channel_slug=None)
return ReviewMediaCreate(review=review, media=media)
77 changes: 32 additions & 45 deletions saleor/graphql/review/mutations/review_submit.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
import graphene
from ....review.models import Review
from ....account.models import User
from ....product.models import Product
from ....permission.enums import ProductPermissions
from django.core.exceptions import ValidationError
from ....review import models
from ....review.error_codes import ReviewErrorCode
from ...core.mutations import ModelMutation
from ..types import ReviewType
from ..errors import ReviewError
from ...core.types import ReviewError
from ...core import ResolveInfo
from ..types import Review


class SubmitProductReviewInput(graphene.InputObjectType):
product_id = graphene.ID(required=True, description="Id of the Reviewed Product")
user_id = graphene.ID(
product = graphene.ID(required=True, description="Id of the Reviewed Product")
user = graphene.ID(
required=True, description="Id of the user who submitted the review"
)
rating = graphene.Int(required=True, description="Rating of the product")
title = graphene.String(required=True, description="Title of the Review")
review = graphene.String(required=True, description="Description of the review")
# media = JSONString()


class SubmitProductReview(ModelMutation):
review = graphene.Field(ReviewType)
review = graphene.Field(Review)

class Arguments:
input = SubmitProductReviewInput(
Expand All @@ -29,44 +28,32 @@ class Arguments:

class Meta:
description = "Create a new product review."
model = Review
object_type = ReviewType
model = models.Review
object_type = Review
error_type_class = ReviewError
error_type_field = "review_errors"

@classmethod
def perform_mutation(cls, root, info, **data):
input_data = data.get("input")

if input_data is None:
raise cls.error_type().error_fields_missing()

# Extract fields from input_data
product_id = input_data.get("product_id")
user_id = input_data.get("user_id")
rating = input_data.get("rating")
title = input_data.get("title")
review = input_data.get("review")

if not all([product_id, user_id, rating, title, review]):
raise cls.error_type().error_fields_missing()

# Fetch related objects
user = User.objects.get(pk=user_id)
product = Product.objects.get(pk=product_id)

# Create and save the review
review = Review(
user=user,
product=product,
rating=rating,
title=title,
review=review,
)
review.save()

return SubmitProductReview(review=review)
def clean_input(
cls, info: ResolveInfo, instance: models.Review, data: dict, **kwargs
):
validation_errors = {}
for field in ["product", "user", "rating", "title", "review"]:
if data[field] == "":
validation_errors[field] = ValidationError(
f"{field} cannot be empty.",
code=ReviewErrorCode.REQUIRED.value,
)
if validation_errors:
raise ValidationError(validation_errors)
return data

@classmethod
def save(cls, info, instance, cleaned_input):
super().save(info, instance, cleaned_input)
def perform_mutation(cls, _root, info: ResolveInfo, /, **data):
instance = cls.get_instance(info, **data)
data = data["input"]
cleaned_input = cls.clean_input(info, instance, data)
instance = cls.construct_instance(instance, cleaned_input)
cls.clean_instance(info, instance)
cls.save(info, instance, cleaned_input)
return cls.success_response(instance)
Loading

0 comments on commit ee2d6a1

Please sign in to comment.