Browse Source

feat: add project base settings, Redis configuration, and user authentication endpoints

main
sina_sajjadi 3 weeks ago
parent
commit
8c66191dad
  1. 6
      .env.dev
  2. 5
      apps/account/custom_user_login.py
  3. 34
      apps/account/serializers/auth_serializers.py
  4. 4
      apps/account/urls_auth.py
  5. 9
      apps/account/views/auth_views.py
  6. 72
      apps/account/views/user.py
  7. 14
      config/redis_config.py
  8. 6
      config/settings/base.py
  9. 17
      config/settings/local.py
  10. 7
      manage.py
  11. 150
      utils/__init__.py
  12. 10
      utils/exceptions.py
  13. 2
      utils/redis.py

6
.env.dev

@ -22,3 +22,9 @@ PHONENUMBER_DEFAULT_REGION=US
CENTRIFUGO_API_URL=http://centrifugo:8000/api CENTRIFUGO_API_URL=http://centrifugo:8000/api
CENTRIFUGO_SECRET=super_secret_centrifugo_token_key_hmac_256 CENTRIFUGO_SECRET=super_secret_centrifugo_token_key_hmac_256
CENTRIFUGO_API_KEY=centrifugo_internal_api_access_key CENTRIFUGO_API_KEY=centrifugo_internal_api_access_key
# Resend Email Service
RESEND_API_KEY=re_7rkP5MPK_9gLDwdw1f23hzP3wsv2F6A8e
RESEND_FROM_EMAIL=onboarding@resend.dev

5
apps/account/custom_user_login.py

@ -11,8 +11,9 @@ class CustomLoginBackend(BaseBackend):
Authenticate with username email and phone_number. Authenticate with username email and phone_number.
""" """
def authenticate(self, request, username=None, password=None):
if user := self.get_user(username):
def authenticate(self, request, username=None, password=None, email=None, **kwargs):
identifier = username or email or kwargs.get('username') or kwargs.get('email')
if user := self.get_user(identifier):
if user.check_password(password): if user.check_password(password):
return user return user

34
apps/account/serializers/auth_serializers.py

