From 7a7f58ba69bd6198717da000f40fa8507daaf972 Mon Sep 17 00:00:00 2001 From: Sayyid Hamid Mahdavi Date: Tue, 29 Jul 2025 19:59:11 +0330 Subject: [PATCH] more utils --- apps/sample/__init__.py | 0 apps/sample/admin.py | 9 ++ apps/sample/apps.py | 6 + apps/sample/filters.py | 11 ++ apps/sample/forms.py | 12 ++ apps/sample/migrations/__init__.py | 0 apps/sample/models.py | 21 ++++ apps/sample/serializers.py | 14 +++ apps/sample/tasks.py | 7 ++ apps/sample/tests.py | 3 + apps/sample/urls.py | 19 ++++ apps/sample/views.py | 63 +++++++++++ main/{settings => }/settings.py | 175 +++++++++++++++++++++-------- main/settings/__init__.py | 2 - main/settings/base_settings.py | 123 -------------------- celery.sh => scripts/celery.sh | 0 run.sh => scripts/run.sh | 0 utils/exceptions.py | 54 +++++++++ utils/i18n.py | 14 +++ utils/router.py | 47 ++++++++ 20 files changed, 410 insertions(+), 170 deletions(-) create mode 100644 apps/sample/__init__.py create mode 100644 apps/sample/admin.py create mode 100644 apps/sample/apps.py create mode 100644 apps/sample/filters.py create mode 100644 apps/sample/forms.py create mode 100644 apps/sample/migrations/__init__.py create mode 100644 apps/sample/models.py create mode 100644 apps/sample/serializers.py create mode 100644 apps/sample/tasks.py create mode 100644 apps/sample/tests.py create mode 100644 apps/sample/urls.py create mode 100644 apps/sample/views.py rename main/{settings => }/settings.py (65%) delete mode 100644 main/settings/__init__.py delete mode 100644 main/settings/base_settings.py rename celery.sh => scripts/celery.sh (100%) rename run.sh => scripts/run.sh (100%) create mode 100644 utils/exceptions.py create mode 100644 utils/i18n.py create mode 100644 utils/router.py diff --git a/apps/sample/__init__.py b/apps/sample/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/sample/admin.py b/apps/sample/admin.py new file mode 100644 index 0000000..54cc0fe --- /dev/null +++ b/apps/sample/admin.py @@ -0,0 +1,9 @@ +from functools import update_wrapper + +from django.contrib import admin + +from .models import Sample + + +admin.site.register(Sample) + diff --git a/apps/sample/apps.py b/apps/sample/apps.py new file mode 100644 index 0000000..2760e8d --- /dev/null +++ b/apps/sample/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SampleConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.sample' diff --git a/apps/sample/filters.py b/apps/sample/filters.py new file mode 100644 index 0000000..33050b7 --- /dev/null +++ b/apps/sample/filters.py @@ -0,0 +1,11 @@ +import django_filters + +from .models import Sample + + +class SampleFilter(django_filters.FilterSet): + class Meta: + model = Sample + fields = { + 'data': ['exact'], + } diff --git a/apps/sample/forms.py b/apps/sample/forms.py new file mode 100644 index 0000000..16da384 --- /dev/null +++ b/apps/sample/forms.py @@ -0,0 +1,12 @@ +from django import forms +from django.utils.translation import gettext_lazy as _ + +from .models import Sample + + +class SampleForm(forms.ModelForm): + # state = forms.ChoiceField(required=False, disabled=True) + class Meta: + model = Sample + fields = ['data', 'user', 'application'] + readonly_fields = ['user', 'application'] diff --git a/apps/sample/migrations/__init__.py b/apps/sample/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/sample/models.py b/apps/sample/models.py new file mode 100644 index 0000000..44bc886 --- /dev/null +++ b/apps/sample/models.py @@ -0,0 +1,21 @@ +from django.conf import settings +from django.db import models +from oauth2_provider.settings import oauth2_settings + +from utils.models import BaseModel + +from django.utils.translation import gettext_lazy as _ + + +class SampleQuerySet(models.QuerySet): + pass + + +class Sample(BaseModel): + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) + application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT, + related_name='+', null=True, blank=True) + + data = models.JSONField(null=True, blank=True) + + objects = SampleQuerySet.as_manager() diff --git a/apps/sample/serializers.py b/apps/sample/serializers.py new file mode 100644 index 0000000..317aebc --- /dev/null +++ b/apps/sample/serializers.py @@ -0,0 +1,14 @@ +from rest_framework import serializers + +from .models import Sample + + +class SampleSerializer(serializers.ModelSerializer): + class Meta: + model = Sample + fields = ( + "user", + "application", + "data" + ) + read_only_fields = ('user','application') \ No newline at end of file diff --git a/apps/sample/tasks.py b/apps/sample/tasks.py new file mode 100644 index 0000000..3a55344 --- /dev/null +++ b/apps/sample/tasks.py @@ -0,0 +1,7 @@ +from main import celery_app + + +@celery_app.task +def sample_application_task(sample_uuid): + from .models import Sample + sample = Sample.objects.get(uuid=sample_uuid) diff --git a/apps/sample/tests.py b/apps/sample/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/sample/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/sample/urls.py b/apps/sample/urls.py new file mode 100644 index 0000000..673b603 --- /dev/null +++ b/apps/sample/urls.py @@ -0,0 +1,19 @@ +from rest_framework import routers +from rest_framework.routers import DefaultRouter, DynamicRoute, Route + +from utils.router import ProfileRouter +from . import views +from django.urls import NoReverseMatch, path, re_path, include + +app_name = 'sample' + +application_router = DefaultRouter() +application_router.register(r'', views.ApplicationSampleViewSet, basename='application' ) + +user_router = ProfileRouter() +user_router.register('', views.UserSampleViewSet, basename='user') + +urlpatterns = [ + path('api/v1/application//', include(application_router.urls), name='application'), + path('api/v1/user/', include(user_router.urls), name='user'), +] diff --git a/apps/sample/views.py b/apps/sample/views.py new file mode 100644 index 0000000..72095e3 --- /dev/null +++ b/apps/sample/views.py @@ -0,0 +1,63 @@ +from django.shortcuts import render + +from django.http import Http404 +from django.shortcuts import render +from oauth2_provider.contrib.rest_framework import TokenMatchesOASRequirements +from rest_framework import viewsets, mixins +from rest_framework.generics import get_object_or_404 +from rest_framework.viewsets import GenericViewSet + +from .models import PushUser, PushMessage +from .serializers import PushUserSerializer, PushMessageSerializer +from apps.gooyal_oauth2.models import Application +from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements +from apps.gooyal_oauth2.utils import get_application +from ..users.models import User + + +from django.shortcuts import render + +from django.http import Http404 +from django.shortcuts import render +from django.urls import reverse +from oauth2_provider.contrib.rest_framework import TokenMatchesOASRequirements +from rest_framework import viewsets, mixins +from rest_framework.generics import get_object_or_404 +from rest_framework.viewsets import GenericViewSet + +from .models import Sample +from .serializers import SampleSerializer +from apps.gooyal_oauth2.models import Application +from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements +from apps.gooyal_oauth2.utils import get_application +from ..users.models import User + + +class ApplicationSampleViewSet(mixins.ListModelMixin, + mixins.CreateModelMixin, + GenericViewSet): + queryset = Sample.objects.all() + serializer_class = SampleSerializer + + permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements] + required_alternate_scopes = { + "POST": [["service_template.application.user-sample:submit"]], + "GET": [["service_template.application.user-sample:list-retrieve"]], + } + + def perform_create(self, serializer): + user_uuid = self.kwargs.get('user_uuid') + user = User.objects.get(uuid=user_uuid) + application = get_application(self.request) + instance = serializer.save(user=user, application=application) + + +class UserSampleViewSet(mixins.RetrieveModelMixin, + GenericViewSet): + queryset = Sample.objects.all() + serializer_class = SampleSerializer + permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements] + required_alternate_scopes = { + "GET": [["service_template.user.self-sample:list-retrieve"]], + } + diff --git a/main/settings/settings.py b/main/settings.py similarity index 65% rename from main/settings/settings.py rename to main/settings.py index dbba9c9..c5c02c3 100644 --- a/main/settings/settings.py +++ b/main/settings.py @@ -1,16 +1,42 @@ -from .base_settings import * -from django.utils.translation import gettext_lazy as _ +""" +Django settings for main project. + +Generated by 'django-admin startproject' using Django 5.1.4. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.1/ref/settings/ +""" + +from pathlib import Path from decouple import config -SECRET_KEY = config('SECRET_KEY', default=SECRET_KEY) +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = config('SECRET_KEY', default='django-insecure-ync*tk)t*bz3zzbza&xvw6cl+wk6+@gk^@)2)-pp=^&v%@knib') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = config('DEBUG', default=DEBUG, cast=bool) +DEBUG = config('DEBUG', default=True, cast=bool) ALLOWED_HOSTS = ['*'] +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', -INSTALLED_APPS += [ # pip installed apps 'drf_spectacular', 'oauth2_provider', @@ -27,6 +53,7 @@ INSTALLED_APPS += [ 'utils', 'apps.users', 'apps.gooyal_oauth2', + 'apps.activity', ] MIDDLEWARE = [ @@ -40,7 +67,83 @@ MIDDLEWARE = [ 'oauth2_provider.middleware.OAuth2TokenMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'utils.exceptions.ErrorMiddleware', ] +ROOT_URLCONF = 'main.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'main.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases + +DATABASES = { + 'default': { + "ENGINE": "django.db.backends.postgresql", + 'NAME': config('DB_NAME'), + 'USER': config('DB_USER'), + 'PASSWORD': config('DB_PASSWORD'), + 'HOST': config('DB_HOST', '127.0.0.1'), + 'PORT': config('DB_PORT', ''), + } +} + +# Password validation +# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# Internationalization +# https://docs.djangoproject.com/en/5.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.1/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +####################### + OAUTH2_PROVIDER_APPLICATION_MODEL = "gooyal_oauth2.Application" OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "gooyal_oauth2.AccessToken" @@ -48,12 +151,12 @@ OAUTH2_PROVIDER_ID_TOKEN_MODEL = "gooyal_oauth2.IDToken" OAUTH2_PROVIDER_GRANT_MODEL = "gooyal_oauth2.Grant" OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "gooyal_oauth2.RefreshToken" -BASE_OAUTH2_PROVIDER_PUBLIC_URL = config("BASE_OAUTH2_PROVIDER_PUBLIC_URL") -BASE_OAUTH2_PROVIDER_PRIVATE_URL = config("BASE_OAUTH2_PROVIDER_PRIVATE_URL") +OAUTH2_PROVIDER_PUBLIC_URL = config("OAUTH2_PROVIDER_PUBLIC_URL") +OAUTH2_PROVIDER_PRIVATE_URL = config("OAUTH2_PROVIDER_PRIVATE_URL") -CLIENT_ID = config('CLIENT_ID') -CLIENT_SECRET = config('CLIENT_SECRET') -SCOPES = config('SCOPES', default='') +OAUTH2_CLIENT_ID = config('OAUTH2_CLIENT_ID') +OAUTH2_CLIENT_SECRET = config('OAUTH2_CLIENT_SECRET') +OAUTH2_SCOPES = config('OAUTH2_SCOPES', default='') OAUTH2_PROVIDER = { # this is the list of available scopes # 'SCOPES_BACKEND_CLASS': 'apps.gooyal_oauth2.scopes.Scopes', @@ -62,9 +165,9 @@ OAUTH2_PROVIDER = { # 'groups': 'Access to your groups', # 'introspection': 'Introspect token scope'}, 'OAUTH2_VALIDATOR_CLASS': 'apps.gooyal_oauth2.validators.OAuth2Validator', - 'RESOURCE_SERVER_INTROSPECTION_URL': BASE_OAUTH2_PROVIDER_PRIVATE_URL + '/introspect/', + 'RESOURCE_SERVER_INTROSPECTION_URL': OAUTH2_PROVIDER_PRIVATE_URL + '/introspect/', # 'RESOURCE_SERVER_AUTH_TOKEN': '3yUqsWtwKYKHnfivFcJu', # OR this but not both: - 'RESOURCE_SERVER_INTROSPECTION_CREDENTIALS': (CLIENT_ID, CLIENT_SECRET), + 'RESOURCE_SERVER_INTROSPECTION_CREDENTIALS': (OAUTH2_CLIENT_ID, OAUTH2_CLIENT_SECRET), } REST_FRAMEWORK = { @@ -84,41 +187,24 @@ REST_FRAMEWORK = { 'DEFAULT_THROTTLE_RATES': { # 'otp_min': '1/min', # 'otp_day': '20/day', - } + }, + 'EXCEPTION_HANDLER': 'utils.exceptions.ExceptionHandler', } - -TEMPLATES[0]['DIRS'] += [BASE_DIR / 'templates'] - AUTHENTICATION_BACKENDS = ( 'oauth2_provider.backends.OAuth2Backend', 'django.contrib.auth.backends.ModelBackend', ) - -DATABASES = { - 'default': { - "ENGINE": "django.contrib.gis.db.backends.postgis", - 'NAME': config('DB_NAME'), - 'USER': config('DB_USER'), - 'PASSWORD': config('DB_PASSWORD'), - 'HOST': config('DB_HOST', '127.0.0.1'), - 'PORT': config('DB_PORT', ''), - } -} - USE_L10N = True - LOCALE_PATHS = [ BASE_DIR / 'locale', ] - MEDIA_URL = 'media/' MEDIA_ROOT = BASE_DIR / 'media' - if DEBUG == True: STATICFILES_DIRS = ( BASE_DIR / 'static', @@ -126,8 +212,6 @@ if DEBUG == True: else: STATIC_ROOT = BASE_DIR / 'static' - - LOGIN_URL = '/users/login/request/' LOGIN_REDIRECT_URL = '/' @@ -135,7 +219,7 @@ CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" CRISPY_TEMPLATE_PACK = "bootstrap5" SPECTACULAR_SETTINGS = { - 'TITLE': 'merchants', + 'TITLE': 'service', 'DESCRIPTION': 'service api reference', 'VERSION': '1.0.0', 'SERVE_INCLUDE_SCHEMA': False, @@ -191,8 +275,10 @@ LOGGING = { 'timeout': 1, # Post request timeout, default is 0.5. Optional 'url': 'https://loki.winsoo.ir/loki/api/v1/push', # Loki url. Defaults to localhost. Optional. # 'auth': ("user", "password"), # Basic auth to authenticate with loki. Default is None (i.e. no auth). Optional - 'tags': { "app": "ads", "env": "production" }, # Tags / Labels to attach to the log. Optional, but strongly encoraged to use. - 'mode': 'thread', # Push mode. Can be 'sync' or 'thread'. Sync is blocking, thread is non-blocking. Defaults to sync. Optional. + 'tags': {"app": "ads", "env": "production"}, + # Tags / Labels to attach to the log. Optional, but strongly encoraged to use. + 'mode': 'thread', + # Push mode. Can be 'sync' or 'thread'. Sync is blocking, thread is non-blocking. Defaults to sync. Optional. }, # 'file': { # 'level': 'DEBUG', @@ -233,8 +319,10 @@ STORAGES = { MINIO_ENDPOINT = config('MINIO_ENDPOINT', default='drive.gooyal.com') MINIO_USE_HTTPS = config('MINIO_USE_HTTPS', default=True, cast=bool) -MINIO_EXTERNAL_ENDPOINT = config('MINIO_EXTERNAL_ENDPOINT', default='drive.gooyal.com') # Default is same as MINIO_ENDPOINT -MINIO_EXTERNAL_ENDPOINT_USE_HTTPS = config('MINIO_EXTERNAL_ENDPOINT_USE_HTTPS', default=True, cast=bool) # Default is same as MINIO_USE_HTTPS +MINIO_EXTERNAL_ENDPOINT = config('MINIO_EXTERNAL_ENDPOINT', + default='drive.gooyal.com') # Default is same as MINIO_ENDPOINT +MINIO_EXTERNAL_ENDPOINT_USE_HTTPS = config('MINIO_EXTERNAL_ENDPOINT_USE_HTTPS', default=True, + cast=bool) # Default is same as MINIO_USE_HTTPS MINIO_REGION = None # Default is set to None MINIO_ACCESS_KEY = config('MINIO_ACCESS_KEY') MINIO_SECRET_KEY = config('MINIO_SECRET_KEY') @@ -272,17 +360,14 @@ MINIO_HTTP_CLIENT: urllib3.poolmanager.PoolManager = urllib3.PoolManager( ) ) - REDIS_BASE_URL = config('REDIS_BASE_URL') - - -CELERY={ - "broker_url" :REDIS_BASE_URL, - "result_backend" : REDIS_BASE_URL, +CELERY = { + "broker_url": REDIS_BASE_URL, + "result_backend": REDIS_BASE_URL, } CELERY_TASK_TRACK_STARTED = True CELERY_TASK_TIME_LIMIT = 30 * 60 CELERY_BROKER_URL = REDIS_BASE_URL -CELERY_RESULT_BACKEND = REDIS_BASE_URL \ No newline at end of file +CELERY_RESULT_BACKEND = REDIS_BASE_URL diff --git a/main/settings/__init__.py b/main/settings/__init__.py deleted file mode 100644 index f3c291d..0000000 --- a/main/settings/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .base_settings import * -from .settings import * \ No newline at end of file diff --git a/main/settings/base_settings.py b/main/settings/base_settings.py deleted file mode 100644 index 838b000..0000000 --- a/main/settings/base_settings.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -Django settings for main project. - -Generated by 'django-admin startproject' using Django 5.1.4. - -For more information on this file, see -https://docs.djangoproject.com/en/5.1/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/5.1/ref/settings/ -""" - -from pathlib import Path - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-ync*tk)t*bz3zzbza&xvw6cl+wk6+@gk^@)2)-pp=^&v%@knib' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] - - -# Application definition - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -] - -ROOT_URLCONF = 'main.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'main.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/5.1/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } -} - - -# Password validation -# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - - -# Internationalization -# https://docs.djangoproject.com/en/5.1/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/5.1/howto/static-files/ - -STATIC_URL = 'static/' - -# Default primary key field type -# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/celery.sh b/scripts/celery.sh similarity index 100% rename from celery.sh rename to scripts/celery.sh diff --git a/run.sh b/scripts/run.sh similarity index 100% rename from run.sh rename to scripts/run.sh diff --git a/utils/exceptions.py b/utils/exceptions.py new file mode 100644 index 0000000..e1956ac --- /dev/null +++ b/utils/exceptions.py @@ -0,0 +1,54 @@ +import logging + +from rest_framework.exceptions import APIException +from rest_framework.views import exception_handler as drf_exception_handler + +logger = logging.getLogger(__name__) + +class ExceptionHandler: + + @classmethod + def __call__(cls, exc, context): + # پاسخ پیش‌فرض DRF را دریافت می‌کنیم + response = drf_exception_handler(exc, context) + + if response is not None: + # ساختار دلخواه خود را تعریف می‌کنیم + response_data = { + 'success': False, + 'status_code': response.status_code, + 'message': 'An error occurred', + 'details': {} + } + + if isinstance(exc, APIException): + response_data['message'] = exc.detail if isinstance(exc.detail, str) else "Validation error" + response_data['details'] = exc.detail if isinstance(exc.detail, dict) else {} + + response.data = response_data + + return response + + +# در فایل middleware.py +from django.http import JsonResponse + +class ErrorMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + return response + + def process_exception(self, request, exception): + logger.exception(exception) + return JsonResponse( + { + "success": False, + "status_code": 500, + "message": "Internal server error", + "details": str(exception) # فقط در حالت توسعه! در تولید بهتر است لاگ شود. + }, + status=500 + ) \ No newline at end of file diff --git a/utils/i18n.py b/utils/i18n.py new file mode 100644 index 0000000..481aec4 --- /dev/null +++ b/utils/i18n.py @@ -0,0 +1,14 @@ +from django.utils import translation + +class ForceAdminLanguageMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if request.path.startswith('/admin/'): + translation.activate('en') + request.LANGUAGE_CODE = 'en' + else: + translation.activate('fa') + request.LANGUAGE_CODE = 'fa' + return self.get_response(request) \ No newline at end of file diff --git a/utils/router.py b/utils/router.py new file mode 100644 index 0000000..41aa31e --- /dev/null +++ b/utils/router.py @@ -0,0 +1,47 @@ +from rest_framework.routers import DefaultRouter, Route, DynamicRoute + + +class ProfileRouter(DefaultRouter): + routes = [ + # List route. + Route( + url=r'^{prefix}{trailing_slash}$', + mapping={ + 'get': 'list', + 'post': 'create' + }, + name='{basename}-list', + detail=False, + initkwargs={'suffix': 'List'} + ), + # Dynamically generated list routes. Generated using + # @action(detail=False) decorator on methods of the viewset. + DynamicRoute( + url=r'^{prefix}/{url_path}{trailing_slash}$', + name='{basename}-{url_name}', + detail=False, + initkwargs={} + ), + # Detail route. + Route( + url=r'^{prefix}{trailing_slash}$', + mapping={ + 'get': 'retrieve', + 'put': 'update', + 'patch': 'partial_update', + 'delete': 'destroy' + }, + name='{basename}-detail', + detail=True, + initkwargs={'suffix': 'Instance'} + ), + # Dynamically generated detail routes. Generated using + # @action(detail=True) decorator on methods of the viewset. + DynamicRoute( + url=r'^{prefix}/{url_path}{trailing_slash}$', + name='{basename}-{url_name}', + detail=True, + initkwargs={} + ), + ] +