diff --git a/blog/docs/drf/viewsets.py b/blog/docs/drf/viewsets.py new file mode 100644 index 00000000..c0e9a5ee --- /dev/null +++ b/blog/docs/drf/viewsets.py @@ -0,0 +1,17 @@ +from django.utils.translation import gettext_lazy as _ +from drf_spectacular.utils import extend_schema +from rest_framework import status + +from core.docs.drf import BASE_ERRORS +from blog.serializers import PostSerializer + +POST_SCHEMA = { + "list": extend_schema( + summary=_("list all posts (read-only)"), + responses={status.HTTP_200_OK: PostSerializer(many=True), **BASE_ERRORS}, + ), + "retrieve": extend_schema( + summary=_("retrieve a single post (read-only)"), + responses={status.HTTP_200_OK: PostSerializer(), **BASE_ERRORS}, + ), +} diff --git a/blog/viewsets.py b/blog/viewsets.py index 48ff8c87..6f3f9fde 100644 --- a/blog/viewsets.py +++ b/blog/viewsets.py @@ -1,28 +1,30 @@ +from django.utils.translation import gettext_lazy as _ from django_filters.rest_framework import DjangoFilterBackend +from drf_spectacular.utils import extend_schema_view from rest_framework.viewsets import ReadOnlyModelViewSet from blog.filters import PostFilter from blog.models import Post from blog.serializers import PostSerializer +from blog.docs.drf.viewsets import POST_SCHEMA from core.permissions import EvibesPermission +@extend_schema_view(**POST_SCHEMA) class PostViewSet(ReadOnlyModelViewSet): # type: ignore [type-arg] - """ - Encapsulates operations for managing and retrieving Post entities in a read-only model view set. - - This class is tailored to handle Post objects that are active and allows filtration based on defined - filters. It integrates with Django's backend filtering system and ensures operations align with the - defined permissions. The view set also includes an additional "retrieve" permission configuration. - - Attributes: - serializer_class: Specifies the serializer to be used for Post objects. - permission_classes: Defines the permissions required to interact with this view set. - queryset: Determines the initial queryset, filtered to include only active Post objects. - filter_backends: Lists the backends to be used for filtering querysets. - filterset_class: Defines the set of filters used for filtering Post objects. - additional: Contains additional configuration, such as specific action permissions. - """ + __doc__ = _( # type: ignore [assignment] + "Encapsulates operations for managing and retrieving Post entities in a read-only model view set.\n\n" + "This class is tailored to handle Post objects that are active and allows filtration based on defined " + "filters. It integrates with Django's backend filtering system and ensures operations align with the " + "defined permissions. The view set also includes an additional 'retrieve' permission configuration.\n\n" + "Attributes:\n" + " serializer_class: Specifies the serializer to be used for Post objects.\n" + " permission_classes: Defines the permissions required to interact with this view set.\n" + " queryset: Determines the initial queryset, filtered to include only active Post objects.\n" + " filter_backends: Lists the backends to be used for filtering querysets.\n" + " filterset_class: Defines the set of filters used for filtering Post objects.\n" + " additional: Contains additional configuration, such as specific action permissions." + ) serializer_class = PostSerializer permission_classes = (EvibesPermission,) diff --git a/core/api_urls.py b/core/api_urls.py index f7dac933..cfa07308 100644 --- a/core/api_urls.py +++ b/core/api_urls.py @@ -5,11 +5,11 @@ from core.sitemaps import BrandSitemap, CategorySitemap, ProductSitemap, StaticP from core.views import ( CacheOperatorView, ContactUsView, + DownloadDigitalAssetView, GlobalSearchView, RequestCursedURLView, SupportedLanguagesView, WebsiteParametersView, - download_digital_asset_view, sitemap_detail, sitemap_index, ) @@ -78,7 +78,7 @@ urlpatterns = [ {"sitemaps": sitemaps}, name="sitemap-detail", ), - path("download//", download_digital_asset_view, name="download_digital_asset"), + path("download//", DownloadDigitalAssetView.as_view(), name="download_digital_asset"), path("search/", GlobalSearchView.as_view(), name="global_search"), path("app/cache/", CacheOperatorView.as_view(), name="cache_operator"), path("app/languages/", SupportedLanguagesView.as_view(), name="supported_languages"), diff --git a/core/docs/drf/viewsets.py b/core/docs/drf/viewsets.py index 5e77da6b..b0fc8de3 100644 --- a/core/docs/drf/viewsets.py +++ b/core/docs/drf/viewsets.py @@ -14,6 +14,8 @@ from core.serializers import ( AttributeSimpleSerializer, AttributeValueDetailSerializer, AttributeValueSimpleSerializer, + BrandDetailSerializer, + BrandSimpleSerializer, BulkAddOrderProductsSerializer, BulkAddWishlistProductSerializer, BulkRemoveOrderProductsSerializer, @@ -29,9 +31,21 @@ from core.serializers import ( OrderProductSimpleSerializer, OrderSimpleSerializer, ProductDetailSerializer, + ProductImageDetailSerializer, + ProductImageSimpleSerializer, ProductSimpleSerializer, + ProductTagDetailSerializer, + ProductTagSimpleSerializer, + PromoCodeDetailSerializer, + PromoCodeSimpleSerializer, + PromotionDetailSerializer, + PromotionSimpleSerializer, RemoveOrderProductSerializer, RemoveWishlistProductSerializer, + StockDetailSerializer, + StockSimpleSerializer, + VendorDetailSerializer, + VendorSimpleSerializer, WishlistDetailSerializer, WishlistSimpleSerializer, ) @@ -652,3 +666,196 @@ ORDER_PRODUCT_SCHEMA = { }, ), } + +BRAND_SCHEMA = { + "list": extend_schema( + summary=_("list all brands (simple view)"), + responses={status.HTTP_200_OK: BrandSimpleSerializer(many=True), **BASE_ERRORS}, + ), + "retrieve": extend_schema( + summary=_("retrieve a single brand (detailed view)"), + responses={status.HTTP_200_OK: BrandDetailSerializer(), **BASE_ERRORS}, + ), + "create": extend_schema( + summary=_("create a brand"), + responses={status.HTTP_201_CREATED: BrandDetailSerializer(), **BASE_ERRORS}, + ), + "destroy": extend_schema( + summary=_("delete a brand"), + responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS}, + ), + "update": extend_schema( + summary=_("rewrite an existing brand saving non-editables"), + responses={status.HTTP_200_OK: BrandDetailSerializer(), **BASE_ERRORS}, + ), + "partial_update": extend_schema( + summary=_("rewrite some fields of an existing brand saving non-editables"), + responses={status.HTTP_200_OK: BrandDetailSerializer(), **BASE_ERRORS}, + ), + "seo_meta": extend_schema( + summary=_("SEO Meta snapshot for brand"), + responses={status.HTTP_200_OK: SeoSnapshotSerializer(), **BASE_ERRORS}, + ), +} + +VENDOR_SCHEMA = { + "list": extend_schema( + summary=_("list all vendors (simple view)"), + responses={status.HTTP_200_OK: VendorSimpleSerializer(many=True), **BASE_ERRORS}, + ), + "retrieve": extend_schema( + summary=_("retrieve a single vendor (detailed view)"), + responses={status.HTTP_200_OK: VendorDetailSerializer(), **BASE_ERRORS}, + ), + "create": extend_schema( + summary=_("create a vendor"), + responses={status.HTTP_201_CREATED: VendorDetailSerializer(), **BASE_ERRORS}, + ), + "destroy": extend_schema( + summary=_("delete a vendor"), + responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS}, + ), + "update": extend_schema( + summary=_("rewrite an existing vendor saving non-editables"), + responses={status.HTTP_200_OK: VendorDetailSerializer(), **BASE_ERRORS}, + ), + "partial_update": extend_schema( + summary=_("rewrite some fields of an existing vendor saving non-editables"), + responses={status.HTTP_200_OK: VendorDetailSerializer(), **BASE_ERRORS}, + ), +} + +PRODUCT_IMAGE_SCHEMA = { + "list": extend_schema( + summary=_("list all product images (simple view)"), + responses={status.HTTP_200_OK: ProductImageSimpleSerializer(many=True), **BASE_ERRORS}, + ), + "retrieve": extend_schema( + summary=_("retrieve a single product image (detailed view)"), + responses={status.HTTP_200_OK: ProductImageDetailSerializer(), **BASE_ERRORS}, + ), + "create": extend_schema( + summary=_("create a product image"), + responses={status.HTTP_201_CREATED: ProductImageDetailSerializer(), **BASE_ERRORS}, + ), + "destroy": extend_schema( + summary=_("delete a product image"), + responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS}, + ), + "update": extend_schema( + summary=_("rewrite an existing product image saving non-editables"), + responses={status.HTTP_200_OK: ProductImageDetailSerializer(), **BASE_ERRORS}, + ), + "partial_update": extend_schema( + summary=_("rewrite some fields of an existing product image saving non-editables"), + responses={status.HTTP_200_OK: ProductImageDetailSerializer(), **BASE_ERRORS}, + ), +} + +PROMOCODE_SCHEMA = { + "list": extend_schema( + summary=_("list all promo codes (simple view)"), + responses={status.HTTP_200_OK: PromoCodeSimpleSerializer(many=True), **BASE_ERRORS}, + ), + "retrieve": extend_schema( + summary=_("retrieve a single promo code (detailed view)"), + responses={status.HTTP_200_OK: PromoCodeDetailSerializer(), **BASE_ERRORS}, + ), + "create": extend_schema( + summary=_("create a promo code"), + responses={status.HTTP_201_CREATED: PromoCodeDetailSerializer(), **BASE_ERRORS}, + ), + "destroy": extend_schema( + summary=_("delete a promo code"), + responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS}, + ), + "update": extend_schema( + summary=_("rewrite an existing promo code saving non-editables"), + responses={status.HTTP_200_OK: PromoCodeDetailSerializer(), **BASE_ERRORS}, + ), + "partial_update": extend_schema( + summary=_("rewrite some fields of an existing promo code saving non-editables"), + responses={status.HTTP_200_OK: PromoCodeDetailSerializer(), **BASE_ERRORS}, + ), +} + +PROMOTION_SCHEMA = { + "list": extend_schema( + summary=_("list all promotions (simple view)"), + responses={status.HTTP_200_OK: PromotionSimpleSerializer(many=True), **BASE_ERRORS}, + ), + "retrieve": extend_schema( + summary=_("retrieve a single promotion (detailed view)"), + responses={status.HTTP_200_OK: PromotionDetailSerializer(), **BASE_ERRORS}, + ), + "create": extend_schema( + summary=_("create a promotion"), + responses={status.HTTP_201_CREATED: PromotionDetailSerializer(), **BASE_ERRORS}, + ), + "destroy": extend_schema( + summary=_("delete a promotion"), + responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS}, + ), + "update": extend_schema( + summary=_("rewrite an existing promotion saving non-editables"), + responses={status.HTTP_200_OK: PromotionDetailSerializer(), **BASE_ERRORS}, + ), + "partial_update": extend_schema( + summary=_("rewrite some fields of an existing promotion saving non-editables"), + responses={status.HTTP_200_OK: PromotionDetailSerializer(), **BASE_ERRORS}, + ), +} + +STOCK_SCHEMA = { + "list": extend_schema( + summary=_("list all stocks (simple view)"), + responses={status.HTTP_200_OK: StockSimpleSerializer(many=True), **BASE_ERRORS}, + ), + "retrieve": extend_schema( + summary=_("retrieve a single stock (detailed view)"), + responses={status.HTTP_200_OK: StockDetailSerializer(), **BASE_ERRORS}, + ), + "create": extend_schema( + summary=_("create a stock record"), + responses={status.HTTP_201_CREATED: StockDetailSerializer(), **BASE_ERRORS}, + ), + "destroy": extend_schema( + summary=_("delete a stock record"), + responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS}, + ), + "update": extend_schema( + summary=_("rewrite an existing stock record saving non-editables"), + responses={status.HTTP_200_OK: StockDetailSerializer(), **BASE_ERRORS}, + ), + "partial_update": extend_schema( + summary=_("rewrite some fields of an existing stock record saving non-editables"), + responses={status.HTTP_200_OK: StockDetailSerializer(), **BASE_ERRORS}, + ), +} + +PRODUCT_TAG_SCHEMA = { + "list": extend_schema( + summary=_("list all product tags (simple view)"), + responses={status.HTTP_200_OK: ProductTagSimpleSerializer(many=True), **BASE_ERRORS}, + ), + "retrieve": extend_schema( + summary=_("retrieve a single product tag (detailed view)"), + responses={status.HTTP_200_OK: ProductTagDetailSerializer(), **BASE_ERRORS}, + ), + "create": extend_schema( + summary=_("create a product tag"), + responses={status.HTTP_201_CREATED: ProductTagDetailSerializer(), **BASE_ERRORS}, + ), + "destroy": extend_schema( + summary=_("delete a product tag"), + responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS}, + ), + "update": extend_schema( + summary=_("rewrite an existing product tag saving non-editables"), + responses={status.HTTP_200_OK: ProductTagDetailSerializer(), **BASE_ERRORS}, + ), + "partial_update": extend_schema( + summary=_("rewrite some fields of an existing product tag saving non-editables"), + responses={status.HTTP_200_OK: ProductTagDetailSerializer(), **BASE_ERRORS}, + ), +} diff --git a/core/views.py b/core/views.py index 355664fe..88125fad 100644 --- a/core/views.py +++ b/core/views.py @@ -301,66 +301,64 @@ class BuyAsBusinessView(APIView): ) -def download_digital_asset_view(request: HttpRequest, *args, **kwargs) -> FileResponse | JsonResponse: - try: - logger.debug(f"download_digital_asset_view: {kwargs}") - op_uuid = str(kwargs.get("order_product_uuid")) - if not op_uuid: - raise BadRequest(_("order_product_uuid is required")) - uuid = urlsafe_base64_decode(op_uuid).decode("utf-8") +class DownloadDigitalAssetView(APIView): + __doc__ = _( # type: ignore [assignment] + "Handles the downloading of a digital asset associated with an order.\n" + "This function attempts to serve the digital asset file located in the " + "storage directory of the project. If the file is not found, an HTTP 404 " + "error is raised to indicate the resource is unavailable." + ) - download = DigitalAssetDownload.objects.get(order_product__uuid=uuid) + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse | FileResponse: + try: + op_uuid = str(kwargs.get("order_product_uuid")) + if not op_uuid: + raise BadRequest(_("order_product_uuid is required")) + uuid = urlsafe_base64_decode(op_uuid).decode("utf-8") - if download.num_downloads >= 1: - raise BadRequest(_("you can only download the digital asset once")) + download = DigitalAssetDownload.objects.get(order_product__uuid=uuid) - if download.order_product.status != "FINISHED": - raise BadRequest(_("the order must be paid before downloading the digital asset")) + if download.num_downloads >= 1: + raise BadRequest(_("you can only download the digital asset once")) - download.num_downloads += 1 - download.save() + if download.order_product.status != "FINISHED": + raise BadRequest(_("the order must be paid before downloading the digital asset")) - if not download.order_product.product: - raise BadRequest(_("the order product does not have a product")) + download.num_downloads += 1 + download.save() - file_path = download.order_product.product.stocks.first().digital_asset.path # type: ignore [union-attr] + if not download.order_product.product: + raise BadRequest(_("the order product does not have a product")) - content_type, encoding = mimetypes.guess_type(file_path) - if not content_type: - content_type = "application/octet-stream" + file_path = download.order_product.product.stocks.first().digital_asset.path # type: ignore [union-attr] - response = FileResponse(open(file_path, "rb"), content_type=content_type) - filename = os.path.basename(file_path) - response["Content-Disposition"] = f'attachment; filename="{filename}"' - return response + content_type, encoding = mimetypes.guess_type(file_path) + if not content_type: + content_type = "application/octet-stream" - except BadRequest as e: - return JsonResponse(camelize({"error": str(e)}), status=400) + response = FileResponse(open(file_path, "rb"), content_type=content_type) + filename = os.path.basename(file_path) + response["Content-Disposition"] = f'attachment; filename="{filename}"' + return response - except DigitalAssetDownload.DoesNotExist: - return JsonResponse(camelize({"error": "Digital asset not found"}), status=404) + except BadRequest as e: + return Response(data=camelize({"error": str(e)}), status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - capture_exception(e) - return JsonResponse( - camelize( - { - "error": "An error occurred while trying to download the digital asset", - "traceback": traceback.format_exc() if settings.DEBUG else None, - "received": {"order_product_uuid": kwargs.get("order_product_uuid", "")}, - } - ), - status=500, - ) + except DigitalAssetDownload.DoesNotExist: + return Response(data=camelize({"error": "Digital asset not found"}), status=status.HTTP_404_NOT_FOUND) - -# noinspection PyTypeChecker -download_digital_asset_view.__doc__ = _( # type: ignore [assignment] - "Handles the downloading of a digital asset associated with an order.\n" - "This function attempts to serve the digital asset file located in the " - "storage directory of the project. If the file is not found, an HTTP 404 " - "error is raised to indicate the resource is unavailable." -) + except Exception as e: + capture_exception(e) + return Response( + data=camelize( + { + "error": "An error occurred while trying to download the digital asset", + "traceback": traceback.format_exc() if settings.DEBUG else None, + "received": {"order_product_uuid": kwargs.get("order_product_uuid", "")}, + } + ), + status=status.HTTP_503_SERVICE_UNAVAILABLE, + ) def favicon_view(request: HttpRequest, *args, **kwargs) -> HttpResponse | FileResponse: diff --git a/core/viewsets.py b/core/viewsets.py index 23007fe9..a2e39c02 100644 --- a/core/viewsets.py +++ b/core/viewsets.py @@ -31,11 +31,18 @@ from core.docs.drf.viewsets import ( ATTRIBUTE_GROUP_SCHEMA, ATTRIBUTE_SCHEMA, ATTRIBUTE_VALUE_SCHEMA, + BRAND_SCHEMA, CATEGORY_SCHEMA, FEEDBACK_SCHEMA, ORDER_PRODUCT_SCHEMA, ORDER_SCHEMA, + PRODUCT_IMAGE_SCHEMA, PRODUCT_SCHEMA, + PRODUCT_TAG_SCHEMA, + PROMOCODE_SCHEMA, + PROMOTION_SCHEMA, + STOCK_SCHEMA, + VENDOR_SCHEMA, WISHLIST_SCHEMA, ) from core.filters import AddressFilter, BrandFilter, CategoryFilter, FeedbackFilter, OrderFilter, ProductFilter @@ -313,6 +320,7 @@ class CategoryViewSet(EvibesViewSet): return Response(SeoSnapshotSerializer(payload).data) +@extend_schema_view(**BRAND_SCHEMA) class BrandViewSet(EvibesViewSet): __doc__ = _( "Represents a viewset for managing Brand instances. " @@ -554,6 +562,7 @@ class ProductViewSet(EvibesViewSet): return Response(SeoSnapshotSerializer(payload).data) +@extend_schema_view(**VENDOR_SCHEMA) class VendorViewSet(EvibesViewSet): __doc__ = _( "Represents a viewset for managing Vendor objects. " @@ -853,6 +862,7 @@ class OrderProductViewSet(EvibesViewSet): return Response(status=status.HTTP_404_NOT_FOUND) +@extend_schema_view(**PRODUCT_IMAGE_SCHEMA) class ProductImageViewSet(EvibesViewSet): __doc__ = _("Manages operations related to Product images in the application. ") @@ -865,6 +875,7 @@ class ProductImageViewSet(EvibesViewSet): } +@extend_schema_view(**PROMOCODE_SCHEMA) class PromoCodeViewSet(EvibesViewSet): __doc__ = _("Manages the retrieval and handling of PromoCode instances through various API actions.") @@ -886,6 +897,7 @@ class PromoCodeViewSet(EvibesViewSet): return qs.filter(user=user) +@extend_schema_view(**PROMOTION_SCHEMA) class PromotionViewSet(EvibesViewSet): __doc__ = _("Represents a view set for managing promotions. ") @@ -898,6 +910,7 @@ class PromotionViewSet(EvibesViewSet): } +@extend_schema_view(**STOCK_SCHEMA) class StockViewSet(EvibesViewSet): __doc__ = _("Handles operations related to Stock data in the system.") @@ -1100,6 +1113,7 @@ class AddressViewSet(EvibesViewSet): ) +@extend_schema_view(**PRODUCT_TAG_SCHEMA) class ProductTagViewSet(EvibesViewSet): __doc__ = _( "Handles operations related to Product Tags within the application. " diff --git a/payments/docs/drf/viewsets.py b/payments/docs/drf/viewsets.py new file mode 100644 index 00000000..ef24c2cd --- /dev/null +++ b/payments/docs/drf/viewsets.py @@ -0,0 +1,17 @@ +from django.utils.translation import gettext_lazy as _ +from drf_spectacular.utils import extend_schema +from rest_framework import status + +from core.docs.drf import BASE_ERRORS +from payments.serializers import TransactionSerializer + +TRANSACTION_SCHEMA = { + "list": extend_schema( + summary=_("list all transactions (read-only)"), + responses={status.HTTP_200_OK: TransactionSerializer(many=True), **BASE_ERRORS}, + ), + "retrieve": extend_schema( + summary=_("retrieve a single transaction (read-only)"), + responses={status.HTTP_200_OK: TransactionSerializer(), **BASE_ERRORS}, + ), +} diff --git a/payments/viewsets.py b/payments/viewsets.py index bb6a08ea..52cdc04a 100644 --- a/payments/viewsets.py +++ b/payments/viewsets.py @@ -1,10 +1,13 @@ from django.utils.translation import gettext_lazy as _ +from drf_spectacular.utils import extend_schema_view from rest_framework.viewsets import ReadOnlyModelViewSet from core.permissions import EvibesPermission, IsOwner from payments.serializers import TransactionSerializer +from payments.docs.drf.viewsets import TRANSACTION_SCHEMA +@extend_schema_view(**TRANSACTION_SCHEMA) class TransactionViewSet(ReadOnlyModelViewSet): # type: ignore __doc__ = _( # type: ignore [assignment] "ViewSet for handling read-only operations on the Transaction model. "