@ -1,13 +1,16 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.password_validation import validate_password from django.contrib.auth.password_validation import validate_password
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from phonenumber_field.serializerfields import PhoneNumberField from phonenumber_field.serializerfields import PhoneNumberField
from apps.account.models.verification import VerificationDocument from apps.account.models.verification import VerificationDocument
User = get_user_model() User = get_user_model()
class RegisterRequestSerializer(serializers.Serializer): class RegisterRequestSerializer(serializers.Serializer):
email = serializers.EmailField(required=True) email = serializers.EmailField(required=True)
password = serializers.CharField(write_only=True, required=True, min_length=6) password = serializers.CharField(write_only=True, required=True, min_length=6)
@ -219,3 +222,34 @@ class PasswordResetRequestSerializer(serializers.Serializer):
def validate_new_password(self, value): def validate_new_password(self, value):
validate_password(value) validate_password(value)
return value return value
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
"""
JWT token obtain serializer that accepts either 'username' or 'email' as the credential.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'email' in self.fields:
self.fields['email'].required = False
if 'username' not in self.fields:
self.fields['username'] = serializers.CharField(required=False, write_only=True)
def to_internal_value(self, data):
if isinstance(data, dict):
mutable_data = data.copy()
identifier = mutable_data.get('email') or mutable_data.get('username')
if identifier:
mutable_data['email'] = identifier
mutable_data['username'] = identifier
return super().to_internal_value(mutable_data)
return super().to_internal_value(data)
def validate(self, attrs):
identifier = attrs.get('email') or attrs.get('username')
if not identifier:
raise serializers.ValidationError({"email": _("This field is required.")})
attrs['email'] = identifier
attrs['username'] = identifier
return super().validate(attrs)

4
apps/account/urls_auth.py

@ -1,10 +1,10 @@
from django.urls import path from django.urls import path
from rest_framework_simplejwt.views import ( from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView, TokenRefreshView,
TokenVerifyView, TokenVerifyView,
) )
from apps.account.views.auth_views import ( from apps.account.views.auth_views import (
CustomTokenObtainPairView,
RegisterView, RegisterView,
UserMeView, UserMeView,
VerificationDocumentListView, VerificationDocumentListView,
@ -14,7 +14,7 @@ from apps.account.views.auth_views import (
urlpatterns = [ urlpatterns = [
# SimpleJWT Token Lifecycle # SimpleJWT Token Lifecycle
path('token/', TokenObtainPairView.as_view(), name='auth_token_obtain_pair'),
path('token/', CustomTokenObtainPairView.as_view(), name='auth_token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='auth_token_refresh'), path('token/refresh/', TokenRefreshView.as_view(), name='auth_token_refresh'),
path('token/verify/', TokenVerifyView.as_view(), name='auth_token_verify'), path('token/verify/', TokenVerifyView.as_view(), name='auth_token_verify'),

9
apps/account/views/auth_views.py

@ -11,6 +11,7 @@ from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.views import TokenObtainPairView
from drf_spectacular.utils import extend_schema, OpenApiResponse, inline_serializer from drf_spectacular.utils import extend_schema, OpenApiResponse, inline_serializer
from apps.account.models.verification import VerificationDocument from apps.account.models.verification import VerificationDocument
@ -23,12 +24,20 @@ from apps.account.serializers.auth_serializers import (
VerificationDocumentUploadSerializer, VerificationDocumentUploadSerializer,
PasswordRecoverRequestSerializer, PasswordRecoverRequestSerializer,
PasswordResetRequestSerializer, PasswordResetRequestSerializer,
CustomTokenObtainPairSerializer,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
User = get_user_model() User = get_user_model()
class CustomTokenObtainPairView(TokenObtainPairView):
"""
Takes a set of user credentials (either 'username' or 'email' + password) and returns access & refresh token pair.
"""
serializer_class = CustomTokenObtainPairSerializer
class RegisterView(GenericAPIView): class RegisterView(GenericAPIView):
permission_classes = [AllowAny] permission_classes = [AllowAny]
serializer_class = RegisterRequestSerializer serializer_class = RegisterRequestSerializer

72
apps/account/views/user.py

@ -249,17 +249,22 @@ class UserRegisterView(CreateAPIView):
code = RedisManager.generate_otp_code() code = RedisManager.generate_otp_code()
logger.info(f"phone= {data['email']}") logger.info(f"phone= {data['email']}")
print(f'send {code}/{data["email"]}')
print("\n" + "=" * 50)
print(f" [OTP CODE]: {code} -> EMAIL: {data['email']}")
print("=" * 50 + "\n")
phone_number = RedisManager().add_to_redis(code, **data) phone_number = RedisManager().add_to_redis(code, **data)
try: try:
send_email([data['email']], code) send_email([data['email']], code)
except Exception as exp: except Exception as exp:
print(f'-exp-register-->{exp}') print(f'-exp-register-->{exp}')
resp_data = {
"user": data,
"message": "The otp code was sent to the user's email",
}
if settings.DEBUG:
resp_data["otp_code"] = code
return Response( return Response(
data= {
"user": data,
"message": "The otp code was sent to the user's email"
},
data=resp_data,
status=status.HTTP_202_ACCEPTED, status=status.HTTP_202_ACCEPTED,
) )
@ -417,23 +422,38 @@ class WebUserRegisterView(CreateAPIView):
code = RedisManager.generate_otp_code() code = RedisManager.generate_otp_code()
logger.info(f"phone= {data['email']}") logger.info(f"phone= {data['email']}")
print(f'send {code}/{data["email"]}')
print("\n" + "=" * 50)
print(f" [OTP CODE]: {code} -> EMAIL: {data['email']}")
print("=" * 50 + "\n")
# Store all registration data including password in Redis # Store all registration data including password in Redis
RedisManager().add_to_redis(code, **data) RedisManager().add_to_redis(code, **data)
email_sent = False
try: try:
send_email([data['email']], code)
email_sent = send_email([data['email']], code)
except Exception as exp: except Exception as exp:
print(f'-exp-register-->{exp}') print(f'-exp-register-->{exp}')
return Response(
data={
"user": {
"id": data.get('id'),
"fullname": data.get('fullname'),
"email": data.get('email'),
},
"message": "The otp code was sent to the user's email"
message = (
"The otp code was sent to the user's email"
if email_sent
else "OTP code generated, but email delivery failed. Please check your email configuration or server logs."
)
resp_data = {
"user": {
"id": data.get('id'),
"fullname": data.get('fullname'),
"email": data.get('email'),
}, },
status=status.HTTP_202_ACCEPTED,
"email_sent": email_sent,
"message": message,
}
if settings.DEBUG:
resp_data["otp_code"] = code
return Response(
data=resp_data,
status=status.HTTP_202_ACCEPTED if email_sent else status.HTTP_200_OK,
) )
@ -566,15 +586,19 @@ class UserRecoverPassword(CreateAPIView):
except Exception as exp: except Exception as exp:
print(f'-exp-register-->{exp}') print(f'-exp-register-->{exp}')
resp_data = {
"id": user.id,
"fullname": user.fullname,
"phone_number": str(user.phone_number) if user.phone_number else None,
"email": user.email if user.email else None,
"avatar": absolute_https_url(user.avatar.url, request) if user.avatar else None,
"message": "Forgot password code sent",
}
if settings.DEBUG:
resp_data["otp_code"] = code
return Response( return Response(
data= {
"id": user.id,
"fullname": user.fullname,
"phone_number": str(user.phone_number) if user.phone_number else None,
"email": user.email if user.email else None,
"avatar": absolute_https_url(user.avatar.url, request) if user.avatar else None,
"message": "Forgot password code sent"
},
data=resp_data,
status=status.HTTP_202_ACCEPTED, status=status.HTTP_202_ACCEPTED,
) )

14
config/redis_config.py

@ -1,15 +1,21 @@
from redis import Redis, ConnectionPool from redis import Redis, ConnectionPool
from django.conf import settings
from config.settings import base as settings
_pool = None
pool = ConnectionPool.from_url(url= settings.REDIS_URL,max_connections=100)
def get_redis_pool():
global _pool
if _pool is None:
redis_url = getattr(settings, 'REDIS_URL', 'redis://127.0.0.1:6379/0')
_pool = ConnectionPool.from_url(url=redis_url, max_connections=100)
return _pool
class RedisConfig: class RedisConfig:
def __init__(self): def __init__(self):
self.redis = Redis(connection_pool=pool, decode_responses=True)
self.redis = Redis(connection_pool=get_redis_pool(), decode_responses=True)

6
config/settings/base.py

@ -413,3 +413,9 @@ if SENTRY_DSN:
dsn=SENTRY_DSN, dsn=SENTRY_DSN,
send_default_pii=True, send_default_pii=True,
) )
# Resend Email Service
RESEND_API_KEY = env('RESEND_API_KEY', default='')
RESEND_FROM_EMAIL = env('RESEND_FROM_EMAIL', default='onboarding@resend.dev')

17
config/settings/local.py

@ -16,14 +16,20 @@ DATABASES = {
} }
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Cache: local-memory (no Redis needed)
# Redis & Cache (local Redis service on port 6379)
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
REDIS_URL = 'redis://127.0.0.1:6379/0'
CACHES = { CACHES = {
'default': { 'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': REDIS_URL,
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
} }
} }
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Celery: run tasks eagerly in-process (no broker needed) # Celery: run tasks eagerly in-process (no broker needed)
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
@ -48,3 +54,10 @@ CORS_ALLOW_ALL_ORIGINS = True
CENTRIFUGO_API_URL = 'http://localhost:8001/api' CENTRIFUGO_API_URL = 'http://localhost:8001/api'
CENTRIFUGO_SECRET = 'local-dev-hmac-secret' CENTRIFUGO_SECRET = 'local-dev-hmac-secret'
CENTRIFUGO_API_KEY = 'local-dev-api-key' CENTRIFUGO_API_KEY = 'local-dev-api-key'
# Resend Email Service
RESEND_API_KEY = env('RESEND_API_KEY', default='re_7rkP5MPK_9gLDwdw1f23hzP3wsv2F6A8e')
RESEND_FROM_EMAIL = env('RESEND_FROM_EMAIL', default='onboarding@resend.dev')
RESEND_SANDBOX_REDIRECT_TO = env('RESEND_SANDBOX_REDIRECT_TO', default='alisina.a.a50182@gmail.com')

7
manage.py

@ -3,6 +3,13 @@
import os import os
import sys import sys
# Ensure UTF-8 output encoding on Windows consoles
if sys.platform == 'win32':
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
if hasattr(sys.stderr, 'reconfigure'):
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""

150
utils/__init__.py

@ -112,108 +112,64 @@ def send_email(recipient, code):
logger.error(f"Cannot send email: recipient list is empty ({recipient})") logger.error(f"Cannot send email: recipient list is empty ({recipient})")
return False return False
from_email = getattr(settings, "RESEND_FROM_EMAIL", "Dovodi <info@imamjavad.online>")
if "<" not in from_email and "@" in from_email:
from_email = f"Dovodi <{from_email}>"
site_domain = getattr(settings, "SITE_DOMAIN", "https://dovodi.newhorizonco.uk/").rstrip('/')
subject = "Код подтверждения | Verification Code"
html_content = f"""
<!DOCTYPE html>
<html lang="ru">
from_email = getattr(settings, "RESEND_FROM_EMAIL", "onboarding@resend.dev")
if "onboarding@resend.dev" in from_email:
from_email = "onboarding@resend.dev"
elif "<" not in from_email and "@" in from_email:
from_email = f"Muslim Connect Hub <{from_email}>"
subject = f"Your Verification Code: {code}"
# Handle Resend sandbox restriction: onboarding@resend.dev can only deliver to the account owner's email
sandbox_redirect = getattr(settings, "RESEND_SANDBOX_REDIRECT_TO", "alisina.a.a50182@gmail.com")
sandbox_notice_html = ""
sandbox_notice_text = ""
if "onboarding@resend.dev" in from_email and sandbox_redirect:
non_sandbox_recipients = [r for r in to_emails if r.lower() != sandbox_redirect.lower()]
if non_sandbox_recipients:
orig_str = ", ".join(to_emails)
print(f"[Resend Sandbox Mode] Notice: Resend sandbox only delivers to {sandbox_redirect}.")
print(f"[Resend Sandbox Mode] Redirecting test email intended for [{orig_str}] -> {sandbox_redirect}")
subject = f"[Dev Test for {orig_str}] Your Verification Code: {code}"
sandbox_notice_html = f"""
<div style="background-color: #FEF3C7; border: 1px solid #F59E0B; border-radius: 8px; padding: 12px; margin-bottom: 20px; font-size: 13px; color: #92400E; text-align: left;">
<strong>Sandbox Redirection Notice:</strong><br/>
This email was intended for <code>{orig_str}</code>. Because Resend's free tier allows delivery only to the verified account email, it was routed to <code>{sandbox_redirect}</code> for testing.
</div>
"""
sandbox_notice_text = f"[Notice: Resend test sandbox redirecting from {orig_str} to {sandbox_redirect}]\n\n"
to_emails = [sandbox_redirect]
html_content = f"""<!DOCTYPE html>
<html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Код подтверждения | Verification Code</title>
<title>Verification Code</title>
</head> </head>
<body style="margin: 0; padding: 0; background-color: #F4F6F9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; color: #15171C; -webkit-font-smoothing: antialiased;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed; background-color: #F4F6F9; padding: 40px 16px;">
<tr>
<td align="center" valign="top">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 540px; background-color: #FFFFFF; border-radius: 16px; border: 1px solid #E2E8F0; box-shadow: 0 4px 20px -2px rgba(21, 23, 28, 0.06); overflow: hidden;">
<!-- Top Gradient Accent Line -->
<tr>
<td style="height: 4px; background: linear-gradient(90deg, #5172E1 0%, #1C458C 100%); line-height: 4px; font-size: 4px;">&nbsp;</td>
</tr>
<!-- Header with Logo -->
<tr>
<td style="padding: 32px 32px 20px 32px; text-align: center; background-color: #FAFAFC; border-bottom: 1px solid #F1F3F7;">
<img src="{site_domain}/static/images/dovoodi_logo.svg" alt="Dovodi" style="height: 38px; width: auto; max-width: 160px; display: inline-block;" />
</td>
</tr>
<!-- Main Body Section -->
<tr>
<td style="padding: 36px 36px 28px 36px;">
<!-- Security Badge -->
<div style="text-align: center; margin-bottom: 16px;">
<span style="display: inline-block; background-color: #EEF2FF; color: #3B66DE; border: 1px solid #D5DEFF; padding: 4px 12px; border-radius: 9999px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px;">
Безопасность / Security
</span>
</div>
<!-- Main Title -->
<h1 style="margin: 0 0 12px 0; font-size: 22px; font-weight: 800; color: #15171C; text-align: center; line-height: 1.3;">
Код подтверждения
</h1>
<!-- Intro Message -->
<p style="margin: 0 0 24px 0; font-size: 14px; color: #646A75; text-align: center; line-height: 1.6;">
Используйте следующий одноразовый код для входа یا подтверждения учетной записи в <strong>Dovodi</strong>:
</p>
<!-- OTP Box -->
<div style="text-align: center; margin: 28px 0; padding: 22px; background-color: #F8FAFD; border: 1.5px dashed #5172E1; border-radius: 14px;">
<span style="font-family: 'SF Pro Mono', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; font-size: 38px; font-weight: 800; color: #15171C; letter-spacing: 10px; display: inline-block;">
{code}
</span>
</div>
<!-- Expiry & Security Notice -->
<div style="background-color: #FFFBEB; border: 1px solid #FDE68A; border-radius: 10px; padding: 12px 16px; margin-bottom: 24px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td style="font-size: 12px; color: #92400E; line-height: 1.5;">
⏱ <strong>Внимание:</strong> Срок действия кода истекает через 5 минут. Если вы не запрашивали этот код, просто проигнорируйте это письмо.
</td>
</tr>
</table>
</div>
<p style="margin: 0; font-size: 13px; color: #8C93A0; text-align: center; line-height: 1.5;">
Никому не передавайте этот код в целях безопасности вашего аккаунта.
</p>
</td>
</tr>
<!-- Footer Section -->
<tr>
<td style="padding: 24px 36px; background-color: #F8F9FB; border-top: 1px solid #EEF0F4; text-align: center;">
<p style="margin: 0 0 6px 0; font-size: 13px; font-weight: 700; color: #15171C;">
Dovodi
</p>
<p style="margin: 0 0 12px 0; font-size: 12px; color: #646A75;">
<a href="https://dovodi.newhorizonco.uk/" style="color: #5172E1; text-decoration: none; font-weight: 600;">dovodi.newhorizonco.uk</a>
&nbsp;•&nbsp;
<a href="mailto:info@imamjavad.online" style="color: #5172E1; text-decoration: none; font-weight: 600;">info@imamjavad.online</a>
</p>
<p style="margin: 0; font-size: 11px; color: #9AA0AC; line-height: 1.4;">
© 2026 Dovodi. Все права защищены.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
<body style="margin: 0; padding: 40px 16px; background-color: #F8FAFC; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; color: #0F172A;">
<div style="max-width: 480px; margin: 0 auto; background: #FFFFFF; border-radius: 12px; border: 1px solid #E2E8F0; padding: 32px; text-align: center; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);">
{sandbox_notice_html}
<h2 style="margin: 0 0 16px; color: #1E293B; font-size: 22px;">Muslim Connect Hub</h2>
<p style="margin: 0 0 24px; color: #64748B; font-size: 15px; line-height: 1.5;">
Use the following verification code to complete your registration:
</p>
<div style="background-color: #F1F5F9; border-radius: 8px; padding: 18px 24px; margin: 24px 0; display: inline-block;">
<span style="font-family: monospace; font-size: 32px; font-weight: 700; letter-spacing: 6px; color: #0284C7;">
{code}
</span>
</div>
<p style="margin: 24px 0 0; color: #94A3B8; font-size: 13px;">
This code will expire in 5 minutes. If you did not request this code, please ignore this email.
</p>
</div>
</body> </body>
</html>
"""
</html>"""
text_content = ( text_content = (
f"Код подтверждения Dovodi: {code}\n\n"
f"Your Dovodi verification code is: {code}\n\n"
f"Срок действия кода истекает через 5 минут. Если вы не запрашивали этот код, проигнорируйте это письмо.\n"
f"Никому не передавайте этот код в целях безопасности вашего аккаунта."
f"{sandbox_notice_text}"
f"Muslim Connect Hub Verification Code: {code}\n\n"
f"This code will expire in 5 minutes. If you did not request this code, please ignore this email."
) )
payload = { payload = {
@ -226,9 +182,9 @@ def send_email(recipient, code):
try: try:
response = requests.post(url, headers=headers, json=payload, timeout=15) response = requests.post(url, headers=headers, json=payload, timeout=15)
print(f"[Resend API] status={response.status_code} response={response.text}")
if response.status_code >= 400: if response.status_code >= 400:
logger.error(f"Failed to send email via Resend: HTTP {response.status_code} - {response.text}") logger.error(f"Failed to send email via Resend: HTTP {response.status_code} - {response.text}")
print(f"Failed to send email via Resend: HTTP {response.status_code} - {response.text}")
return False return False
logger.info(f"Email OTP sent successfully via Resend to {to_emails}") logger.info(f"Email OTP sent successfully via Resend to {to_emails}")
return True return True

10
utils/exceptions.py

@ -29,7 +29,10 @@ def exception_handler(exc, context):
if response is not None: if response is not None:
# تعیین نوع خطاها # تعیین نوع خطاها
print(f'>>> {type(exc)}/ {exc}')
try:
print(f'>>> {type(exc)}/ {exc}')
except Exception:
pass
if isinstance(exc, ValidationError): if isinstance(exc, ValidationError):
error_code = "validation_error" error_code = "validation_error"
@ -44,7 +47,10 @@ def exception_handler(exc, context):
else: else:
# فرمت کردن خطاها به فرمت جدید # فرمت کردن خطاها به فرمت جدید
print(f'>>>>>> {response.data}')
try:
print(f'>>>>>> {response.data}')
except Exception:
pass
for field, errors in response.data.items(): for field, errors in response.data.items():
if isinstance(errors, list): if isinstance(errors, list):
for error in errors: for error in errors:

2
utils/redis.py

@ -63,7 +63,7 @@ class RedisManager(RedisConfig):
data = self.redis.get(key) data = self.redis.get(key)
if not data: if not data:
return None return None
raw = data.decode()
raw = data.decode() if hasattr(data, 'decode') else data
if raw.startswith('{'): if raw.startswith('{'):
return json.loads(raw) return json.loads(raw)
return self.__deserialize(raw) return self.__deserialize(raw)

Loading…
Cancel
Save