From f56711028690dff5d73b41394e7d8658df6c168b Mon Sep 17 00:00:00 2001 From: Mojtaba-z Date: Mon, 27 Oct 2025 11:21:29 +0330 Subject: [PATCH] filter all apis of organization city & province - cant remove own user or organoization --- apps/authentication/api/v1/api.py | 73 ++++++++++--------- .../api/v1/serializers/serializer.py | 11 +++ apps/authentication/exceptions.py | 10 ++- .../0038_organizationtype_region_scope.py | 18 +++++ apps/authentication/mixins/__init__.py | 0 apps/authentication/mixins/region_filter.py | 41 +++++++++++ apps/authorization/api/v1/api.py | 7 +- apps/core/api.py | 34 ++++++++- apps/herd/web/api/v1/api.py | 73 +++++-------------- 9 files changed, 172 insertions(+), 95 deletions(-) create mode 100644 apps/authentication/migrations/0038_organizationtype_region_scope.py create mode 100644 apps/authentication/mixins/__init__.py create mode 100644 apps/authentication/mixins/region_filter.py diff --git a/apps/authentication/api/v1/api.py b/apps/authentication/api/v1/api.py index 0d092b0..b9fbd3e 100644 --- a/apps/authentication/api/v1/api.py +++ b/apps/authentication/api/v1/api.py @@ -1,7 +1,19 @@ +import random +import typing + +from django.contrib.auth.hashers import make_password +from django.core.cache import cache +from django.db import transaction +from rest_framework import filters +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework.viewsets import ModelViewSet +from rest_framework_simplejwt.views import TokenObtainPairView + from apps.authentication.api.v1.serializers.jwt import CustomizedTokenObtainPairSerializer -from apps.core.mixins.soft_delete_mixin import SoftDeleteMixin -from rest_framework.decorators import action, permission_classes -from apps.authentication import permissions as auth_permissions from apps.authentication.api.v1.serializers.serializer import ( CitySerializer, ProvinceSerializer, @@ -10,16 +22,6 @@ from apps.authentication.api.v1.serializers.serializer import ( UserSerializer, BankAccountSerializer, ) -from rest_framework_simplejwt.views import TokenObtainPairView -from apps.core.mixins.search_mixin import DynamicSearchMixin -from apps.core.pagination import CustomPageNumberPagination -from apps.authorization.api.v1 import api as authorize_view -from rest_framework.permissions import IsAuthenticated -from django.contrib.auth.hashers import make_password -from apps.authentication.tools import get_token_jti -from common.helpers import get_organization_by_user -from rest_framework.viewsets import ModelViewSet -from rest_framework.permissions import AllowAny from apps.authentication.models import ( User, City, @@ -29,16 +31,14 @@ from apps.authentication.models import ( BankAccountInformation, BlacklistedAccessToken ) -from rest_framework.response import Response -from common.tools import CustomOperations -from rest_framework.views import APIView -from django.core.cache import cache -from rest_framework import filters -from rest_framework import status -from django.db import transaction +from apps.authentication.tools import get_token_jti +from apps.authorization.api.v1 import api as authorize_view +from apps.core.api import BaseViewSet +from apps.core.mixins.search_mixin import DynamicSearchMixin +from apps.core.mixins.soft_delete_mixin import SoftDeleteMixin +from common.helpers import get_organization_by_user from common.sms import send_sms -import random -import typing +from common.tools import CustomOperations class CustomizedTokenObtainPairView(TokenObtainPairView): @@ -148,11 +148,6 @@ class UserViewSet(SoftDeleteMixin, ModelViewSet): else: return Response(serializer.errors, status=status.HTTP_403_FORBIDDEN) - def destroy(self, request, *args, **kwargs): - instance = self.get_object() - self.perform_destroy(instance) - return Response(status=status.HTTP_204_NO_CONTENT) - @action( methods=['get'], detail=False, @@ -168,18 +163,24 @@ class UserViewSet(SoftDeleteMixin, ModelViewSet): return Response(serializer.data, status.HTTP_200_OK) -class CityViewSet(SoftDeleteMixin, ModelViewSet): +class CityViewSet(BaseViewSet, SoftDeleteMixin, ModelViewSet): """ Crud operations for city model """ # queryset = City.objects.all() serializer_class = CitySerializer def list(self, request, *args, **kwargs): """ return list of cities by province """ + param = request.query_params + + if param.get('province'): + queryset = self.queryset.filter( + province_id=int(request.GET['province']) + ) + else: + queryset = self.get_queryset() serializer = self.serializer_class( - self.queryset.filter( - province_id=int(request.GET['province']) - ), many=True + queryset, many=True ) return Response(serializer.data, status=status.HTTP_200_OK) @@ -196,7 +197,7 @@ class OrganizationTypeViewSet(SoftDeleteMixin, ModelViewSet): serializer_class = OrganizationTypeSerializer -class OrganizationViewSet(SoftDeleteMixin, ModelViewSet, DynamicSearchMixin): +class OrganizationViewSet(BaseViewSet, SoftDeleteMixin, ModelViewSet, DynamicSearchMixin): """ Crud operations for organization model """ # queryset = Organization.objects.all() serializer_class = OrganizationSerializer @@ -221,12 +222,13 @@ class OrganizationViewSet(SoftDeleteMixin, ModelViewSet, DynamicSearchMixin): def list(self, request, *args, **kwargs): """ all organization """ + queryset = self.get_queryset() - query = self.filter_query(self.queryset) + query = self.filter_query(queryset) page = self.paginate_queryset(query.order_by('-create_date')) # paginate queryset - if page is not None: + if page is not None: # noqa serializer = self.serializer_class(page, many=True) return self.get_paginated_response(serializer.data) @@ -328,7 +330,8 @@ class OrganizationViewSet(SoftDeleteMixin, ModelViewSet, DynamicSearchMixin): child_organizations = self.get_all_org_child(organization) # search & filter - queryset = self.filter_query(self.queryset.filter(id__in={instance.id for instance in child_organizations})) + queryset = self.filter_query( + self.get_queryset().filter(id__in={instance.id for instance in child_organizations})) page = self.paginate_queryset(queryset) # paginate queryset diff --git a/apps/authentication/api/v1/serializers/serializer.py b/apps/authentication/api/v1/serializers/serializer.py index e6d19cf..0d5a05c 100644 --- a/apps/authentication/api/v1/serializers/serializer.py +++ b/apps/authentication/api/v1/serializers/serializer.py @@ -1,8 +1,10 @@ import typing from django.contrib.auth.hashers import make_password +from django.db.models import Q from rest_framework import serializers +from apps.authentication.exceptions import UserExistException from apps.authentication.models import ( User, City, @@ -99,6 +101,13 @@ class UserSerializer(serializers.ModelSerializer): } } + def validate(self, attrs): + mobile = attrs['mobile'] + national_code = attrs['national_code'] + + if self.Meta.model.objects.filter(Q(mobile=mobile) | Q(national_code=national_code)).exists(): + raise UserExistException() + def to_representation(self, instance): """ Custom output """ @@ -133,6 +142,8 @@ class UserSerializer(serializers.ModelSerializer): instance.nationality = validated_data.get('nationality') instance.ownership = validated_data.get('ownership') instance.address = validated_data.get('address') + instance.address = validated_data.get('unit_name') + instance.address = validated_data.get('unit_national_id') instance.photo = validated_data.get('photo') instance.province = validated_data.get('province', instance.province) instance.city = validated_data.get('city', instance.city) diff --git a/apps/authentication/exceptions.py b/apps/authentication/exceptions.py index 32c97f4..ff3307d 100644 --- a/apps/authentication/exceptions.py +++ b/apps/authentication/exceptions.py @@ -1,6 +1,6 @@ -from rest_framework.exceptions import APIException from django.utils.translation import gettext_lazy as _ from rest_framework import status +from rest_framework.exceptions import APIException class TokenBlackListedException(APIException): @@ -17,3 +17,11 @@ class OrganizationBankAccountException(APIException): status_code = status.HTTP_403_FORBIDDEN default_detail = "برای این سازمان حساب بانکی تعریف نشده است, ابتدا حساب بانکی تعریف کنید" # noqa default_code = "برای این سازمان حساب بانکی تعریف نشده است" # noqa + + +class UserExistException(APIException): + """ if user exist """ + + status_code = status.HTTP_403_FORBIDDEN + default_detail = _('کاربری با این شماره موبایل یا با این نام کاربری از قبل وجود دارد') # noqa + default_code = 'user_does_not_exist' diff --git a/apps/authentication/migrations/0038_organizationtype_region_scope.py b/apps/authentication/migrations/0038_organizationtype_region_scope.py new file mode 100644 index 0000000..60a3134 --- /dev/null +++ b/apps/authentication/migrations/0038_organizationtype_region_scope.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-10-27 05:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0037_user_unit_name_user_unit_national_id'), + ] + + operations = [ + migrations.AddField( + model_name='organizationtype', + name='region_scope', + field=models.CharField(choices=[('national', 'کشوری'), ('province', 'استان'), ('city', 'شهرستان')], default='city', max_length=50), + ), + ] diff --git a/apps/authentication/mixins/__init__.py b/apps/authentication/mixins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/authentication/mixins/region_filter.py b/apps/authentication/mixins/region_filter.py new file mode 100644 index 0000000..997145b --- /dev/null +++ b/apps/authentication/mixins/region_filter.py @@ -0,0 +1,41 @@ +import typing + +from common.helpers import get_organization_by_user + + +class RegionFilterMixin: + """ + Filters queryset automatically based on: + - Query params (city / province) + - Or the organization type of the current user + """ + + def filter_by_region(self, queryset, org: bool = None) -> typing.Any: + request = self.request # noqa + city_id = int(request.query_params.get('city_id')) + province_id = int(request.query_params.get('province_id')) + organization = get_organization_by_user(self.request.user) # noqa + + if city_id: + queryset = queryset.filter(city_id=city_id) + + elif province_id: + if hasattr(queryset.model, 'province_id'): + queryset = queryset.filter(province_id=province_id) + + # filter by organization type region + if org: + scope = organization.activity_fields + + if scope == 'CI': + queryset = queryset.filter(city=organization.city) + + elif scope == 'PR': + if hasattr(queryset.model, 'province_id'): + queryset = queryset.filter(province=organization.province) + + # if organization is admin of system + elif scope == 'CO': + return queryset + + return queryset diff --git a/apps/authorization/api/v1/api.py b/apps/authorization/api/v1/api.py index bc5a482..b3a2419 100644 --- a/apps/authorization/api/v1/api.py +++ b/apps/authorization/api/v1/api.py @@ -19,6 +19,7 @@ from apps.authorization.models import ( UserRelations, Page ) +from apps.core.api import BaseViewSet from apps.core.exceptions import ConflictException from apps.core.mixins.search_mixin import DynamicSearchMixin from apps.core.mixins.soft_delete_mixin import SoftDeleteMixin @@ -118,7 +119,7 @@ class PermissionViewSet(SoftDeleteMixin, viewsets.ModelViewSet): return Response(serializer.data) -class UserRelationViewSet(SoftDeleteMixin, viewsets.ModelViewSet, DynamicSearchMixin): +class UserRelationViewSet(BaseViewSet, SoftDeleteMixin, viewsets.ModelViewSet, DynamicSearchMixin): """ Crud Operations for User Relations """ queryset = UserRelations.objects.all() @@ -130,6 +131,8 @@ class UserRelationViewSet(SoftDeleteMixin, viewsets.ModelViewSet, DynamicSearchM 'user__phone', 'user__national_code', 'user__province__name', + 'user__unit_name', + 'user__unit_national_id', 'user__city__name', 'role__name' ] @@ -138,7 +141,7 @@ class UserRelationViewSet(SoftDeleteMixin, viewsets.ModelViewSet, DynamicSearchM role_param = self.request.query_params.get('role') # noqa if role_param != '': - queryset = self.queryset.filter(role_id=int(role_param)) + queryset = self.get_queryset().filter(role_id=int(role_param)) else: queryset = self.get_queryset().order_by('-create_date') diff --git a/apps/core/api.py b/apps/core/api.py index 649d26b..5144b6c 100644 --- a/apps/core/api.py +++ b/apps/core/api.py @@ -1,8 +1,36 @@ -from apps.core.serializers import MobileTestSerializer, SystemConfigSerializer -from apps.core.models import MobileTest, SystemConfig -from rest_framework.response import Response from rest_framework import viewsets +from apps.authentication.mixins.region_filter import RegionFilterMixin +from apps.core.models import MobileTest, SystemConfig +from apps.core.serializers import MobileTestSerializer, SystemConfigSerializer + + +class BaseViewSet(RegionFilterMixin, viewsets.ModelViewSet): + """ + All view sets in the project should inherit from this class. + It applies region-based filtering automatically to GET (list) requests. + """ + + def get_queryset(self): + queryset = super().get_queryset() + request = self.request + user = request.user + user_relation = user.user_relation.all() + + if self.request.method.lower() == 'get' and not self.kwargs.get('pk'): + queryset = self.filter_by_region(queryset, org=True) + + if not user_relation.first().role.type.key == 'ADM': + model_name = queryset.model.__name__.lower() + + if model_name == 'user': + queryset = queryset.exclude(id=user.id) + + elif model_name == 'organization': + queryset = queryset.exclude(id=user_relation.first().organization.id) + + return queryset + class MobileTestViewSet(viewsets.ModelViewSet): queryset = MobileTest.objects.all() diff --git a/apps/herd/web/api/v1/api.py b/apps/herd/web/api/v1/api.py index 462e650..f540e25 100644 --- a/apps/herd/web/api/v1/api.py +++ b/apps/herd/web/api/v1/api.py @@ -1,22 +1,22 @@ +from django.db import transaction +from rest_framework import status +from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.response import Response + +from apps.authentication.api.v1.api import UserViewSet +from apps.core.api import BaseViewSet +from apps.core.mixins.search_mixin import DynamicSearchMixin +from apps.core.mixins.soft_delete_mixin import SoftDeleteMixin +from apps.herd.models import Herd, Rancher from apps.herd.web.api.v1.serializers import HerdSerializer, RancherSerializer from apps.livestock.web.api.v1.serializers import LiveStockSerializer from apps.product.web.api.v1.serializers import product_serializers -from apps.core.mixins.soft_delete_mixin import SoftDeleteMixin -from apps.core.mixins.search_mixin import DynamicSearchMixin -from apps.authentication.api.v1.api import UserViewSet from common.helpers import get_organization_by_user -from rest_framework.exceptions import APIException -from apps.product import models as product_models -from rest_framework.response import Response -from rest_framework.decorators import action from common.tools import CustomOperations -from rest_framework import viewsets -from apps.herd.models import Herd, Rancher -from django.db import transaction -from rest_framework import status -class HerdViewSet(SoftDeleteMixin, viewsets.ModelViewSet): +class HerdViewSet(BaseViewSet, SoftDeleteMixin, viewsets.ModelViewSet): """ Herd ViewSet """ queryset = Herd.objects.all() serializer_class = HerdSerializer @@ -57,47 +57,12 @@ class HerdViewSet(SoftDeleteMixin, viewsets.ModelViewSet): @transaction.atomic def my_herds(self, request): """ get current user herds """ - serializer = self.serializer_class(self.queryset.filter(owner=request.user.id), many=True) + serializer = self.serializer_class(self.get_queryset().filter(owner=request.user.id), many=True) if serializer.data: return Response(serializer.data, status=status.HTTP_200_OK) else: return Response(status=status.HTTP_204_NO_CONTENT) - @action( - methods=['post'], - detail=True, - url_path='trash', - url_name='trash', - name='trash' - ) - @transaction.atomic - def trash(self, request, pk=None): - """ Sent herd to trash """ - try: - herd = self.queryset.get(id=pk) - herd.trash = True - herd.save() - return Response(status=status.HTTP_200_OK) - except APIException as e: - return Response(e, status=status.HTTP_204_NO_CONTENT) - - @action( - methods=['post'], - detail=True, - url_path='delete', - url_name='delete', - name='delete' - ) - @transaction.atomic - def delete(self, request, pk=None): - """ full delete of herd """ - try: - herd = self.queryset.get(id=pk) - herd.delete() - return Response(status=status.HTTP_200_OK) - except APIException as e: - return Response(e, status=status.HTTP_204_NO_CONTENT) - @action( methods=['get'], detail=True, @@ -113,12 +78,12 @@ class HerdViewSet(SoftDeleteMixin, viewsets.ModelViewSet): # paginate queryset page = self.paginate_queryset(queryset) - if page is not None: + if page is not None: # noqa serializer = LiveStockSerializer(page, many=True) return self.get_paginated_response(serializer.data) -class RancherViewSet(SoftDeleteMixin, viewsets.ModelViewSet, DynamicSearchMixin): +class RancherViewSet(BaseViewSet, SoftDeleteMixin, viewsets.ModelViewSet, DynamicSearchMixin): queryset = Rancher.objects.all() serializer_class = RancherSerializer search_fields = [ @@ -137,9 +102,9 @@ class RancherViewSet(SoftDeleteMixin, viewsets.ModelViewSet, DynamicSearchMixin) def list(self, request, *args, **kwargs): """ list of ranchers """ - search = self.filter_query(self.queryset.order_by('-modify_date')) # search & filter + search = self.filter_query(self.get_queryset().order_by('-modify_date')) # search & filter page = self.paginate_queryset(search) - if page is not None: + if page is not None: # noqa serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) @@ -158,7 +123,7 @@ class RancherViewSet(SoftDeleteMixin, viewsets.ModelViewSet, DynamicSearchMixin) # paginate queryset page = self.paginate_queryset(queryset) - if page is not None: + if page is not None: # noqa serializer = HerdSerializer(page, many=True) return self.get_paginated_response(serializer.data) @@ -175,6 +140,6 @@ class RancherViewSet(SoftDeleteMixin, viewsets.ModelViewSet, DynamicSearchMixin) rancher = self.get_object() # rancher object page = self.paginate_queryset(rancher.plans.all()) - if page is not None: + if page is not None: # noqa serializer = product_serializers.IncentivePlanRancherSerializer(page, many=True) return self.get_paginated_response(serializer.data)