From 6d8f5e3fec592b99f1cb82b1900ada6f45efa11e Mon Sep 17 00:00:00 2001 From: behruz-dev Date: Fri, 21 Nov 2025 19:17:04 +0500 Subject: [PATCH] kop narsalar qoshildi --- .env.example | 12 +++- config/conf/__init__.py | 2 + config/conf/rest_framework.py | 7 ++ config/conf/simple_jwt.py | 10 +++ config/env.py | 3 + config/settings/base.py | 32 +++++++--- config/urls.py | 34 +++++++++- core/apps/accounts/__init__.py | 0 core/apps/accounts/admin/__init__.py | 1 + core/apps/accounts/admin/user.py | 14 ++++ core/apps/accounts/apps.py | 9 +++ core/apps/accounts/migrations/0001_initial.py | 47 ++++++++++++++ .../0002_user_region_alter_user_is_active.py | 25 ++++++++ core/apps/accounts/migrations/__init__.py | 0 core/apps/accounts/models/__init__.py | 1 + core/apps/accounts/models/user.py | 16 +++++ core/apps/accounts/serializers/user.py | 41 ++++++++++++ core/apps/accounts/urls.py | 11 ++++ core/apps/accounts/views/user.py | 37 +++++++++++ core/apps/authentication/__init__.py | 0 core/apps/authentication/apps.py | 6 ++ .../authentication/migrations/__init__.py | 0 core/apps/authentication/serializers/login.py | 5 ++ .../authentication/serializers/response.py | 5 ++ core/apps/authentication/urls.py | 8 +++ core/apps/authentication/views/login.py | 45 +++++++++++++ core/apps/shared/__init__.py | 0 core/apps/shared/admin/__init__.py | 1 + core/apps/shared/admin/region.py | 10 +++ core/apps/shared/apps.py | 9 +++ core/apps/shared/middlewares/response_time.py | 14 ++++ core/apps/shared/migrations/0001_initial.py | 27 ++++++++ core/apps/shared/migrations/__init__.py | 0 core/apps/shared/models/__init__.py | 2 + core/apps/shared/models/base.py | 9 +++ core/apps/shared/models/region.py | 11 ++++ core/apps/shared/serializers/base.py | 16 +++++ core/apps/shared/serializers/region.py | 12 ++++ core/apps/shared/urls.py | 13 ++++ core/apps/shared/utils/response_mixin.py | 64 +++++++++++++++++++ core/apps/shared/views/region.py | 34 ++++++++++ requirements.txt | 18 +++++- 42 files changed, 595 insertions(+), 16 deletions(-) create mode 100644 config/conf/__init__.py create mode 100644 config/conf/rest_framework.py create mode 100644 config/conf/simple_jwt.py create mode 100644 config/env.py create mode 100644 core/apps/accounts/__init__.py create mode 100644 core/apps/accounts/admin/__init__.py create mode 100644 core/apps/accounts/admin/user.py create mode 100644 core/apps/accounts/apps.py create mode 100644 core/apps/accounts/migrations/0001_initial.py create mode 100644 core/apps/accounts/migrations/0002_user_region_alter_user_is_active.py create mode 100644 core/apps/accounts/migrations/__init__.py create mode 100644 core/apps/accounts/models/__init__.py create mode 100644 core/apps/accounts/models/user.py create mode 100644 core/apps/accounts/serializers/user.py create mode 100644 core/apps/accounts/urls.py create mode 100644 core/apps/accounts/views/user.py create mode 100644 core/apps/authentication/__init__.py create mode 100644 core/apps/authentication/apps.py create mode 100644 core/apps/authentication/migrations/__init__.py create mode 100644 core/apps/authentication/serializers/login.py create mode 100644 core/apps/authentication/serializers/response.py create mode 100644 core/apps/authentication/urls.py create mode 100644 core/apps/authentication/views/login.py create mode 100644 core/apps/shared/__init__.py create mode 100644 core/apps/shared/admin/__init__.py create mode 100644 core/apps/shared/admin/region.py create mode 100644 core/apps/shared/apps.py create mode 100644 core/apps/shared/middlewares/response_time.py create mode 100644 core/apps/shared/migrations/0001_initial.py create mode 100644 core/apps/shared/migrations/__init__.py create mode 100644 core/apps/shared/models/__init__.py create mode 100644 core/apps/shared/models/base.py create mode 100644 core/apps/shared/models/region.py create mode 100644 core/apps/shared/serializers/base.py create mode 100644 core/apps/shared/serializers/region.py create mode 100644 core/apps/shared/urls.py create mode 100644 core/apps/shared/utils/response_mixin.py create mode 100644 core/apps/shared/views/region.py diff --git a/.env.example b/.env.example index 3412444..cb66df6 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,9 @@ -POSTGRES_PASSWORD=20090912 -POSTGRES_USER=postgres -POSTGRES_DB=meridyn_pharma_db \ No newline at end of file +POSTGRES_PASSWORD= +POSTGRES_USER= +POSTGRES_DB= +POSTGRES_PORT= +POSTGRES_HOST= + +SECRET_KEY= +DEBUG=True +ALLOWED_HOSTS=localhost,127.0.0.1 diff --git a/config/conf/__init__.py b/config/conf/__init__.py new file mode 100644 index 0000000..7a6ccb5 --- /dev/null +++ b/config/conf/__init__.py @@ -0,0 +1,2 @@ +from .rest_framework import * +from .simple_jwt import * \ No newline at end of file diff --git a/config/conf/rest_framework.py b/config/conf/rest_framework.py new file mode 100644 index 0000000..27753b7 --- /dev/null +++ b/config/conf/rest_framework.py @@ -0,0 +1,7 @@ +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": [ + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ], +} \ No newline at end of file diff --git a/config/conf/simple_jwt.py b/config/conf/simple_jwt.py new file mode 100644 index 0000000..493d2a3 --- /dev/null +++ b/config/conf/simple_jwt.py @@ -0,0 +1,10 @@ +from datetime import timedelta + + +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(days=10), + "REFRESH_TOKEN_LIFETIME": timedelta(days=365), + "ROTATE_REFRESH_TOKENS": True, + "BLACKLIST_AFTER_ROTATION": True, + "UPDATE_LAST_LOGIN": True, +} \ No newline at end of file diff --git a/config/env.py b/config/env.py new file mode 100644 index 0000000..25bfeb4 --- /dev/null +++ b/config/env.py @@ -0,0 +1,3 @@ +from environ import Env + +env = Env() \ No newline at end of file diff --git a/config/settings/base.py b/config/settings/base.py index 6f5dfaf..96b9d52 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -1,11 +1,13 @@ from pathlib import Path +from config.env import env + BASE_DIR = Path(__file__).resolve().parent.parent.parent +env.read_env(BASE_DIR / '.env') - -SECRET_KEY = 'django-insecure-*44juz^a(752$j#8m7=w45$7fmi_z-t3e9v8kiojqay)b((gp3' -DEBUG = True -ALLOWED_HOSTS = [] +SECRET_KEY = env.str('SECRET_KEY') +DEBUG = env.bool('DEBUG') +ALLOWED_HOSTS = env.list('ALLOWED_HOSTS') INSTALLED_APPS = [ @@ -15,6 +17,15 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + # packages + 'drf_yasg', + 'rest_framework', + 'rest_framework_simplejwt', + + # local apps + 'core.apps.shared', + 'core.apps.authentication', + 'core.apps.accounts', ] MIDDLEWARE = [ @@ -25,6 +36,7 @@ MIDDLEWARE = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'core.apps.shared.middlewares.response_time.ResponseTimeMiddleware', ] ROOT_URLCONF = 'config.urls' @@ -50,11 +62,11 @@ WSGI_APPLICATION = 'config.wsgi.application' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'meridyn_pharma_db', - 'USER': 'postgres', - 'PASSWORD': '20090912', - 'HOST': 'db', - 'PORT': '5432', + 'NAME': env.str('POSTGRES_DB'), + 'USER': env.str('POSTGRES_USER'), + 'PASSWORD': env.str('POSTGRES_PASSWORD'), + 'HOST': env.str('POSTGRES_HOST'), + 'PORT': env.str('POSTGRES_PORT'), } } @@ -92,3 +104,5 @@ MEDIA_ROOT = BASE_DIR / 'resources/media' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +AUTH_USER_MODEL = 'accounts.User' \ No newline at end of file diff --git a/config/urls.py b/config/urls.py index 0253c44..e850a69 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,8 +1,23 @@ from django.contrib import admin -from django.urls import path +from django.urls import path, include from django.conf import settings from django.conf.urls.static import static +from drf_yasg.views import get_schema_view +from drf_yasg import openapi + +schema_view = get_schema_view( + openapi.Info( + title="Snippets API", + default_version='v1', + description="Test description", + terms_of_service="https://www.google.com/policies/terms/", + contact=openapi.Contact(email="contact@snippets.local"), + license=openapi.License(name="BSD License"), + ), + public=True, +) + urlpatterns = [ path('admin/', admin.site.urls), @@ -10,3 +25,20 @@ urlpatterns = [ urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + + +urlpatterns += [ + path('swagger/', schema_view.without_ui(cache_timeout=0), name='schema-json'), + path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), + path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), +] + +urlpatterns += [ + path('api/v1/', include( + [ + path('accounts/', include('core.apps.accounts.urls')), + path('authentication/', include('core.apps.authentication.urls')), + path('shared/', include('core.apps.shared.urls')), + ], + )), +] \ No newline at end of file diff --git a/core/apps/accounts/__init__.py b/core/apps/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/accounts/admin/__init__.py b/core/apps/accounts/admin/__init__.py new file mode 100644 index 0000000..82da278 --- /dev/null +++ b/core/apps/accounts/admin/__init__.py @@ -0,0 +1 @@ +from .user import * \ No newline at end of file diff --git a/core/apps/accounts/admin/user.py b/core/apps/accounts/admin/user.py new file mode 100644 index 0000000..f455a2a --- /dev/null +++ b/core/apps/accounts/admin/user.py @@ -0,0 +1,14 @@ +from django.contrib import admin +from django.contrib.auth.models import Group + +# accounts +from core.apps.accounts.models import User + + +@admin.register(User) +class UserAdmin(admin.ModelAdmin): + list_display = ['telegram_id', 'first_name', 'last_name', 'is_active'] + search_fields = ['telegram_id', 'first_name', 'last_name'] + + +admin.site.unregister(Group) \ No newline at end of file diff --git a/core/apps/accounts/apps.py b/core/apps/accounts/apps.py new file mode 100644 index 0000000..bf98a92 --- /dev/null +++ b/core/apps/accounts/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'core.apps.accounts' + + def ready(self): + import core.apps.accounts.admin \ No newline at end of file diff --git a/core/apps/accounts/migrations/0001_initial.py b/core/apps/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..5c3f7d9 --- /dev/null +++ b/core/apps/accounts/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# Generated by Django 5.2 on 2025-11-21 12:23 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('telegram_id', models.CharField(max_length=200, null=True)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/core/apps/accounts/migrations/0002_user_region_alter_user_is_active.py b/core/apps/accounts/migrations/0002_user_region_alter_user_is_active.py new file mode 100644 index 0000000..0b0680b --- /dev/null +++ b/core/apps/accounts/migrations/0002_user_region_alter_user_is_active.py @@ -0,0 +1,25 @@ +# Generated by Django 5.2 on 2025-11-21 12:35 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ('shared', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='region', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='regions', to='shared.region'), + ), + migrations.AlterField( + model_name='user', + name='is_active', + field=models.BooleanField(default=False), + ), + ] diff --git a/core/apps/accounts/migrations/__init__.py b/core/apps/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/accounts/models/__init__.py b/core/apps/accounts/models/__init__.py new file mode 100644 index 0000000..82da278 --- /dev/null +++ b/core/apps/accounts/models/__init__.py @@ -0,0 +1 @@ +from .user import * \ No newline at end of file diff --git a/core/apps/accounts/models/user.py b/core/apps/accounts/models/user.py new file mode 100644 index 0000000..8eedd03 --- /dev/null +++ b/core/apps/accounts/models/user.py @@ -0,0 +1,16 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser + +# shared +from core.apps.shared.models import BaseModel, Region + + +class User(AbstractUser, BaseModel): + telegram_id = models.CharField(max_length=200, null=True) + region = models.ForeignKey(Region, on_delete=models.SET_NULL, related_name='regions', null=True) + is_active = models.BooleanField(default=False) + + def __str__(self): + return f"{self.first_name} {self.last_name} - {self.telegram_id}" + + \ No newline at end of file diff --git a/core/apps/accounts/serializers/user.py b/core/apps/accounts/serializers/user.py new file mode 100644 index 0000000..c6ecf0f --- /dev/null +++ b/core/apps/accounts/serializers/user.py @@ -0,0 +1,41 @@ +# django +from django.db import transaction + +# rest framework +from rest_framework import serializers + +# accounts +from core.apps.accounts.models import User +# shared +from core.apps.shared.models import Region + + +class UserCreateSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = [ + 'first_name', 'last_name', 'telegram_id', 'region' + ] + + def validate(self, data): + if User.objects.filter(username=data['telegram_id']).exists(): + raise serializers.ValidationError("User mavjud") + region = Region.objects.filter(id=data['region']).first() + if not region: + raise serializers.ValidationError("Region topilmadi") + data['region'] = region + return data + + def create(self, validated_data): + with transaction.atomic(): + user = User.objects.create( + first_name=validated_data.get('first_name'), + last_name=validated_data.get('last_name'), + telegram_id=validated_data.get('telegram_id'), + region=validated_data.get('region'), + is_active=False, + username=validated_data.get('telegram_id'), + ) + user.region.users_count += 1 + user.region.save() + return user diff --git a/core/apps/accounts/urls.py b/core/apps/accounts/urls.py new file mode 100644 index 0000000..8df702e --- /dev/null +++ b/core/apps/accounts/urls.py @@ -0,0 +1,11 @@ +from django.urls import path, include + +from core.apps.accounts.views import user as user_views + +urlpatterns = [ + path('user/', include( + [ + path('create', user_views.RegisterUserApiView.as_view(), name='user-register-api'), + ], + )), +] \ No newline at end of file diff --git a/core/apps/accounts/views/user.py b/core/apps/accounts/views/user.py new file mode 100644 index 0000000..de75a2b --- /dev/null +++ b/core/apps/accounts/views/user.py @@ -0,0 +1,37 @@ +# rest framework +from rest_framework import generics + +# drf yasg +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi + +# accounts +from core.apps.accounts.models import User +from core.apps.accounts.serializers import user as serializers + +# shared +from core.apps.shared.utils.response_mixin import ResponseMixin +from core.apps.shared.serializers.base import BaseResponseSerializer, SuccessResponseSerializer + + +class RegisterUserApiView(generics.GenericAPIView, ResponseMixin): + serializer_class = serializers.UserCreateSerializer + queryset = User.objects.all() + + @swagger_auto_schema( + operation_description='Create User', + responses={ + 200: SuccessResponseSerializer(), + 400: BaseResponseSerializer(), + 500: BaseResponseSerializer(), + } + ) + def post(self, request): + try: + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(raise_exception=True): + serializer.save() + return self.success_response(message='Foydalanuvchi qoshildi', status_code=201) + return self.failure_response(data=serializer.errors, message='Foydalanuvchi qoshilmadi') + except Exception as e: + return self.error_response(data=str(e), message='xatolik') \ No newline at end of file diff --git a/core/apps/authentication/__init__.py b/core/apps/authentication/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/authentication/apps.py b/core/apps/authentication/apps.py new file mode 100644 index 0000000..928f422 --- /dev/null +++ b/core/apps/authentication/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AuthenticationConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'core.apps.authentication' diff --git a/core/apps/authentication/migrations/__init__.py b/core/apps/authentication/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/authentication/serializers/login.py b/core/apps/authentication/serializers/login.py new file mode 100644 index 0000000..e9820bf --- /dev/null +++ b/core/apps/authentication/serializers/login.py @@ -0,0 +1,5 @@ +from rest_framework import serializers + + +class LoginSerializer(serializers.Serializer): + telegram_id = serializers.CharField() \ No newline at end of file diff --git a/core/apps/authentication/serializers/response.py b/core/apps/authentication/serializers/response.py new file mode 100644 index 0000000..2ddb56f --- /dev/null +++ b/core/apps/authentication/serializers/response.py @@ -0,0 +1,5 @@ +from rest_framework import serializers + + +class LoginResponseSerializer(serializers.Serializer): + token = serializers.CharField(required=False, allow_null=True) \ No newline at end of file diff --git a/core/apps/authentication/urls.py b/core/apps/authentication/urls.py new file mode 100644 index 0000000..90ff1c2 --- /dev/null +++ b/core/apps/authentication/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from core.apps.authentication.views import login + + +urlpatterns = [ + path('login/', login.LoginApiView.as_view(), name='login-api'), +] \ No newline at end of file diff --git a/core/apps/authentication/views/login.py b/core/apps/authentication/views/login.py new file mode 100644 index 0000000..260482f --- /dev/null +++ b/core/apps/authentication/views/login.py @@ -0,0 +1,45 @@ +# rest framework +from rest_framework import generics +# rest framework simple jwt +from rest_framework_simplejwt.tokens import RefreshToken +# drf yasg +from drf_yasg.utils import swagger_auto_schema + +# shared +from core.apps.shared.utils.response_mixin import ResponseMixin +from core.apps.shared.serializers.base import BaseResponseSerializer, SuccessResponseSerializer +# accounts +from core.apps.accounts.models import User +# authentication +from core.apps.authentication.serializers.login import LoginSerializer +from core.apps.authentication.serializers import response as response_serializers + + +class LoginApiView(generics.GenericAPIView, ResponseMixin): + serializer_class = LoginSerializer + queryset = User.objects.all() + + @swagger_auto_schema( + responses={ + 200: SuccessResponseSerializer(data_serializer=response_serializers.LoginResponseSerializer()), + 400: BaseResponseSerializer(), + 500: BaseResponseSerializer(), + } + ) + def post(self, request): + try: + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(raise_exception=True): + telegram_id = serializer.validated_data.get('telegram_id') + user = User.objects.filter(telegram_id=telegram_id).first() + if not user: + return self.failure_response(message="User topilmadi") + if not user.is_active: + return self.failure_response(message="User tasdiqlanmagan") + + token = RefreshToken.for_user(user) + return self.success_response(data={'token': str(token.access_token)}) + + return self.failure_response(data=serializer.errors, message='siz tarafdan xatolik') + except Exception as e: + return self.error_response(data=str(e), message='xatolik') \ No newline at end of file diff --git a/core/apps/shared/__init__.py b/core/apps/shared/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/shared/admin/__init__.py b/core/apps/shared/admin/__init__.py new file mode 100644 index 0000000..08c1add --- /dev/null +++ b/core/apps/shared/admin/__init__.py @@ -0,0 +1 @@ +from .region import * \ No newline at end of file diff --git a/core/apps/shared/admin/region.py b/core/apps/shared/admin/region.py new file mode 100644 index 0000000..edb1cb6 --- /dev/null +++ b/core/apps/shared/admin/region.py @@ -0,0 +1,10 @@ +from django.contrib import admin + +# shared +from core.apps.shared.models import Region + + +@admin.register(Region) +class RegionAdmin(admin.ModelAdmin): + list_display = ['name', 'users_count'] + search_fields = ['name'] \ No newline at end of file diff --git a/core/apps/shared/apps.py b/core/apps/shared/apps.py new file mode 100644 index 0000000..9def33b --- /dev/null +++ b/core/apps/shared/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class SharedConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'core.apps.shared' + + def ready(self): + import core.apps.shared.admin \ No newline at end of file diff --git a/core/apps/shared/middlewares/response_time.py b/core/apps/shared/middlewares/response_time.py new file mode 100644 index 0000000..eec137a --- /dev/null +++ b/core/apps/shared/middlewares/response_time.py @@ -0,0 +1,14 @@ +import time + +from django.utils.deprecation import MiddlewareMixin + + +class ResponseTimeMiddleware(MiddlewareMixin): + def process_request(self, request): + request.start_time = time.time() + + def process_response(self, request, response): + if hasattr(request, "start_time"): + response_time = time.time() - request.start_time + response["X-Response-Time"] = f"{response_time:.3f}s" + return response diff --git a/core/apps/shared/migrations/0001_initial.py b/core/apps/shared/migrations/0001_initial.py new file mode 100644 index 0000000..93174d7 --- /dev/null +++ b/core/apps/shared/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2 on 2025-11-21 12:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Region', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=200, unique=True)), + ('users_count', models.PositiveIntegerField(default=0)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/core/apps/shared/migrations/__init__.py b/core/apps/shared/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/shared/models/__init__.py b/core/apps/shared/models/__init__.py new file mode 100644 index 0000000..52b0b29 --- /dev/null +++ b/core/apps/shared/models/__init__.py @@ -0,0 +1,2 @@ +from .base import * +from .region import Region \ No newline at end of file diff --git a/core/apps/shared/models/base.py b/core/apps/shared/models/base.py new file mode 100644 index 0000000..342a4e2 --- /dev/null +++ b/core/apps/shared/models/base.py @@ -0,0 +1,9 @@ +from django.db import models + + +class BaseModel(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True \ No newline at end of file diff --git a/core/apps/shared/models/region.py b/core/apps/shared/models/region.py new file mode 100644 index 0000000..7afe3fe --- /dev/null +++ b/core/apps/shared/models/region.py @@ -0,0 +1,11 @@ +from django.db import models + +from core.apps.shared.models.base import BaseModel + + +class Region(BaseModel): + name = models.CharField(max_length=200, unique=True) + users_count = models.PositiveIntegerField(default=0) + + def __str__(self): + return self.name \ No newline at end of file diff --git a/core/apps/shared/serializers/base.py b/core/apps/shared/serializers/base.py new file mode 100644 index 0000000..3b8785e --- /dev/null +++ b/core/apps/shared/serializers/base.py @@ -0,0 +1,16 @@ +from rest_framework import serializers + + +class BaseResponseSerializer(serializers.Serializer): + status_code = serializers.IntegerField() + message = serializers.CharField(required=False, allow_null=True) + data = serializers.JSONField(required=False, allow_null=True) + + +class SuccessResponseSerializer(BaseResponseSerializer): + def __init__(self, data_serializer=None, *args, **kwargs): + super().__init__(*args, **kwargs) + if data_serializer: + self.fields['data'] = data_serializer + else: + self.fields['data'] = serializers.JSONField(required=False) \ No newline at end of file diff --git a/core/apps/shared/serializers/region.py b/core/apps/shared/serializers/region.py new file mode 100644 index 0000000..9299b57 --- /dev/null +++ b/core/apps/shared/serializers/region.py @@ -0,0 +1,12 @@ +from rest_framework import serializers + +# shared +from core.apps.shared.models import Region + + +class RegionSerializer(serializers.ModelSerializer): + class Meta: + model = Region + fields = [ + 'id', 'name', 'created_at' + ] \ No newline at end of file diff --git a/core/apps/shared/urls.py b/core/apps/shared/urls.py new file mode 100644 index 0000000..43f0f03 --- /dev/null +++ b/core/apps/shared/urls.py @@ -0,0 +1,13 @@ +from django.urls import path, include + +# shared region view +from core.apps.shared.views import region as region_view + + +urlpatterns = [ + path('region/', include( + [ + path('list/', region_view.RegionListApiView.as_view(), name='region-list-api'), + ], + )), +] \ No newline at end of file diff --git a/core/apps/shared/utils/response_mixin.py b/core/apps/shared/utils/response_mixin.py new file mode 100644 index 0000000..686ac88 --- /dev/null +++ b/core/apps/shared/utils/response_mixin.py @@ -0,0 +1,64 @@ +from rest_framework import status +from rest_framework.response import Response + + +class ResponseMixin: + """ + Mixin to customize the response format + + Example Usage: + + class MyAPIView(APIView, APIViewResponseMixin): + def get(self, request): + try: + # Your logic here + data = {"key": "value"} + return self.success_response(data=data, message="Data retrieved successfully") + except Exception as e: + return self.error_response(message=str(e)) + + """ + + SUCCESS = "success" # 200 + FAILURE = "failure" # 400 + ERROR = "error" # 500 + + @classmethod + def success_response(cls, data=None, message=None, status_code=status.HTTP_200_OK): + """ + Returns Success Response + """ + response_data = {"status_code": status_code, "status": cls.SUCCESS} + if message is not None: + response_data["message"] = message + if data is not None: + response_data["data"] = data + return Response(response_data, status=status_code) + + @classmethod + def failure_response( + cls, data=None, message=None, status_code=status.HTTP_400_BAD_REQUEST + ): + """ + Returns Failure Response + """ + response_data = {"status_code": status_code, "status": cls.FAILURE} + if message is not None: + response_data["message"] = message + if data is not None: + response_data["data"] = data + return Response(response_data, status=status_code) + + @classmethod + def error_response( + cls, data=None, message=None, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR + ): + """ + Returns Error Response + """ + response_data = {"status_code": status_code, "status": cls.ERROR} + if message is not None: + response_data["message"] = message + if data is not None: + response_data["data"] = data + return Response(response_data, status=status_code) diff --git a/core/apps/shared/views/region.py b/core/apps/shared/views/region.py new file mode 100644 index 0000000..18ac213 --- /dev/null +++ b/core/apps/shared/views/region.py @@ -0,0 +1,34 @@ +# rest framework +from rest_framework import generics + +# drf yasg +from drf_yasg.utils import swagger_auto_schema + +# shared +from core.apps.shared.utils.response_mixin import ResponseMixin +from core.apps.shared.serializers.region import RegionSerializer +from core.apps.shared.models import Region +from core.apps.shared.serializers.base import BaseResponseSerializer, SuccessResponseSerializer + + + +class RegionListApiView(generics.ListAPIView, ResponseMixin): + serializer_class = RegionSerializer + queryset = Region.objects.order_by('name') + pagination_class = None + + @swagger_auto_schema( + operation_description="Get region list", + responses={ + 200: SuccessResponseSerializer(data_serializer=RegionSerializer()), + 400: BaseResponseSerializer(), + 500: BaseResponseSerializer(), + }, + + ) + def get(self, request): + try: + serializer = self.serializer_class(self.get_queryset(), many=True) + return self.success_response(data=serializer.data, message="malumotlar fetch qilindi") + except Exception as e: + return self.error_response(data=str(e), message="malumotlarni fetch qilishda xatolik") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8f0228c..5ed8122 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,18 @@ asgiref==3.11.0 +click==8.3.1 Django==5.2 +django-environ==0.12.0 +djangorestframework==3.16.1 +djangorestframework_simplejwt==5.5.1 +drf-yasg==1.21.11 +gunicorn==23.0.0 +h11==0.16.0 +inflection==0.5.1 +packaging==25.0 +psycopg2-binary==2.9.11 +PyJWT==2.10.1 +pytz==2025.2 +PyYAML==6.0.3 sqlparse==0.5.3 -uvicorn -gunicorn -psycopg2-binary \ No newline at end of file +uritemplate==4.2.0 +uvicorn==0.38.0