Compare commits
No commits in common. "114204b1052b040f399e75c2a24ffff77045234e" and "1f39535f05fd5068dcdbed390cc30442c8dbc2be" have entirely different histories.
114204b105
...
1f39535f05
6 changed files with 19 additions and 105 deletions
|
|
@ -1,6 +1,5 @@
|
|||
import logging
|
||||
from contextlib import suppress
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
|
|
@ -19,9 +18,6 @@ from graphene import (
|
|||
String,
|
||||
relay,
|
||||
)
|
||||
from graphene import (
|
||||
Decimal as GrapheneDecimal,
|
||||
)
|
||||
from graphene.types.generic import GenericScalar
|
||||
from graphene_django import DjangoObjectType
|
||||
from graphene_django.filter import (
|
||||
|
|
@ -540,7 +536,7 @@ class ProductType(DjangoObjectType):
|
|||
attribute_groups = DjangoFilterConnectionField(
|
||||
AttributeGroupType, description=_("attribute groups")
|
||||
)
|
||||
price = GrapheneDecimal(description=_("price"))
|
||||
price = Float(description=_("price"))
|
||||
quantity = Float(description=_("quantity"))
|
||||
feedbacks_count = Int(description=_("number of feedbacks"))
|
||||
personal_orders_only = Boolean(description=_("only available for personal orders"))
|
||||
|
|
@ -548,7 +544,7 @@ class ProductType(DjangoObjectType):
|
|||
rating = Float(
|
||||
description=_("rating value from 1 to 10, inclusive, or 0 if not set.")
|
||||
)
|
||||
discount_price = GrapheneDecimal(description=_("discount price"))
|
||||
discount_price = Float(description=_("discount price"))
|
||||
|
||||
class Meta:
|
||||
model = Product
|
||||
|
|
@ -578,8 +574,8 @@ class ProductType(DjangoObjectType):
|
|||
def resolve_description(self: Product, _info) -> str:
|
||||
return render_markdown(self.description or "")
|
||||
|
||||
def resolve_price(self: Product, _info) -> Decimal:
|
||||
return self.price or Decimal("0.0")
|
||||
def resolve_price(self: Product, _info) -> float:
|
||||
return self.price or 0.0
|
||||
|
||||
def resolve_rating(self: Product, _info) -> float:
|
||||
return self.rating or 0.0
|
||||
|
|
@ -659,7 +655,7 @@ class ProductType(DjangoObjectType):
|
|||
def resolve_video(self: Product, info) -> str:
|
||||
return info.context.build_absolute_uri(self.video.url) if self.video else ""
|
||||
|
||||
def resolve_discount_price(self: Product, _info) -> Decimal:
|
||||
def resolve_discount_price(self: Product, _info) -> float | None:
|
||||
return self.discount_price
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -770,13 +770,9 @@ class Product(NiceModel):
|
|||
).order_by("-discount_percent")
|
||||
|
||||
@cached_property
|
||||
def discount_price(self) -> Decimal:
|
||||
def discount_price(self) -> Decimal | None:
|
||||
promo = self.promos.first()
|
||||
return (
|
||||
Decimal((self.price / 100) * promo.discount_percent)
|
||||
if promo
|
||||
else Decimal("0.0")
|
||||
)
|
||||
return (self.price / 100) * promo.discount_percent if promo else None
|
||||
|
||||
@property
|
||||
def rating(self) -> float:
|
||||
|
|
|
|||
|
|
@ -4,10 +4,8 @@ import os
|
|||
import traceback
|
||||
from contextlib import suppress
|
||||
from datetime import date, timedelta
|
||||
from decimal import Decimal
|
||||
from os import getenv
|
||||
|
||||
import orjson
|
||||
import requests
|
||||
from constance import config
|
||||
from django.conf import settings
|
||||
|
|
@ -137,7 +135,11 @@ class CustomGraphQLView(FileUploadGraphQLView):
|
|||
def get_context(self, request):
|
||||
return request
|
||||
|
||||
def json_encode(self, request, d, pretty=False):
|
||||
@staticmethod
|
||||
def json_encode(request, d, pretty=False):
|
||||
from decimal import Decimal
|
||||
|
||||
import orjson
|
||||
|
||||
def _default(obj):
|
||||
if isinstance(obj, Decimal):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import datetime
|
||||
import logging
|
||||
|
||||
from django import forms
|
||||
|
|
@ -8,11 +7,9 @@ from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
|||
from django.shortcuts import render
|
||||
from django.urls import path, reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from health_check.exceptions import HealthCheckException
|
||||
from unfold.sites import UnfoldAdminSite
|
||||
|
||||
from engine.vibes_auth.utils.otp import generate_otp_code, send_admin_otp_email_task
|
||||
from schon.health_checks import DynamicMail
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -34,18 +31,6 @@ class OTPVerifyForm(forms.Form):
|
|||
|
||||
|
||||
class SchonAdminSite(UnfoldAdminSite):
|
||||
@staticmethod
|
||||
def _email_backend_configured() -> bool:
|
||||
"""Use DynamicMail health check to verify the SMTP backend is reachable."""
|
||||
from asgiref.sync import async_to_sync
|
||||
|
||||
try:
|
||||
check = DynamicMail(timeout=datetime.timedelta(seconds=5))
|
||||
async_to_sync(check.run)()
|
||||
except (HealthCheckException, Exception):
|
||||
return False
|
||||
return True
|
||||
|
||||
def login(self, request: HttpRequest, extra_context=None) -> HttpResponse:
|
||||
if request.method == "POST":
|
||||
email = request.POST.get("username", "")
|
||||
|
|
@ -53,23 +38,13 @@ class SchonAdminSite(UnfoldAdminSite):
|
|||
user = authenticate(request, username=email, password=password)
|
||||
|
||||
if user is not None and user.is_staff: # ty: ignore[unresolved-attribute]
|
||||
if self._email_backend_configured():
|
||||
code = generate_otp_code(user)
|
||||
send_admin_otp_email_task.delay(user_pk=str(user.pk), code=code)
|
||||
request.session["_2fa_user_id"] = str(user.pk)
|
||||
messages.info(
|
||||
request,
|
||||
_("A verification code has been sent to your email."),
|
||||
)
|
||||
return HttpResponseRedirect(reverse("admin:verify-otp"))
|
||||
|
||||
logger.warning(
|
||||
"Email backend is not configured — "
|
||||
"skipping OTP, logging in user %s directly.",
|
||||
user.pk,
|
||||
code = generate_otp_code(user)
|
||||
send_admin_otp_email_task.delay(user_pk=str(user.pk), code=code)
|
||||
request.session["_2fa_user_id"] = str(user.pk)
|
||||
messages.info(
|
||||
request, _("A verification code has been sent to your email.")
|
||||
)
|
||||
login(request, user)
|
||||
return HttpResponseRedirect(reverse("admin:index"))
|
||||
return HttpResponseRedirect(reverse("admin:verify-otp"))
|
||||
|
||||
return super().login(request, extra_context)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
"""Custom health checks that read runtime config from constance."""
|
||||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import datetime
|
||||
import logging
|
||||
import smtplib
|
||||
|
||||
from health_check.base import HealthCheck
|
||||
from health_check.exceptions import ServiceUnavailable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class DynamicMail(HealthCheck):
|
||||
"""
|
||||
Check that the SMTP backend configured via constance can open a connection.
|
||||
|
||||
Unlike the built-in ``health_check.Mail``, this check reads host, port,
|
||||
credentials, and TLS/SSL flags from ``constance.config`` so it always
|
||||
reflects whatever an administrator has set at runtime.
|
||||
|
||||
Args:
|
||||
timeout: Timeout for the SMTP connection attempt.
|
||||
|
||||
"""
|
||||
|
||||
timeout: datetime.timedelta = dataclasses.field(
|
||||
default=datetime.timedelta(seconds=15), repr=False
|
||||
)
|
||||
|
||||
def _check_connection(self) -> None:
|
||||
"""Blocking SMTP probe — runs inside a thread."""
|
||||
from engine.core.utils import get_dynamic_email_connection
|
||||
|
||||
connection = get_dynamic_email_connection()
|
||||
connection.timeout = int(self.timeout.total_seconds())
|
||||
logger.debug("Trying to open dynamic mail connection.")
|
||||
try:
|
||||
connection.open()
|
||||
except smtplib.SMTPException as e:
|
||||
raise ServiceUnavailable(
|
||||
"Failed to open connection with SMTP server"
|
||||
) from e
|
||||
except ConnectionRefusedError as e:
|
||||
raise ServiceUnavailable("Connection refused") from e
|
||||
except OSError as e:
|
||||
raise ServiceUnavailable("Could not connect to mail server") from e
|
||||
finally:
|
||||
connection.close()
|
||||
logger.debug("Dynamic mail connection established successfully.")
|
||||
|
||||
async def run(self) -> None:
|
||||
await asyncio.to_thread(self._check_connection)
|
||||
|
|
@ -29,7 +29,7 @@ urlpatterns = [
|
|||
"health_check.Cache",
|
||||
"health_check.DNS",
|
||||
"health_check.Database",
|
||||
"schon.health_checks.DynamicMail",
|
||||
"health_check.Mail",
|
||||
"health_check.Storage",
|
||||
"health_check.contrib.psutil.Disk",
|
||||
"health_check.contrib.psutil.Memory",
|
||||
|
|
|
|||
Loading…
Reference in a new issue