kop narsalar qoshildi

This commit is contained in:
behruz-dev
2025-11-21 19:17:04 +05:00
parent cb0cdfde26
commit 6d8f5e3fec
42 changed files with 595 additions and 16 deletions

View File

@@ -1,3 +1,9 @@
POSTGRES_PASSWORD=20090912 POSTGRES_PASSWORD=
POSTGRES_USER=postgres POSTGRES_USER=
POSTGRES_DB=meridyn_pharma_db POSTGRES_DB=
POSTGRES_PORT=
POSTGRES_HOST=
SECRET_KEY=
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1

2
config/conf/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from .rest_framework import *
from .simple_jwt import *

View File

@@ -0,0 +1,7 @@
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}

10
config/conf/simple_jwt.py Normal file
View File

@@ -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,
}

3
config/env.py Normal file
View File

@@ -0,0 +1,3 @@
from environ import Env
env = Env()

View File

@@ -1,11 +1,13 @@
from pathlib import Path from pathlib import Path
from config.env import env
BASE_DIR = Path(__file__).resolve().parent.parent.parent BASE_DIR = Path(__file__).resolve().parent.parent.parent
env.read_env(BASE_DIR / '.env')
SECRET_KEY = env.str('SECRET_KEY')
SECRET_KEY = 'django-insecure-*44juz^a(752$j#8m7=w45$7fmi_z-t3e9v8kiojqay)b((gp3' DEBUG = env.bool('DEBUG')
DEBUG = True ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')
ALLOWED_HOSTS = []
INSTALLED_APPS = [ INSTALLED_APPS = [
@@ -15,6 +17,15 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
# packages
'drf_yasg',
'rest_framework',
'rest_framework_simplejwt',
# local apps
'core.apps.shared',
'core.apps.authentication',
'core.apps.accounts',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -25,6 +36,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'core.apps.shared.middlewares.response_time.ResponseTimeMiddleware',
] ]
ROOT_URLCONF = 'config.urls' ROOT_URLCONF = 'config.urls'
@@ -50,11 +62,11 @@ WSGI_APPLICATION = 'config.wsgi.application'
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql', 'ENGINE': 'django.db.backends.postgresql',
'NAME': 'meridyn_pharma_db', 'NAME': env.str('POSTGRES_DB'),
'USER': 'postgres', 'USER': env.str('POSTGRES_USER'),
'PASSWORD': '20090912', 'PASSWORD': env.str('POSTGRES_PASSWORD'),
'HOST': 'db', 'HOST': env.str('POSTGRES_HOST'),
'PORT': '5432', 'PORT': env.str('POSTGRES_PORT'),
} }
} }
@@ -92,3 +104,5 @@ MEDIA_ROOT = BASE_DIR / 'resources/media'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = 'accounts.User'

View File

@@ -1,8 +1,23 @@
from django.contrib import admin from django.contrib import admin
from django.urls import path from django.urls import path, include
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static 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 = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
@@ -10,3 +25,20 @@ urlpatterns = [
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += [
path('swagger<format>/', 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')),
],
)),
]

View File

View File

@@ -0,0 +1 @@
from .user import *

View File

@@ -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)

View File

@@ -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

View File

@@ -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()),
],
),
]

View File

@@ -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),
),
]

View File

@@ -0,0 +1 @@
from .user import *

View File

@@ -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}"

View File

@@ -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

View File

@@ -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'),
],
)),
]

View File

@@ -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')

View File

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class AuthenticationConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'core.apps.authentication'

View File

@@ -0,0 +1,5 @@
from rest_framework import serializers
class LoginSerializer(serializers.Serializer):
telegram_id = serializers.CharField()

View File

@@ -0,0 +1,5 @@
from rest_framework import serializers
class LoginResponseSerializer(serializers.Serializer):
token = serializers.CharField(required=False, allow_null=True)

View File

@@ -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'),
]

View File

@@ -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')

View File

View File

@@ -0,0 +1 @@
from .region import *

View File

@@ -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']

9
core/apps/shared/apps.py Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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,
},
),
]

View File

View File

@@ -0,0 +1,2 @@
from .base import *
from .region import Region

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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'
]

13
core/apps/shared/urls.py Normal file
View File

@@ -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'),
],
)),
]

View File

@@ -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)

View File

@@ -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")

View File

@@ -1,6 +1,18 @@
asgiref==3.11.0 asgiref==3.11.0
click==8.3.1
Django==5.2 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 sqlparse==0.5.3
uvicorn uritemplate==4.2.0
gunicorn uvicorn==0.38.0
psycopg2-binary