diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 9361866..20b46d5 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -153,7 +153,7 @@ jobs: update_env \ "DB_HOST=postgres" \ "DB_NAME=sifatbahodb" \ - "DB_PORT=5432" - + "DB_PORT=5432" \ + "DIDOX_TOKEN=${{ secrets.DIDOX_TOKEN }}" export PORT=8085 docker stack deploy -c stack.yaml ${{ env.PROJECT_NAME }} --with-registry-auth diff --git a/config/urls.py b/config/urls.py index 1c27670..0e857fb 100644 --- a/config/urls.py +++ b/config/urls.py @@ -13,7 +13,7 @@ from config.env import env def home(request): - return HttpResponse("OK: #46c96214578b78c1fe731f9a2ceb4429b81d4da9") + return HttpResponse("OK: #88dedd85c79ccf732b2adac03616bd14e67a1579") urlpatterns = [ diff --git a/core/apps/accounts/serializers/user.py b/core/apps/accounts/serializers/user.py index 9ba1eb8..9019962 100644 --- a/core/apps/accounts/serializers/user.py +++ b/core/apps/accounts/serializers/user.py @@ -30,6 +30,22 @@ class UserUpdateSerializer(serializers.ModelSerializer): "last_name", "avatar" ] + +class AdminUserSerializer(serializers.ModelSerializer): + avatar = serializers.SerializerMethodField(method_name='get_avatar') + + class Meta: + model = get_user_model() + fields = "__all__" + + def get_avatar(self, obj): + request = self.context.get('request') + if obj.avatar: + return request.build_absolute_uri(obj.avatar.url) + return None + + + class UserCreateSerializer(serializers.ModelSerializer): class Meta: model = get_user_model() @@ -38,5 +54,4 @@ class UserCreateSerializer(serializers.ModelSerializer): "first_name", "last_name", "password", - "role" - ] \ No newline at end of file + "role"] \ No newline at end of file diff --git a/core/apps/accounts/urls.py b/core/apps/accounts/urls.py index 507a041..f2b21ff 100644 --- a/core/apps/accounts/urls.py +++ b/core/apps/accounts/urls.py @@ -5,7 +5,7 @@ Accounts app urls from django.urls import path, include from rest_framework_simplejwt import views as jwt_views from .views import RegisterView, ResetPasswordView, MeView, ChangePasswordView, UserListApiView, AdminUserListApiView, \ - AdminUpdateAPIView, AdminCreateAPIView + AdminUserView, AdminCreateAPIView, AdminUpdateAPIView from rest_framework.routers import DefaultRouter router = DefaultRouter() @@ -13,6 +13,7 @@ router.register("auth", RegisterView, basename="auth") router.register("auth", ResetPasswordView, basename="reset-password") router.register("auth", MeView, basename="me") router.register("auth", ChangePasswordView, basename="change-password") +router.register("user", AdminUserView, basename="user-crud") urlpatterns = [ diff --git a/core/apps/accounts/views/user.py b/core/apps/accounts/views/user.py index 89619d9..7a63242 100644 --- a/core/apps/accounts/views/user.py +++ b/core/apps/accounts/views/user.py @@ -1,13 +1,15 @@ from django.contrib.auth import get_user_model from django.shortcuts import get_object_or_404 +from django_core.mixins import BaseViewSetMixin from drf_spectacular.utils import extend_schema from rest_framework import generics, filters 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 core.apps.accounts.choices.user import RoleChoice -from core.apps.accounts.serializers.user import UserSerializer, UserCreateSerializer +from core.apps.accounts.serializers.user import UserSerializer, AdminUserSerializer, UserCreateSerializer User = get_user_model() @@ -33,6 +35,18 @@ class AdminUserListApiView(generics.ListAPIView): search_fields = ['phone', 'first_name', 'last_name'] +@extend_schema(tags=["User"], request=AdminUserSerializer) +class AdminUserView(BaseViewSetMixin, ModelViewSet): + queryset = User.objects.filter(role=RoleChoice.USER) + serializer_class = AdminUserSerializer + permission_classes = [IsAuthenticated] + filter_backends = [filters.SearchFilter] + search_fields = ['phone', 'first_name', 'last_name'] + + def serializer_context(self): + return self.serializer_class(context={"request": self.request}) + + @extend_schema(tags=['User'], responses={200: UserSerializer}, request=UserCreateSerializer) @@ -49,10 +63,10 @@ class AdminCreateAPIView(APIView): return Response(serializer.data, status=201) + @extend_schema(tags=['User'], responses={200: UserSerializer}, request=UserCreateSerializer) - class AdminUpdateAPIView(APIView): permission_classes = [IsAuthenticated] @@ -66,5 +80,3 @@ class AdminUpdateAPIView(APIView): serializer.save() return Response(serializer.data, status=200) - - diff --git a/core/apps/evaluation/migrations/0032_certificatemodel.py b/core/apps/evaluation/migrations/0032_certificatemodel.py new file mode 100644 index 0000000..cbd26f6 --- /dev/null +++ b/core/apps/evaluation/migrations/0032_certificatemodel.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.4 on 2026-04-23 11:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('evaluation', '0031_remove_autoevaluationmodel_object_location_city_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='CertificateModel', + 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)), + ('title', models.CharField(max_length=255, verbose_name='title')), + ('file_url', models.URLField(max_length=255, verbose_name='file url')), + ], + options={ + 'verbose_name': 'Certificate', + 'verbose_name_plural': 'Certificates', + 'db_table': 'certificate', + }, + ), + ] diff --git a/core/apps/evaluation/migrations/0032_evaluationrequestmodel_is_archive.py b/core/apps/evaluation/migrations/0032_evaluationrequestmodel_is_archive.py new file mode 100644 index 0000000..edb3779 --- /dev/null +++ b/core/apps/evaluation/migrations/0032_evaluationrequestmodel_is_archive.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.4 on 2026-04-23 07:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('evaluation', '0031_remove_autoevaluationmodel_object_location_city_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='evaluationrequestmodel', + name='is_archive', + field=models.BooleanField(default=False, verbose_name='is archive'), + ), + ] diff --git a/core/apps/evaluation/migrations/0032_quickevaluationmodel_is_archive.py b/core/apps/evaluation/migrations/0032_quickevaluationmodel_is_archive.py new file mode 100644 index 0000000..c66262c --- /dev/null +++ b/core/apps/evaluation/migrations/0032_quickevaluationmodel_is_archive.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.4 on 2026-04-23 07:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('evaluation', '0031_remove_autoevaluationmodel_object_location_city_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='quickevaluationmodel', + name='is_archive', + field=models.BooleanField(default=False, verbose_name='is archive'), + ), + ] diff --git a/core/apps/evaluation/migrations/0033_merge_20260423_1622.py b/core/apps/evaluation/migrations/0033_merge_20260423_1622.py new file mode 100644 index 0000000..addd7d4 --- /dev/null +++ b/core/apps/evaluation/migrations/0033_merge_20260423_1622.py @@ -0,0 +1,15 @@ +# Generated by Django 6.0.4 on 2026-04-23 11:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('evaluation', '0032_certificatemodel'), + ('evaluation', '0032_evaluationrequestmodel_is_archive'), + ('evaluation', '0032_quickevaluationmodel_is_archive'), + ] + + operations = [ + ] diff --git a/core/apps/evaluation/migrations/0034_remove_certificatemodel_file_url_and_more.py b/core/apps/evaluation/migrations/0034_remove_certificatemodel_file_url_and_more.py new file mode 100644 index 0000000..21973b2 --- /dev/null +++ b/core/apps/evaluation/migrations/0034_remove_certificatemodel_file_url_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 6.0.4 on 2026-04-23 13:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('evaluation', '0033_merge_20260423_1622'), + ] + + operations = [ + migrations.RemoveField( + model_name='certificatemodel', + name='file_url', + ), + migrations.AddField( + model_name='certificatemodel', + name='file', + field=models.FileField(blank=True, null=True, upload_to='certificates/', verbose_name='file'), + ), + ] diff --git a/core/apps/evaluation/models/__init__.py b/core/apps/evaluation/models/__init__.py index a625148..46a6a07 100644 --- a/core/apps/evaluation/models/__init__.py +++ b/core/apps/evaluation/models/__init__.py @@ -11,3 +11,4 @@ from .report import * # noqa from .request import * # noqa from .valuation import * # noqa from .vehicle import * # noqa +from .certificate import * # noqa diff --git a/core/apps/evaluation/models/certificate.py b/core/apps/evaluation/models/certificate.py new file mode 100644 index 0000000..4089da4 --- /dev/null +++ b/core/apps/evaluation/models/certificate.py @@ -0,0 +1,30 @@ +from django.db import models +from django_core.models import AbstractBaseModel +from django.utils.translation import gettext_lazy as _ +from model_bakery import baker + + +class CertificateModel(AbstractBaseModel): + title = models.CharField( + verbose_name=_("title"), + max_length=255 + ) + + file = models.FileField( + verbose_name=_("file"), + upload_to="certificates/", + blank=True, + null=True + ) + + def __str__(self): + return self.title + + @classmethod + def _baker(cls): + return baker.make(cls) + + class Meta: + db_table = "certificate" + verbose_name = _("Certificate") + verbose_name_plural = _("Certificates") \ No newline at end of file diff --git a/core/apps/evaluation/models/quick.py b/core/apps/evaluation/models/quick.py index d501397..819e16c 100644 --- a/core/apps/evaluation/models/quick.py +++ b/core/apps/evaluation/models/quick.py @@ -132,6 +132,11 @@ class QuickEvaluationModel(AbstractBaseModel): default=QuickEvaluationStatus.CREATED, ) + is_archive = models.BooleanField( + verbose_name=_("is archive"), + default=False, + ) + def __str__(self): return f"Quick Evaluation {self.pk} by {self.created_by}" diff --git a/core/apps/evaluation/models/request.py b/core/apps/evaluation/models/request.py index baf6c14..a26c7d0 100644 --- a/core/apps/evaluation/models/request.py +++ b/core/apps/evaluation/models/request.py @@ -118,6 +118,10 @@ class EvaluationrequestModel(AbstractBaseModel): choices=RequestStatus.choices, default=RequestStatus.PENDING, ) + is_archive = models.BooleanField( + verbose_name=_("is archive"), + default=False, + ) def __str__(self): return f"Requests #{self.pk} — {self.get_rate_type_display()}" diff --git a/core/apps/evaluation/serializers/__init__.py b/core/apps/evaluation/serializers/__init__.py index 600d37c..d137a69 100644 --- a/core/apps/evaluation/serializers/__init__.py +++ b/core/apps/evaluation/serializers/__init__.py @@ -12,3 +12,4 @@ from .request import * # noqa from .valuation import * # noqa from .vehicle import * # noqa from .tech_passport import * # noqa +from .certificate import * # noqa diff --git a/core/apps/evaluation/serializers/certificate/__init__.py b/core/apps/evaluation/serializers/certificate/__init__.py new file mode 100644 index 0000000..d736920 --- /dev/null +++ b/core/apps/evaluation/serializers/certificate/__init__.py @@ -0,0 +1 @@ +from .certificate import * # noqa diff --git a/core/apps/evaluation/serializers/certificate/certificate.py b/core/apps/evaluation/serializers/certificate/certificate.py new file mode 100644 index 0000000..b9afd95 --- /dev/null +++ b/core/apps/evaluation/serializers/certificate/certificate.py @@ -0,0 +1,13 @@ +from rest_framework import serializers +from core.apps.evaluation.models import CertificateModel + + +class BaseCertificateSerializer(serializers.ModelSerializer): + + class Meta: + model = CertificateModel + fields = [ + "id", + "title", + "file", + ] \ No newline at end of file diff --git a/core/apps/evaluation/serializers/quick/QuickEvaluation.py b/core/apps/evaluation/serializers/quick/QuickEvaluation.py index 93348d7..23019f4 100644 --- a/core/apps/evaluation/serializers/quick/QuickEvaluation.py +++ b/core/apps/evaluation/serializers/quick/QuickEvaluation.py @@ -38,7 +38,8 @@ class BaseQuickevaluationSerializer(serializers.ModelSerializer): "state_car_name", "created_at", "distance_covered", - "tex_passport_serie_num" + "tex_passport_serie_num", + "is_archive" ] @@ -125,3 +126,8 @@ class CreateQuickevaluationSerializer(serializers.ModelSerializer): if request and request.user and request.user.is_authenticated: validated_data["created_by"] = request.user return super().create(validated_data) + + +class ArchiveQuickevaluationSerializer(serializers.Serializer): + id = serializers.IntegerField(required=True) + is_archive = serializers.BooleanField(required=True) \ No newline at end of file diff --git a/core/apps/evaluation/serializers/request/EvaluationRequest.py b/core/apps/evaluation/serializers/request/EvaluationRequest.py index 368af46..3554f9b 100644 --- a/core/apps/evaluation/serializers/request/EvaluationRequest.py +++ b/core/apps/evaluation/serializers/request/EvaluationRequest.py @@ -55,6 +55,7 @@ class BaseEvaluationrequestSerializer(serializers.ModelSerializer): "user", "created_at", "updated_at", + "is_archive", ] def get_location(self, obj): @@ -183,3 +184,7 @@ class CreateEvaluationrequestSerializer(serializers.ModelSerializer): validated_data["location_name"] = str(location_name) validated_data["user"] = self.context["request"].user return super().create(validated_data) + +class ArchiveEvaluationrequestSerializer(serializers.Serializer): + id = serializers.IntegerField(required=True) + is_archive = serializers.BooleanField(required=True) \ No newline at end of file diff --git a/core/apps/evaluation/urls.py b/core/apps/evaluation/urls.py index 25bd122..1c93b06 100644 --- a/core/apps/evaluation/urls.py +++ b/core/apps/evaluation/urls.py @@ -29,7 +29,9 @@ from .views import ( DidoxCompanyInfoAPIView, TechPassportAPIView, EvaluationStatusChange, - GetArchivedEvaluationListAPIView + CertificateView, + ArchiveQuickEvaluationView, + ArchiveEvaluationrequestView, GetArchivedEvaluationListAPIView, ArchivedEvaluation, ) router = DefaultRouter() @@ -54,14 +56,14 @@ router.register("vehicle", VehicleView, basename="vehicle") router.register("valuation", ValuationView, basename="valuation") router.register("property-owner", PropertyOwnerView, basename="property-owner") router.register("customer", CustomerView, basename="customer") +router.register("certificate", CertificateView, basename="certificate") urlpatterns = [ path("", include(router.urls)), path("auto-evaluation/appraisers/", include( [ path("/list/", AutoEvaluationListAppraisersView.as_view(), name="auto-evaluation-list-appraisers"), path("/set/", AutoEvaluationSetAppraisersView.as_view(), name="auto-evaluation-set-appraisers"), - path("/remove/", AutoEvaluationRemoveAppraisersView.as_view(), - name="auto-evaluation-remove-appraisers"), + path("/remove/", AutoEvaluationRemoveAppraisersView.as_view(), name="auto-evaluation-remove-appraisers"), ] )), path( @@ -76,10 +78,13 @@ urlpatterns = [ ), path("evaluation-request//change-status/", EvaluationStatusChange.as_view(), name="evaluation-change-status"), + path("archive/quick-evaluation/", ArchiveQuickEvaluationView.as_view(), name="quick-evaluation-archive"), + path("archive/evaluation-request/", ArchiveEvaluationrequestView.as_view(), name="evaluation-request-archive"), path("archived-evaluvation/", GetArchivedEvaluationListAPIView.as_view(), name="archived-evaluation"), - path("auto-evaluvation-change-status/", GetArchivedEvaluationListAPIView.as_view(), + path("auto-evaluvation-change-status/", ArchivedEvaluation.as_view(), name="archived-evaluation"), + ] diff --git a/core/apps/evaluation/views/__init__.py b/core/apps/evaluation/views/__init__.py index 9fd419f..a6f2bd5 100644 --- a/core/apps/evaluation/views/__init__.py +++ b/core/apps/evaluation/views/__init__.py @@ -13,3 +13,4 @@ from .valuation import * # noqa from .vehicle import * # noqa from .didox import * # noqa from .tech_passport import * # noqa +from .certificate import * # noqa diff --git a/core/apps/evaluation/views/auto.py b/core/apps/evaluation/views/auto.py index d1dfd4e..1476ecf 100644 --- a/core/apps/evaluation/views/auto.py +++ b/core/apps/evaluation/views/auto.py @@ -160,7 +160,6 @@ class AutoEvaluationListAppraisersView(GenericAPIView): @extend_schema(tags=["AutoEvaluation"]) class GetArchivedEvaluationListAPIView(ListAPIView): permission_classes = [IsAuthenticated] - def get_queryset(self): return AutoEvaluationModel.objects.filter(is_archived=True) diff --git a/core/apps/evaluation/views/certificate.py b/core/apps/evaluation/views/certificate.py new file mode 100644 index 0000000..5df696a --- /dev/null +++ b/core/apps/evaluation/views/certificate.py @@ -0,0 +1,23 @@ +from django_core.mixins import BaseViewSetMixin +from drf_spectacular.utils import extend_schema +from rest_framework.permissions import IsAuthenticated +from rest_framework.viewsets import ModelViewSet +from core.apps.evaluation.models import CertificateModel +from core.apps.evaluation.serializers.certificate import BaseCertificateSerializer +from rest_framework.filters import SearchFilter +from rest_framework.parsers import MultiPartParser, FormParser + +@extend_schema(tags=["Certificate"],request=BaseCertificateSerializer) +class CertificateView(BaseViewSetMixin, ModelViewSet): + queryset = CertificateModel.objects.all() + serializer_class = BaseCertificateSerializer + permission_classes = [IsAuthenticated] + + parser_classes = [MultiPartParser, FormParser] + + filter_backends = [SearchFilter] + search_fields = ["title"] + + pagination_class = None + + action_permission_classes = {} diff --git a/core/apps/evaluation/views/didox.py b/core/apps/evaluation/views/didox.py index f327e6d..273a658 100644 --- a/core/apps/evaluation/views/didox.py +++ b/core/apps/evaluation/views/didox.py @@ -1,6 +1,6 @@ from rest_framework.response import Response from rest_framework import status -from rest_framework.permissions import AllowAny +from rest_framework.permissions import IsAuthenticated from rest_framework.generics import GenericAPIView @@ -11,7 +11,7 @@ from core.services.didox import DidoxService class DidoxCompanyInfoAPIView(GenericAPIView): authentication_classes = [] - permission_classes = [AllowAny] + permission_classes = [IsAuthenticated] @extend_schema( tags=["Didox"], @@ -31,7 +31,6 @@ class DidoxCompanyInfoAPIView(GenericAPIView): def get(self, request, *args, **kwargs): tin = kwargs.get("tin") - # 🔥 TYPE CHECK try: tin = int(tin) except (TypeError, ValueError): @@ -48,4 +47,14 @@ class DidoxCompanyInfoAPIView(GenericAPIView): status=status.HTTP_502_BAD_GATEWAY ) + # if both name and personalNum are null/empty -> 404 + name = data.get("name") + personal_num = data.get("personalNum") + + if not name and not personal_num: + return Response( + {"detail": "Company or person not found"}, + status=status.HTTP_404_NOT_FOUND + ) + return Response(data, status=status.HTTP_200_OK) \ No newline at end of file diff --git a/core/apps/evaluation/views/quick.py b/core/apps/evaluation/views/quick.py index 87a4e80..d0970ef 100644 --- a/core/apps/evaluation/views/quick.py +++ b/core/apps/evaluation/views/quick.py @@ -1,10 +1,14 @@ from django_core.mixins import BaseViewSetMixin from django_filters.rest_framework import DjangoFilterBackend -from drf_spectacular.utils import extend_schema +from drf_spectacular.utils import extend_schema, OpenApiResponse from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.parsers import FormParser, MultiPartParser -from rest_framework.permissions import AllowAny +from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.viewsets import ModelViewSet +from rest_framework.generics import GenericAPIView +from rest_framework.response import Response +from rest_framework import status +from django.shortcuts import get_object_or_404 from core.apps.evaluation.filters.quick import QuickevaluationFilter from core.apps.evaluation.models import QuickEvaluationModel @@ -12,6 +16,7 @@ from core.apps.evaluation.serializers.quick import ( CreateQuickevaluationSerializer, ListQuickevaluationSerializer, RetrieveQuickevaluationSerializer, + ArchiveQuickevaluationSerializer, ) @@ -20,7 +25,7 @@ class QuickEvaluationView(BaseViewSetMixin, ModelViewSet): queryset = QuickEvaluationModel.objects.select_related( "created_by", "brand", "marka", "color", "fuel_type", "body_type", "state_car", "car_position", - ).all() + ).filter(is_archive=False) serializer_class = ListQuickevaluationSerializer permission_classes = [AllowAny] parser_classes = [MultiPartParser, FormParser] @@ -50,3 +55,76 @@ class QuickEvaluationView(BaseViewSetMixin, ModelViewSet): "retrieve": RetrieveQuickevaluationSerializer, "create": CreateQuickevaluationSerializer, } + +@extend_schema(tags=["QuickEvaluation"]) +class ArchiveQuickEvaluationView(GenericAPIView): + permission_classes = [IsAuthenticated] + + def get_serializer_class(self): + if self.request.method == "GET": + return ListQuickevaluationSerializer + return ArchiveQuickevaluationSerializer + + @extend_schema( + tags=["QuickEvaluation"], + summary="Get archived quick evaluations list", + description=""" + Returns only archived quick evaluations. + + This endpoint works like quick-evaluation/, + but only records with is_archive=True are returned. + """, + responses={200: ListQuickevaluationSerializer(many=True)}, + ) + def get(self, request, *args, **kwargs): + queryset = QuickEvaluationModel.objects.filter( + is_archive=True + ).order_by("-created_at") + + serializer = self.get_serializer(queryset, many=True) + + return Response(serializer.data, status=status.HTTP_200_OK) + + @extend_schema( + tags=["QuickEvaluation"], + summary="Archive or unarchive quick evaluation", + description=""" + Update archive status for quick evaluation. + + - is_archive=true → archive + - is_archive=false → remove from archive + """, + request=ArchiveQuickevaluationSerializer, + responses={ + 200: OpenApiResponse( + description="Archive status updated successfully" + ), + 400: OpenApiResponse( + description="Validation error" + ), + 404: OpenApiResponse( + description="Quick evaluation not found" + ), + }, + ) + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + validated_data = serializer.validated_data + + obj = get_object_or_404( + QuickEvaluationModel, + id=validated_data["id"] + ) + + obj.is_archive = validated_data["is_archive"] + obj.save(update_fields=["is_archive"]) + + return Response( + { + "success": True, + "message": "Archive status updated successfully" + }, + status=status.HTTP_200_OK + ) \ No newline at end of file diff --git a/core/apps/evaluation/views/request.py b/core/apps/evaluation/views/request.py index ab17196..0675d6a 100644 --- a/core/apps/evaluation/views/request.py +++ b/core/apps/evaluation/views/request.py @@ -1,23 +1,27 @@ from django.shortcuts import get_object_or_404 + from django_core.mixins import BaseViewSetMixin from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import extend_schema -from rest_framework import status from rest_framework.filters import OrderingFilter, SearchFilter -from rest_framework.permissions import IsAuthenticated +from rest_framework.pagination import PageNumberPagination +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.viewsets import ModelViewSet from rest_framework.response import Response from rest_framework.views import APIView -from rest_framework.viewsets import ModelViewSet +from rest_framework import status -from core.apps.accounts.choices import RoleChoice -from core.apps.evaluation.choices.request import RequestStatus from core.apps.evaluation.filters.request import EvaluationrequestFilter from core.apps.evaluation.models import EvaluationrequestModel from core.apps.evaluation.serializers.request import ( CreateEvaluationrequestSerializer, ListEvaluationrequestSerializer, RetrieveEvaluationrequestSerializer, + ArchiveEvaluationrequestSerializer, ) +from core.apps.evaluation.choices.request import RequestStatus +from rest_framework.generics import GenericAPIView +from drf_spectacular.utils import OpenApiResponse # class RequestPagination(PageNumberPagination): @@ -170,4 +174,77 @@ class EvaluationStatusChange(APIView): 'success': True, 'status': evaluation.status, 'id': evaluation.pk - }) \ No newline at end of file + }) + +@extend_schema(tags=["EvaluationRequest"]) +class ArchiveEvaluationrequestView(GenericAPIView): + permission_classes = [IsAuthenticated] + + def get_serializer_class(self): + if self.request.method == "GET": + return ListEvaluationrequestSerializer + return ArchiveEvaluationrequestSerializer + + @extend_schema( + tags=["EvaluationRequest"], + summary="Get archived evaluation requests list", + description=""" + Returns only archived evaluation requests. + + This endpoint works like evaluation-request/, + but only records with is_archive=True are returned. + """, + responses={200: ListEvaluationrequestSerializer(many=True)}, + ) + def get(self, request, *args, **kwargs): + queryset = EvaluationrequestModel.objects.filter( + is_archive=True + ).order_by("-created_at") + + serializer = self.get_serializer(queryset, many=True) + + return Response(serializer.data, status=status.HTTP_200_OK) + + @extend_schema( + tags=["EvaluationRequest"], + summary="Archive or unarchive evaluation request", + description=""" + Update archive status for evaluation request. + + - is_archive=true → archive + - is_archive=false → remove from archive + """, + request=ArchiveEvaluationrequestSerializer, + responses={ + 200: OpenApiResponse( + description="Archive status updated successfully" + ), + 400: OpenApiResponse( + description="Validation error" + ), + 404: OpenApiResponse( + description="Evaluation request not found" + ), + }, + ) + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + validated_data = serializer.validated_data + + obj = get_object_or_404( + EvaluationrequestModel, + id=validated_data["id"] + ) + + obj.is_archive = validated_data["is_archive"] + obj.save(update_fields=["is_archive"]) + + return Response( + { + "success": True, + "message": "Archive status updated successfully" + }, + status=status.HTTP_200_OK + ) \ No newline at end of file diff --git a/core/apps/evaluation/views/tech_passport.py b/core/apps/evaluation/views/tech_passport.py index 6d7171f..b1af112 100644 --- a/core/apps/evaluation/views/tech_passport.py +++ b/core/apps/evaluation/views/tech_passport.py @@ -1,6 +1,6 @@ from rest_framework.response import Response from rest_framework import status -from rest_framework.permissions import AllowAny +from rest_framework.permissions import IsAuthenticated from rest_framework.generics import GenericAPIView from drf_spectacular.utils import ( @@ -14,7 +14,7 @@ from ..serializers import TechPassportSerializer class TechPassportAPIView(GenericAPIView): authentication_classes = [] - permission_classes = [AllowAny] + permission_classes = [IsAuthenticated] @extend_schema( tags=["Tech Passport"], @@ -33,18 +33,27 @@ class TechPassportAPIView(GenericAPIView): def post(self, request, *args, **kwargs): serializer = TechPassportSerializer(data=request.data) serializer.is_valid(raise_exception=True) + data = serializer.validated_data - try: - result = TechPassportService.get_auto_info( - autonumber=data["autonumber"], - tech_pass_number=data["tech_pass_number"], - tech_pass_series=data["tech_pass_series"], - ) - return Response(result, status=status.HTTP_200_OK) + result = TechPassportService.get_auto_info( + autonumber=data["autonumber"], + tech_pass_number=data["tech_pass_number"], + tech_pass_series=data["tech_pass_series"], + ) - except Exception as e: + response_data = result["data"] + status_code = result["status_code"] + + # success bo‘lsa faqat data ichidagi data qaytariladi + if status_code == 200: return Response( - {"detail": str(e)}, - status=status.HTTP_400_BAD_REQUEST - ) \ No newline at end of file + response_data.get("data", {}), + status=status.HTTP_200_OK + ) + + # error bo‘lsa original response qaytariladi + return Response( + response_data, + status=status_code + ) \ No newline at end of file diff --git a/core/services/tech_passport.py b/core/services/tech_passport.py index 0d50849..bd9e05b 100644 --- a/core/services/tech_passport.py +++ b/core/services/tech_passport.py @@ -44,21 +44,28 @@ class TechPassportService: verify=False ) - response.raise_for_status() - logger.info( - f"Tech passport info fetched successfully: {response.status_code}" + f"Tech passport response status: {response.status_code}" ) - response_data = response.json() + try: + response_data = response.json() + except ValueError: + response_data = { + "detail": "Invalid response from external service" + } - return response_data.get("data", {}) + return { + "status_code": response.status_code, + "data": response_data + } except requests.exceptions.RequestException as e: - logger.error( - f"Error while fetching tech passport info: {str(e)}" - ) + logger.error(str(e)) + return { - "success": False, - "message": str(e) + "status_code": 500, + "data": { + "detail": str(e) + } } \ No newline at end of file diff --git a/stack.yaml b/stack.yaml index cd59834..1e72694 100644 --- a/stack.yaml +++ b/stack.yaml @@ -84,7 +84,7 @@ services: max-file: "5" web: - image: husanjon/sifatbaho:103 + image: husanjon/sifatbaho:115 env_file: - .env environment: @@ -129,7 +129,7 @@ services: max-file: "5" celery: - image: husanjon/sifatbaho:103 + image: husanjon/sifatbaho:115 env_file: - .env environment: