diff --git a/apps/core/admin.py b/apps/core/admin.py index 925adee..b6e2a4b 100644 --- a/apps/core/admin.py +++ b/apps/core/admin.py @@ -1,4 +1,12 @@ from django.contrib import admin +from .models import Config + + +class ConfigAdmin(admin.ModelAdmin): + list_display = ['key', 'value', 'value_type', 'get_value', 'description', 'comment'] + +admin.site.register(Config, ConfigAdmin) + diff --git a/apps/core/decorators.py b/apps/core/decorators.py new file mode 100644 index 0000000..3c547bc --- /dev/null +++ b/apps/core/decorators.py @@ -0,0 +1,31 @@ +from functools import wraps + +from django.utils import timezone + +from apps.core.models import Config, ConfigValueChoices +from utils.exceptions import ServiceUnavailable + + +def service_availability(key:str): + def with_params(view_func): + @wraps(view_func) + def wrapper(self, request, *args, **kwargs): + print(key) + print(f"Executing {view_func.__name__} action") + + service_is_available = Config.get_value_of(f"SERVICE_IS_AVAILABLE_{key.upper()}", "True", ConfigValueChoices.BOOLEAN) + config = Config.objects.get(key=f"SERVICE_IS_AVAILABLE_{key.upper()}") + if not service_is_available: + raise ServiceUnavailable( + detail={ + # "code": 'service_unavailable_at_now', + # "timestamp": timezone.now().isoformat(), + "message": config.description or "این سرویس در حال حاضر در دسترس نیست" + }, + ) + + return view_func(self, request, *args, **kwargs) + + return wrapper + + return with_params diff --git a/apps/core/migrations/0001_initial.py b/apps/core/migrations/0001_initial.py new file mode 100644 index 0000000..96131c5 --- /dev/null +++ b/apps/core/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 5.1.4 on 2026-03-29 13:38 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Config', + fields=[ + ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)), + ('updated_at', models.DateTimeField(auto_now=True, db_index=True)), + ('key', models.CharField(db_index=True, max_length=255, unique=True, verbose_name='key')), + ('value', models.TextField(blank=True, null=True, verbose_name='value')), + ('value_type', models.CharField(choices=[('INT', 'Integer'), ('FLOAT', 'Float'), ('BOOLEAN', 'Boolean'), ('STRING', 'String'), ('DATE', 'Date'), ('DATETIME', 'DateTime'), ('JSON', 'JSON')], max_length=64)), + ('description', models.TextField(blank=True, null=True, verbose_name='description')), + ('comment', models.TextField(blank=True, null=True, verbose_name='comment')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/core/migrations/__init__.py b/apps/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/core/models.py b/apps/core/models.py index 6461db4..8b14c79 100644 --- a/apps/core/models.py +++ b/apps/core/models.py @@ -6,6 +6,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from django.core.cache import cache + class BaseModel(models.Model): id = None uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True) @@ -16,7 +17,6 @@ class BaseModel(models.Model): abstract = True - class BasePGModel(models.Model): created_at = models.DateTimeField(auto_now_add=True, db_index=True) updated_at = models.DateTimeField(auto_now=True, db_index=True) @@ -39,8 +39,64 @@ class BasePGModel(models.Model): abstract = True +class ConfigValueChoices(models.TextChoices): + INT = 'INT', _('Integer') + FLOAT = 'FLOAT', _('Float') + BOOLEAN = 'BOOLEAN', _('Boolean') + STRING = 'STRING', _('String') + DATE = 'DATE', _('Date') + DATETIME = 'DATETIME', _('DateTime') + JSON = 'JSON', _('JSON') +class Config(BaseModel): + key = models.CharField(_('key'), max_length=255, unique=True, db_index=True) + value = models.TextField(_('value'), null=True, blank=True) + value_type = models.CharField(choices=ConfigValueChoices.choices, max_length=64) + description = models.TextField(_('description'), null=True, blank=True) + comment = models.TextField(_('comment'), null=True, blank=True) + def get_value(self): + return Config.get_value_of(self.key) + @staticmethod + def type_cast(value, value_type): + try: + if value_type == ConfigValueChoices.STRING.value: + value = str(value) + + elif value_type == ConfigValueChoices.BOOLEAN.value: + value = value.lower() == 'true' + + elif value_type == ConfigValueChoices.DATE.value: + value = datetime.fromisoformat(value) + + elif value_type == ConfigValueChoices.DATETIME.value: + value = datetime.fromisoformat(value) + + elif value_type == ConfigValueChoices.JSON.value: + value = json.loads(value) + + elif value_type == ConfigValueChoices.INT.value: + value = int(value) + + elif value_type == ConfigValueChoices.FLOAT.value: + value = float(value) + + except Exception as e: + value = None + + return value + + @staticmethod + def get_value_of(key, default=None, casting_type=None): + # TODO: use cache + config, created = Config.objects.get_or_create(key=key, defaults={'value': default}) + + if config.value is None: + return default + + value = Config.type_cast(config.value, casting_type or config.value_type) + + return value diff --git a/apps/core/views.py b/apps/core/views.py index ac24b77..15f5953 100644 --- a/apps/core/views.py +++ b/apps/core/views.py @@ -10,7 +10,8 @@ from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView -from utils.exceptions import StatusError +from apps.core.decorators import service_availability +from utils.exceptions import ServiceUnavailable # Create your views here. @@ -22,16 +23,8 @@ class HomeView(TemplateView): class StatusView(APIView): permission_classes = [AllowAny] + @service_availability('status') def get(self, request, format=None): - if self.request.GET.get('raise_error'): - raise StatusError( - # code='service_unavailable_at_now', - detail={ - # "code": 'service_unavailable_at_now', - "timestamp": timezone.now().isoformat(), - "message": "برخی سرویس‌های خارجی در دسترس نیستند لطفا بعدا تلاش کنید" - }, - ) data = { "status": "ok", diff --git a/utils/exceptions.py b/utils/exceptions.py index 1b36b45..d23f1ec 100644 --- a/utils/exceptions.py +++ b/utils/exceptions.py @@ -84,7 +84,7 @@ class UnprocessableEntity(APIException): default_detail = 'The request was well-formed but cannot be processed due to semantic errors.' default_code = 'unprocessable_entity' -class StatusError(APIException): +class ServiceUnavailable(APIException): status_code = status.HTTP_503_SERVICE_UNAVAILABLE default_ = _('SERVICE_UNAVAILABLE') default_code = 'service_unavailable'