diff --git a/core/apps/accounts/serializers/user/update.py b/core/apps/accounts/serializers/user/update.py new file mode 100644 index 0000000..80bc413 --- /dev/null +++ b/core/apps/accounts/serializers/user/update.py @@ -0,0 +1,46 @@ +# django +from django.db import transaction + +# rest framework +from rest_framework import serializers + + +# accounts +from core.apps.accounts.models import User, Role + + +class UpdateUserSerializer(serializers.Serializer): + first_name = serializers.CharField(required=False) + last_name = serializers.CharField(required=False) + role_id = serializers.IntegerField() + phone_number = serializers.CharField(required=False) + username = serializers.CharField(required=False) + password = serializers.CharField(required=False) + is_active = serializers.BooleanField(default=True) + profile_image = serializers.ImageField(required=False) + + def validate(self, data): + role = Role.objects.filter(id=data['role_id']).first() + if not role: + raise serializers.ValidationError({"role": "Role not found"}) + if data.get('username'): + if User.objects.filter(username=data['username']).exists(): + raise serializers.ValidationError({"username": "User with this username already exist, please try another username"}) + data['role'] = role + return data + + def update(self, instance, validated_data): + with transaction.atomic(): + instance.first_name = validated_data.get('first_name', instance.first_name) + instance.last_name = validated_data.get('last_name', instance.last_name) + instance.role = validated_data.get('role', instance.role) + instance.phone_number = validated_data.get('phone_number', instance.phone_number) + instance.username = validated_data.get('username', instance.username) + instance.is_active = validated_data.get('is_active', instance.is_active) + instance.profile_image = validated_data.get('profile_image', instance.profile_image) + + if validated_data.get('password'): + instance.set_password(validated_data.get('password')) + + instance.save() + return instance \ No newline at end of file diff --git a/core/apps/accounts/urls.py b/core/apps/accounts/urls.py index 6c66607..43a9ff3 100644 --- a/core/apps/accounts/urls.py +++ b/core/apps/accounts/urls.py @@ -10,6 +10,8 @@ from rest_framework.routers import DefaultRouter from core.apps.accounts.views.user import UserViewSet from core.apps.accounts.views.user.create import CreateUserApiView from core.apps.accounts.views.user.list import ListUserApiView +from core.apps.accounts.views.user.update import UpdateUserApiView +from core.apps.accounts.views.user.delete import SoftDeleteUserApiView, HardDeleteUserApiView # ------- auth ------ from core.apps.accounts.views.auth.login import LoginApiView @@ -19,6 +21,9 @@ urlpatterns = [ [ path('create/', CreateUserApiView.as_view(), name='user-create-api'), path('list/', ListUserApiView.as_view(), name='user-list-api'), + path('/update/', UpdateUserApiView.as_view(), name='user-update-api'), + path('/soft_delete/', SoftDeleteUserApiView.as_view(), name='user-soft-delete-api'), + path('/hard_delete/', HardDeleteUserApiView.as_view(), name='user-soft-delete-api'), ] )), # ------ authentication ------ diff --git a/core/apps/accounts/views/user/delete.py b/core/apps/accounts/views/user/delete.py new file mode 100644 index 0000000..a24eba2 --- /dev/null +++ b/core/apps/accounts/views/user/delete.py @@ -0,0 +1,100 @@ +# rest framework +from rest_framework import views, permissions + +# drf yasg +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema + + +# accounts +from core.apps.accounts.models import User + +# utils +from core.utils.response.mixin import ResponseMixin + + +class SoftDeleteUserApiView(views.APIView, ResponseMixin): + permission_classes = [permissions.IsAuthenticated] + + @swagger_auto_schema( + tags=['user'], + operation_summary="Soft delete a user by ID", + operation_description=""" + Mark a user as deleted without permanently removing them from the database. + + Authentication: + - Requires a valid Bearer access token. + + Process: + - The system retrieves the user by the provided ID. + - If the user exists, the `is_deleted` flag is set to True. + - The user record remains in the database but is considered inactive/deleted. + + Response: + - 200 OK: User successfully soft deleted. + - 404 Not Found: No user found with the given ID. + - 500 Internal Server Error: Unexpected error occurred during deletion. + + Notes: + - This endpoint only marks the user as deleted and does not remove related data. + - Only authenticated users with proper permissions can perform this action. + """, + ) + def delete(self, request, id): + try: + user = User.objects.filter(id=id).first() + if not user: + return self.not_found_response(message="User not found with this id") + user.is_deleted = True + user.save() + return self.deleted_response( + message="User successfully deleted", + ) + except Exception as e: + return self.error_response( + data=str(e) + ) + + + + +class HardDeleteUserApiView(views.APIView, ResponseMixin): + permission_classes = [permissions.IsAuthenticated] + + @swagger_auto_schema( + tags=['user'], + operation_summary="Permanently delete a user by ID.", + operation_description=""" + Permanently remove a user from the database by their ID. + + Authentication: + - Requires a valid Bearer access token. + + Process: + - The system retrieves the user by the provided ID. + - If the user exists, the user record is permanently deleted from the database. + + Response: + - 200 OK: User successfully deleted. + - 404 Not Found: No user found with the given ID. + - 500 Internal Server Error: Unexpected error occurred during deletion. + + Notes: + - This action permanently removes the user and cannot be undone. + - Only authenticated users with proper permissions can perform this action. + - All related data handling (foreign keys, constraints) depends on the database setup. + """, + ) + def delete(self, request, id): + try: + user = User.objects.filter(id=id).first() + if not user: + return self.not_found_response(message="User not found with this id") + user.delete() + return self.deleted_response( + message="User successfully deleted", + ) + except Exception as e: + return self.error_response( + data=str(e) + ) \ No newline at end of file diff --git a/core/apps/accounts/views/user/update.py b/core/apps/accounts/views/user/update.py new file mode 100644 index 0000000..4945c98 --- /dev/null +++ b/core/apps/accounts/views/user/update.py @@ -0,0 +1,134 @@ +# rest framework +from rest_framework import generics, permissions, parsers + +# drf yasg +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema + + +# accounts +from core.apps.accounts.models import User +from core.apps.accounts.serializers.user.update import UpdateUserSerializer +from core.apps.accounts.serializers.user.user import UserSerializer + +# utils +from core.utils.response.mixin import ResponseMixin + + +class UpdateUserApiView(generics.GenericAPIView, ResponseMixin): + serializer_class = UpdateUserSerializer + queryset = User.objects.all() + permission_classes = [permissions.IsAuthenticated] + parser_classes = [parsers.MultiPartParser, parsers.FormParser] + + @swagger_auto_schema( + tags=['user'], + operation_summary="Api for update user with id", + operation_description=""" + Update the information of an existing user using their ID. + + Authentication: + - Requires a valid Bearer access token. + + Process: + - The system retrieves the user by the provided ID. + - If the user exists, the incoming data is validated using the UpdateUserSerializer. + - Valid fields are updated and saved. + - Returns the updated user data on success. + + Request: + - Accepts partial user data (PATCH). + - Request format is multipart/form-data + - Fields can include first_name, last_name, username, phone_number, profile_image, and others defined in the serializer. + + Response: + - 200 OK: Successfully updates and returns the updated user information. + - 400 Bad Request: Returned when input validation fails. + - 404 Not Found: Returned when there is no user associated with the given ID. + - 500 Internal Server Error: Returned when an unexpected error occurs. + + Notes: + - Only authenticated users can access this endpoint. + - Supports partial updates, meaning not all fields are required. + """, + responses={ + 200: openapi.Response( + schema=None, + description="Success", + examples={ + "application/json": { + "status_code": 200, + "status": "success", + "message": "User successfully updated", + "data": { + "id": 0, + "first_name": "sting", + "last_name": "sting", + "phone_number": "sting", + "username": "sting", + "profile_image": "sting", + "created_at": "sting", + "updated_at": "sting", + } + } + } + ), + 404: openapi.Response( + schema=None, + description="Not Found", + examples={ + "application/json": { + "status_code": 404, + "status": "not_found", + "message": "User not found with given id", + "data": {} + } + } + ), + 400: openapi.Response( + schema=None, + description="Failure", + examples={ + "application/json": { + "status_code": 400, + "status": "failure", + "message": "Kiritayotgan malumotingizni tekshirib ko'ring", + "data": "string", + } + } + ), + 500: openapi.Response( + schema=None, + description="Error", + examples={ + "application/json": { + "status_code": 500, + "status": "error", + "message": "Xatolik, Iltimos backend dasturchiga murojaat qiling", + "data": "string", + } + } + ), + } + ) + def patch(self, request, id): + try: + instance = User.objects.filter(id=id).first() + if not instance: + return self.not_found_response(data={}, message="User not found with given id") + serializer = self.serializer_class( + data=request.data, instance=instance, partial=True + ) + if serializer.is_valid(): + updated_instance = serializer.save() + return self.success_response( + data=UserSerializer(updated_instance).data, + message="User successfully updated" + ) + return self.failure_response( + data=serializer.errors, + ) + except Exception as e: + return self.error_response( + data=str(e) + ) \ No newline at end of file diff --git a/core/utils/response/mixin.py b/core/utils/response/mixin.py index 71e4511..4a81d3d 100644 --- a/core/utils/response/mixin.py +++ b/core/utils/response/mixin.py @@ -46,7 +46,7 @@ class ResponseMixin: response_data["message"] = "Kiritayotgan malumotingizni tekshirib ko'ring" if data is not None: response_data["data"] = data - return + return Response(response_data, status=status.HTTP_400_BAD_REQUEST) @classmethod def not_found_response(cls, data=None, message=None):