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
|
import logging
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from decimal import Decimal
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -19,9 +18,6 @@ from graphene import (
|
||||||
String,
|
String,
|
||||||
relay,
|
relay,
|
||||||
)
|
)
|
||||||
from graphene import (
|
|
||||||
Decimal as GrapheneDecimal,
|
|
||||||
)
|
|
||||||
from graphene.types.generic import GenericScalar
|
from graphene.types.generic import GenericScalar
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
from graphene_django.filter import (
|
from graphene_django.filter import (
|
||||||
|
|
@ -540,7 +536,7 @@ class ProductType(DjangoObjectType):
|
||||||
attribute_groups = DjangoFilterConnectionField(
|
attribute_groups = DjangoFilterConnectionField(
|
||||||
AttributeGroupType, description=_("attribute groups")
|
AttributeGroupType, description=_("attribute groups")
|
||||||
)
|
)
|
||||||
price = GrapheneDecimal(description=_("price"))
|
price = Float(description=_("price"))
|
||||||
quantity = Float(description=_("quantity"))
|
quantity = Float(description=_("quantity"))
|
||||||
feedbacks_count = Int(description=_("number of feedbacks"))
|
feedbacks_count = Int(description=_("number of feedbacks"))
|
||||||
personal_orders_only = Boolean(description=_("only available for personal orders"))
|
personal_orders_only = Boolean(description=_("only available for personal orders"))
|
||||||
|
|
@ -548,7 +544,7 @@ class ProductType(DjangoObjectType):
|
||||||
rating = Float(
|
rating = Float(
|
||||||
description=_("rating value from 1 to 10, inclusive, or 0 if not set.")
|
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:
|
class Meta:
|
||||||
model = Product
|
model = Product
|
||||||
|
|
@ -578,8 +574,8 @@ class ProductType(DjangoObjectType):
|
||||||
def resolve_description(self: Product, _info) -> str:
|
def resolve_description(self: Product, _info) -> str:
|
||||||
return render_markdown(self.description or "")
|
return render_markdown(self.description or "")
|
||||||
|
|
||||||
def resolve_price(self: Product, _info) -> Decimal:
|
def resolve_price(self: Product, _info) -> float:
|
||||||
return self.price or Decimal("0.0")
|
return self.price or 0.0
|
||||||
|
|
||||||
def resolve_rating(self: Product, _info) -> float:
|
def resolve_rating(self: Product, _info) -> float:
|
||||||
return self.rating or 0.0
|
return self.rating or 0.0
|
||||||
|
|
@ -659,7 +655,7 @@ class ProductType(DjangoObjectType):
|
||||||
def resolve_video(self: Product, info) -> str:
|
def resolve_video(self: Product, info) -> str:
|
||||||
return info.context.build_absolute_uri(self.video.url) if self.video else ""
|
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
|
return self.discount_price
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -770,13 +770,9 @@ class Product(NiceModel):
|
||||||
).order_by("-discount_percent")
|
).order_by("-discount_percent")
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def discount_price(self) -> Decimal:
|
def discount_price(self) -> Decimal | None:
|
||||||
promo = self.promos.first()
|
promo = self.promos.first()
|
||||||
return (
|
return (self.price / 100) * promo.discount_percent if promo else None
|
||||||
Decimal((self.price / 100) * promo.discount_percent)
|
|
||||||
if promo
|
|
||||||
else Decimal("0.0")
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rating(self) -> float:
|
def rating(self) -> float:
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,8 @@ import os
|
||||||
import traceback
|
import traceback
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
from decimal import Decimal
|
|
||||||
from os import getenv
|
from os import getenv
|
||||||
|
|
||||||
import orjson
|
|
||||||
import requests
|
import requests
|
||||||
from constance import config
|
from constance import config
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -137,7 +135,11 @@ class CustomGraphQLView(FileUploadGraphQLView):
|
||||||
def get_context(self, request):
|
def get_context(self, request):
|
||||||
return 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):
|
def _default(obj):
|
||||||
if isinstance(obj, Decimal):
|
if isinstance(obj, Decimal):
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import datetime
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
@ -8,11 +7,9 @@ from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.urls import path, reverse
|
from django.urls import path, reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from health_check.exceptions import HealthCheckException
|
|
||||||
from unfold.sites import UnfoldAdminSite
|
from unfold.sites import UnfoldAdminSite
|
||||||
|
|
||||||
from engine.vibes_auth.utils.otp import generate_otp_code, send_admin_otp_email_task
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -34,18 +31,6 @@ class OTPVerifyForm(forms.Form):
|
||||||
|
|
||||||
|
|
||||||
class SchonAdminSite(UnfoldAdminSite):
|
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:
|
def login(self, request: HttpRequest, extra_context=None) -> HttpResponse:
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
email = request.POST.get("username", "")
|
email = request.POST.get("username", "")
|
||||||
|
|
@ -53,23 +38,13 @@ class SchonAdminSite(UnfoldAdminSite):
|
||||||
user = authenticate(request, username=email, password=password)
|
user = authenticate(request, username=email, password=password)
|
||||||
|
|
||||||
if user is not None and user.is_staff: # ty: ignore[unresolved-attribute]
|
if user is not None and user.is_staff: # ty: ignore[unresolved-attribute]
|
||||||
if self._email_backend_configured():
|
code = generate_otp_code(user)
|
||||||
code = generate_otp_code(user)
|
send_admin_otp_email_task.delay(user_pk=str(user.pk), code=code)
|
||||||
send_admin_otp_email_task.delay(user_pk=str(user.pk), code=code)
|
request.session["_2fa_user_id"] = str(user.pk)
|
||||||
request.session["_2fa_user_id"] = str(user.pk)
|
messages.info(
|
||||||
messages.info(
|
request, _("A verification code has been sent to your email.")
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
login(request, user)
|
return HttpResponseRedirect(reverse("admin:verify-otp"))
|
||||||
return HttpResponseRedirect(reverse("admin:index"))
|
|
||||||
|
|
||||||
return super().login(request, extra_context)
|
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.Cache",
|
||||||
"health_check.DNS",
|
"health_check.DNS",
|
||||||
"health_check.Database",
|
"health_check.Database",
|
||||||
"schon.health_checks.DynamicMail",
|
"health_check.Mail",
|
||||||
"health_check.Storage",
|
"health_check.Storage",
|
||||||
"health_check.contrib.psutil.Disk",
|
"health_check.contrib.psutil.Disk",
|
||||||
"health_check.contrib.psutil.Memory",
|
"health_check.contrib.psutil.Memory",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue