diff --git a/config/conf/navigation.py b/config/conf/navigation.py index 08788cc..33f8975 100644 --- a/config/conf/navigation.py +++ b/config/conf/navigation.py @@ -112,6 +112,22 @@ PAGES = [ }, ], }, + { + "title": _("Tarix"), + "separator": True, + "items": [ + { + "title": _("Avto baholash tarixi"), + "icon": "history", + "link": reverse_lazy("admin:evaluation_autoevaluationhistorymodel_changelist"), + }, + { + "title": _("Tezkor baholash tarixi"), + "icon": "manage_history", + "link": reverse_lazy("admin:evaluation_quickevaluationhistorymodel_changelist"), + }, + ], + }, { "title": _("Ma'lumotnomalari"), "separator": True, diff --git a/core/apps/evaluation/admin/__init__.py b/core/apps/evaluation/admin/__init__.py index e1e2f06..bc24d35 100644 --- a/core/apps/evaluation/admin/__init__.py +++ b/core/apps/evaluation/admin/__init__.py @@ -1,6 +1,7 @@ from .auto import * # noqa from .customer import * # noqa from .document import * # noqa +from .history import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa diff --git a/core/apps/evaluation/admin/history.py b/core/apps/evaluation/admin/history.py new file mode 100644 index 0000000..6144761 --- /dev/null +++ b/core/apps/evaluation/admin/history.py @@ -0,0 +1,157 @@ +from django.contrib import admin +from django.utils.translation import gettext_lazy as _ +from unfold.admin import ModelAdmin + +from core.apps.evaluation.choices.history import EvaluationEventType +from core.apps.evaluation.models import AutoevaluationhistoryModel, QuickevaluationhistoryModel + + +class BaseHistoryAdmin(ModelAdmin): + """ + History yozuvlari o'zgartirib bo'lmaydi — faqat o'qiladi. + Signallar tomonidan avtomatik yoziladi. + """ + + list_filter = ( + "event_type", + "actor_role", + "created_at", + ) + search_fields = ( + "actor_full_name", + "meta", + ) + ordering = ("created_at",) + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return False + + +@admin.register(AutoevaluationhistoryModel) +class AutoevaluationhistoryAdmin(BaseHistoryAdmin): + list_display = ( + "id", + "auto_evaluation", + "event_type_colored", + "actor_full_name", + "actor_role", + "created_at", + ) + autocomplete_fields = ("auto_evaluation",) + readonly_fields = ( + "auto_evaluation", + "event_type", + "actor_id", + "actor_full_name", + "actor_role", + "meta", + "created_at", + ) + fieldsets = ( + (_("Baholash"), { + "fields": ("auto_evaluation",), + }), + (_("Hodisa"), { + "fields": ( + "event_type", + "meta", + ), + }), + (_("Kim bajargan"), { + "fields": ( + "actor_id", + "actor_full_name", + "actor_role", + ), + }), + (_("Tizim"), { + "classes": ("collapse",), + "fields": ("created_at",), + }), + ) + + @admin.display(description=_("Hodisa turi")) + def event_type_colored(self, obj): + colors = { + EvaluationEventType.ORDER_CREATED: "#16a34a", + EvaluationEventType.STATUS_CHANGED: "#2563eb", + EvaluationEventType.EVALUATOR_ASSIGNED: "#7c3aed", + EvaluationEventType.DOCUMENT_UPLOADED: "#d97706", + EvaluationEventType.PAYMENT_MADE: "#dc2626", + } + color = colors.get(obj.event_type, "#6b7280") + label = obj.get_event_type_display() + from django.utils.html import format_html + return format_html( + '{}', + color, + label, + ) + + +@admin.register(QuickevaluationhistoryModel) +class QuickevaluationhistoryAdmin(BaseHistoryAdmin): + list_display = ( + "id", + "quick_evaluation", + "event_type_colored", + "actor_full_name", + "actor_role", + "created_at", + ) + autocomplete_fields = ("quick_evaluation",) + readonly_fields = ( + "quick_evaluation", + "event_type", + "actor_id", + "actor_full_name", + "actor_role", + "meta", + "created_at", + ) + fieldsets = ( + (_("Tezkor baholash"), { + "fields": ("quick_evaluation",), + }), + (_("Hodisa"), { + "fields": ( + "event_type", + "meta", + ), + }), + (_("Kim bajargan"), { + "fields": ( + "actor_id", + "actor_full_name", + "actor_role", + ), + }), + (_("Tizim"), { + "classes": ("collapse",), + "fields": ("created_at",), + }), + ) + + @admin.display(description=_("Hodisa turi")) + def event_type_colored(self, obj): + colors = { + EvaluationEventType.ORDER_CREATED: "#16a34a", + EvaluationEventType.STATUS_CHANGED: "#2563eb", + EvaluationEventType.EVALUATOR_ASSIGNED: "#7c3aed", + EvaluationEventType.DOCUMENT_UPLOADED: "#d97706", + EvaluationEventType.PAYMENT_MADE: "#dc2626", + } + color = colors.get(obj.event_type, "#6b7280") + label = obj.get_event_type_display() + from django.utils.html import format_html + return format_html( + '{}', + color, + label, + ) diff --git a/core/apps/evaluation/apps.py b/core/apps/evaluation/apps.py index b6fd2be..3601e2c 100644 --- a/core/apps/evaluation/apps.py +++ b/core/apps/evaluation/apps.py @@ -4,3 +4,6 @@ from django.apps import AppConfig class ModuleConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "core.apps.evaluation" + + def ready(self): + import core.apps.evaluation.signals # noqa diff --git a/core/apps/evaluation/choices/history.py b/core/apps/evaluation/choices/history.py new file mode 100644 index 0000000..9368e75 --- /dev/null +++ b/core/apps/evaluation/choices/history.py @@ -0,0 +1,10 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class EvaluationEventType(models.TextChoices): + ORDER_CREATED = "order_created", _("Buyurtma yaratildi") + STATUS_CHANGED = "status_changed", _("Status o'zgartirildi") + EVALUATOR_ASSIGNED = "evaluator_assigned", _("Baholovchi biriktirildi") + DOCUMENT_UPLOADED = "document_uploaded", _("Hujjat yuklandi") + PAYMENT_MADE = "payment_made", _("To'lov qilindi") diff --git a/core/apps/evaluation/filters/__init__.py b/core/apps/evaluation/filters/__init__.py index e1e2f06..bc24d35 100644 --- a/core/apps/evaluation/filters/__init__.py +++ b/core/apps/evaluation/filters/__init__.py @@ -1,6 +1,7 @@ from .auto import * # noqa from .customer import * # noqa from .document import * # noqa +from .history import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa diff --git a/core/apps/evaluation/filters/history.py b/core/apps/evaluation/filters/history.py new file mode 100644 index 0000000..390f79f --- /dev/null +++ b/core/apps/evaluation/filters/history.py @@ -0,0 +1,54 @@ +from django_filters import rest_framework as filters + +from core.apps.evaluation.choices.history import EvaluationEventType +from core.apps.evaluation.models import AutoevaluationhistoryModel, QuickevaluationhistoryModel + + +class AutoevaluationhistoryFilter(filters.FilterSet): + auto_evaluation = filters.NumberFilter( + field_name="auto_evaluation", lookup_expr="exact" + ) + event_type = filters.ChoiceFilter( + field_name="event_type", + choices=EvaluationEventType.choices, + ) + created_from = filters.DateTimeFilter( + field_name="created_at", lookup_expr="gte" + ) + created_to = filters.DateTimeFilter( + field_name="created_at", lookup_expr="lte" + ) + + class Meta: + model = AutoevaluationhistoryModel + fields = [ + "auto_evaluation", + "event_type", + "created_from", + "created_to", + ] + + +class QuickevaluationhistoryFilter(filters.FilterSet): + quick_evaluation = filters.NumberFilter( + field_name="quick_evaluation", lookup_expr="exact" + ) + event_type = filters.ChoiceFilter( + field_name="event_type", + choices=EvaluationEventType.choices, + ) + created_from = filters.DateTimeFilter( + field_name="created_at", lookup_expr="gte" + ) + created_to = filters.DateTimeFilter( + field_name="created_at", lookup_expr="lte" + ) + + class Meta: + model = QuickevaluationhistoryModel + fields = [ + "quick_evaluation", + "event_type", + "created_from", + "created_to", + ] diff --git a/core/apps/evaluation/forms/__init__.py b/core/apps/evaluation/forms/__init__.py index e1e2f06..bc24d35 100644 --- a/core/apps/evaluation/forms/__init__.py +++ b/core/apps/evaluation/forms/__init__.py @@ -1,6 +1,7 @@ from .auto import * # noqa from .customer import * # noqa from .document import * # noqa +from .history import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa diff --git a/core/apps/evaluation/forms/history.py b/core/apps/evaluation/forms/history.py new file mode 100644 index 0000000..aa42f05 --- /dev/null +++ b/core/apps/evaluation/forms/history.py @@ -0,0 +1,17 @@ +from django import forms + +from core.apps.evaluation.models import AutoevaluationhistoryModel, QuickevaluationhistoryModel + + +class AutoevaluationhistoryForm(forms.ModelForm): + + class Meta: + model = AutoevaluationhistoryModel + fields = "__all__" + + +class QuickevaluationhistoryForm(forms.ModelForm): + + class Meta: + model = QuickevaluationhistoryModel + fields = "__all__" diff --git a/core/apps/evaluation/migrations/0023_autoevaluationhistorymodel_and_more.py b/core/apps/evaluation/migrations/0023_autoevaluationhistorymodel_and_more.py new file mode 100644 index 0000000..a0215c1 --- /dev/null +++ b/core/apps/evaluation/migrations/0023_autoevaluationhistorymodel_and_more.py @@ -0,0 +1,52 @@ +# Generated by Django 5.2.7 on 2026-04-02 10:03 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('evaluation', '0022_alter_referenceitemmodel_type'), + ] + + operations = [ + migrations.CreateModel( + name='AutoevaluationhistoryModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('event_type', models.CharField(choices=[('order_created', 'Buyurtma yaratildi'), ('status_changed', "Status o'zgartirildi"), ('evaluator_assigned', 'Baholovchi biriktirildi'), ('document_uploaded', 'Hujjat yuklandi'), ('payment_made', "To'lov qilindi")], max_length=50, verbose_name='event type')), + ('actor_id', models.BigIntegerField(blank=True, null=True, verbose_name='actor id')), + ('actor_full_name', models.CharField(default='Tizim', max_length=255, verbose_name='actor full name')), + ('actor_role', models.CharField(default='system', max_length=50, verbose_name='actor role')), + ('meta', models.JSONField(blank=True, default=dict, verbose_name='meta')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), + ('auto_evaluation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history', to='evaluation.autoevaluationmodel', verbose_name='auto evaluation')), + ], + options={ + 'verbose_name': 'Auto Evaluation History', + 'verbose_name_plural': 'Auto Evaluation Histories', + 'db_table': 'AutoEvaluationHistory', + 'ordering': ['created_at'], + }, + ), + migrations.CreateModel( + name='QuickevaluationhistoryModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('event_type', models.CharField(choices=[('order_created', 'Buyurtma yaratildi'), ('status_changed', "Status o'zgartirildi"), ('evaluator_assigned', 'Baholovchi biriktirildi'), ('document_uploaded', 'Hujjat yuklandi'), ('payment_made', "To'lov qilindi")], max_length=50, verbose_name='event type')), + ('actor_id', models.BigIntegerField(blank=True, null=True, verbose_name='actor id')), + ('actor_full_name', models.CharField(default='Tizim', max_length=255, verbose_name='actor full name')), + ('actor_role', models.CharField(default='system', max_length=50, verbose_name='actor role')), + ('meta', models.JSONField(blank=True, default=dict, verbose_name='meta')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), + ('quick_evaluation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history', to='evaluation.quickevaluationmodel', verbose_name='quick evaluation')), + ], + options={ + 'verbose_name': 'Quick Evaluation History', + 'verbose_name_plural': 'Quick Evaluation Histories', + 'db_table': 'QuickEvaluationHistory', + 'ordering': ['created_at'], + }, + ), + ] diff --git a/core/apps/evaluation/migrations/0024_add_history_indexes.py b/core/apps/evaluation/migrations/0024_add_history_indexes.py new file mode 100644 index 0000000..f5d13ca --- /dev/null +++ b/core/apps/evaluation/migrations/0024_add_history_indexes.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2.7 on 2026-04-02 11:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('evaluation', '0023_autoevaluationhistorymodel_and_more'), + ] + + operations = [ + migrations.AddIndex( + model_name='autoevaluationhistorymodel', + index=models.Index(fields=['auto_evaluation_id', 'created_at'], name='auto_hist_eval_date_idx'), + ), + migrations.AddIndex( + model_name='autoevaluationhistorymodel', + index=models.Index(fields=['event_type'], name='auto_hist_event_type_idx'), + ), + migrations.AddIndex( + model_name='quickevaluationhistorymodel', + index=models.Index(fields=['quick_evaluation_id', 'created_at'], name='quick_hist_eval_date_idx'), + ), + migrations.AddIndex( + model_name='quickevaluationhistorymodel', + index=models.Index(fields=['event_type'], name='quick_hist_event_type_idx'), + ), + ] diff --git a/core/apps/evaluation/models/__init__.py b/core/apps/evaluation/models/__init__.py index e1e2f06..bc24d35 100644 --- a/core/apps/evaluation/models/__init__.py +++ b/core/apps/evaluation/models/__init__.py @@ -1,6 +1,7 @@ from .auto import * # noqa from .customer import * # noqa from .document import * # noqa +from .history import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa diff --git a/core/apps/evaluation/models/history.py b/core/apps/evaluation/models/history.py new file mode 100644 index 0000000..d7deff8 --- /dev/null +++ b/core/apps/evaluation/models/history.py @@ -0,0 +1,124 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from core.apps.evaluation.choices.history import EvaluationEventType + + +class AutoevaluationhistoryModel(models.Model): + """AutoEvaluation bo'yicha barcha harakatlar logi — faqat o'qiladi, signallar tomonidan yoziladi.""" + + auto_evaluation = models.ForeignKey( + "evaluation.AutoEvaluationModel", + on_delete=models.CASCADE, + related_name="history", + verbose_name=_("auto evaluation"), + ) + event_type = models.CharField( + verbose_name=_("event type"), + max_length=50, + choices=EvaluationEventType.choices, + ) + + # Actor — kim bajargan (system bo'lsa actor_id=None) + actor_id = models.BigIntegerField( + verbose_name=_("actor id"), + null=True, + blank=True, + ) + actor_full_name = models.CharField( + verbose_name=_("actor full name"), + max_length=255, + default="Tizim", + ) + actor_role = models.CharField( + verbose_name=_("actor role"), + max_length=50, + default="system", + ) + + # Har bir event_type uchun turli struktura + meta = models.JSONField( + verbose_name=_("meta"), + default=dict, + blank=True, + ) + + created_at = models.DateTimeField( + verbose_name=_("created at"), + auto_now_add=True, + ) + + def __str__(self): + return f"{self.get_event_type_display()} — AutoEval #{self.auto_evaluation_id}" + + class Meta: + db_table = "AutoEvaluationHistory" + verbose_name = _("Auto Evaluation History") + verbose_name_plural = _("Auto Evaluation Histories") + ordering = ["created_at"] + indexes = [ + # Asosiy query: bir baholash tarixi + sana bo'yicha tartib + models.Index(fields=["auto_evaluation_id", "created_at"], name="auto_hist_eval_date_idx"), + # event_type bo'yicha filter + models.Index(fields=["event_type"], name="auto_hist_event_type_idx"), + ] + + +class QuickevaluationhistoryModel(models.Model): + """QuickEvaluation bo'yicha barcha harakatlar logi — faqat o'qiladi, signallar tomonidan yoziladi.""" + + quick_evaluation = models.ForeignKey( + "evaluation.QuickEvaluationModel", + on_delete=models.CASCADE, + related_name="history", + verbose_name=_("quick evaluation"), + ) + event_type = models.CharField( + verbose_name=_("event type"), + max_length=50, + choices=EvaluationEventType.choices, + ) + + # Actor — kim bajargan (system bo'lsa actor_id=None) + actor_id = models.BigIntegerField( + verbose_name=_("actor id"), + null=True, + blank=True, + ) + actor_full_name = models.CharField( + verbose_name=_("actor full name"), + max_length=255, + default="Tizim", + ) + actor_role = models.CharField( + verbose_name=_("actor role"), + max_length=50, + default="system", + ) + + # Har bir event_type uchun turli struktura + meta = models.JSONField( + verbose_name=_("meta"), + default=dict, + blank=True, + ) + + created_at = models.DateTimeField( + verbose_name=_("created at"), + auto_now_add=True, + ) + + def __str__(self): + return f"{self.get_event_type_display()} — QuickEval #{self.quick_evaluation_id}" + + class Meta: + db_table = "QuickEvaluationHistory" + verbose_name = _("Quick Evaluation History") + verbose_name_plural = _("Quick Evaluation Histories") + ordering = ["created_at"] + indexes = [ + # Asosiy query: bir baholash tarixi + sana bo'yicha tartib + models.Index(fields=["quick_evaluation_id", "created_at"], name="quick_hist_eval_date_idx"), + # event_type bo'yicha filter + models.Index(fields=["event_type"], name="quick_hist_event_type_idx"), + ] diff --git a/core/apps/evaluation/permissions/__init__.py b/core/apps/evaluation/permissions/__init__.py index e1e2f06..bc24d35 100644 --- a/core/apps/evaluation/permissions/__init__.py +++ b/core/apps/evaluation/permissions/__init__.py @@ -1,6 +1,7 @@ from .auto import * # noqa from .customer import * # noqa from .document import * # noqa +from .history import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa diff --git a/core/apps/evaluation/permissions/history.py b/core/apps/evaluation/permissions/history.py new file mode 100644 index 0000000..b793dda --- /dev/null +++ b/core/apps/evaluation/permissions/history.py @@ -0,0 +1,23 @@ +from rest_framework import permissions + + +class AutoevaluationhistoryPermission(permissions.BasePermission): + + def __init__(self) -> None: ... + + def __call__(self, *args, **kwargs): + return self + + def has_permission(self, request, view): + return True + + +class QuickevaluationhistoryPermission(permissions.BasePermission): + + def __init__(self) -> None: ... + + def __call__(self, *args, **kwargs): + return self + + def has_permission(self, request, view): + return True diff --git a/core/apps/evaluation/serializers/__init__.py b/core/apps/evaluation/serializers/__init__.py index e1e2f06..bc24d35 100644 --- a/core/apps/evaluation/serializers/__init__.py +++ b/core/apps/evaluation/serializers/__init__.py @@ -1,6 +1,7 @@ from .auto import * # noqa from .customer import * # noqa from .document import * # noqa +from .history import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa diff --git a/core/apps/evaluation/serializers/history/AutoEvaluationHistory.py b/core/apps/evaluation/serializers/history/AutoEvaluationHistory.py new file mode 100644 index 0000000..593488b --- /dev/null +++ b/core/apps/evaluation/serializers/history/AutoEvaluationHistory.py @@ -0,0 +1,28 @@ +from rest_framework import serializers + +from core.apps.evaluation.models import AutoevaluationhistoryModel + + +class BaseAutoevaluationhistorySerializer(serializers.ModelSerializer): + class Meta: + model = AutoevaluationhistoryModel + fields = [ + "id", + "name", + ] + + +class ListAutoevaluationhistorySerializer(BaseAutoevaluationhistorySerializer): + class Meta(BaseAutoevaluationhistorySerializer.Meta): ... + + +class RetrieveAutoevaluationhistorySerializer(BaseAutoevaluationhistorySerializer): + class Meta(BaseAutoevaluationhistorySerializer.Meta): ... + + +class CreateAutoevaluationhistorySerializer(BaseAutoevaluationhistorySerializer): + class Meta(BaseAutoevaluationhistorySerializer.Meta): + fields = [ + "id", + "name", + ] diff --git a/core/apps/evaluation/serializers/history/History.py b/core/apps/evaluation/serializers/history/History.py new file mode 100644 index 0000000..db79fe4 --- /dev/null +++ b/core/apps/evaluation/serializers/history/History.py @@ -0,0 +1,62 @@ +from rest_framework import serializers + +from core.apps.evaluation.models import AutoevaluationhistoryModel, QuickevaluationhistoryModel + + +class BaseHistorySerializer(serializers.ModelSerializer): + event_type_display = serializers.CharField(source="get_event_type_display", read_only=True) + actor = serializers.SerializerMethodField() + + def get_actor(self, obj): + return { + "id": obj.actor_id, + "full_name": obj.actor_full_name, + "role": obj.actor_role, + } + + +# ─── AutoEvaluation History ──────────────────────────────────────────────── + +class ListAutoevaluationhistorySerializer(BaseHistorySerializer): + class Meta: + model = AutoevaluationhistoryModel + fields = [ + "id", + "event_type", + "event_type_display", + "actor", + "meta", + "created_at", + ] + + +class RetrieveAutoevaluationhistorySerializer(ListAutoevaluationhistorySerializer): + class Meta(ListAutoevaluationhistorySerializer.Meta): + fields = ListAutoevaluationhistorySerializer.Meta.fields + ["auto_evaluation"] + + +# Read-only — API orqali yozilmaydi, faqat signallar yozadi +CreateAutoevaluationhistorySerializer = ListAutoevaluationhistorySerializer + + +# ─── QuickEvaluation History ─────────────────────────────────────────────── + +class ListQuickevaluationhistorySerializer(BaseHistorySerializer): + class Meta: + model = QuickevaluationhistoryModel + fields = [ + "id", + "event_type", + "event_type_display", + "actor", + "meta", + "created_at", + ] + + +class RetrieveQuickevaluationhistorySerializer(ListQuickevaluationhistorySerializer): + class Meta(ListQuickevaluationhistorySerializer.Meta): + fields = ListQuickevaluationhistorySerializer.Meta.fields + ["quick_evaluation"] + + +CreateQuickevaluationhistorySerializer = ListQuickevaluationhistorySerializer diff --git a/core/apps/evaluation/serializers/history/QuickEvaluationHistory.py b/core/apps/evaluation/serializers/history/QuickEvaluationHistory.py new file mode 100644 index 0000000..7990dad --- /dev/null +++ b/core/apps/evaluation/serializers/history/QuickEvaluationHistory.py @@ -0,0 +1,28 @@ +from rest_framework import serializers + +from core.apps.evaluation.models import QuickevaluationhistoryModel + + +class BaseQuickevaluationhistorySerializer(serializers.ModelSerializer): + class Meta: + model = QuickevaluationhistoryModel + fields = [ + "id", + "name", + ] + + +class ListQuickevaluationhistorySerializer(BaseQuickevaluationhistorySerializer): + class Meta(BaseQuickevaluationhistorySerializer.Meta): ... + + +class RetrieveQuickevaluationhistorySerializer(BaseQuickevaluationhistorySerializer): + class Meta(BaseQuickevaluationhistorySerializer.Meta): ... + + +class CreateQuickevaluationhistorySerializer(BaseQuickevaluationhistorySerializer): + class Meta(BaseQuickevaluationhistorySerializer.Meta): + fields = [ + "id", + "name", + ] diff --git a/core/apps/evaluation/serializers/history/__init__.py b/core/apps/evaluation/serializers/history/__init__.py new file mode 100644 index 0000000..1506100 --- /dev/null +++ b/core/apps/evaluation/serializers/history/__init__.py @@ -0,0 +1,8 @@ +from .History import ( # noqa + CreateAutoevaluationhistorySerializer, + CreateQuickevaluationhistorySerializer, + ListAutoevaluationhistorySerializer, + ListQuickevaluationhistorySerializer, + RetrieveAutoevaluationhistorySerializer, + RetrieveQuickevaluationhistorySerializer, +) diff --git a/core/apps/evaluation/services/__init__.py b/core/apps/evaluation/services/__init__.py new file mode 100644 index 0000000..7c367dd --- /dev/null +++ b/core/apps/evaluation/services/__init__.py @@ -0,0 +1 @@ +from .history import EvaluationHistoryService # noqa diff --git a/core/apps/evaluation/services/history.py b/core/apps/evaluation/services/history.py new file mode 100644 index 0000000..4b8a6fd --- /dev/null +++ b/core/apps/evaluation/services/history.py @@ -0,0 +1,51 @@ +from core.apps.evaluation.choices.history import EvaluationEventType + + +class EvaluationHistoryService: + """ + AutoEvaluation va QuickEvaluation uchun history yozuvlarini yaratuvchi servis. + Faqat shu servis orqali yoziladi — frontend va boshqa kodlar bu sinfdan foydalanadi. + """ + + @staticmethod + def _build_actor(user) -> dict: + if user is None: + return {"id": None, "full_name": "Tizim", "role": "system"} + full_name = "" + if hasattr(user, "get_full_name"): + full_name = user.get_full_name().strip() + if not full_name and hasattr(user, "phone"): + full_name = str(user.phone) + return { + "id": user.id, + "full_name": full_name or str(user), + "role": getattr(user, "role", "user"), + } + + @classmethod + def log_auto(cls, auto_evaluation, event_type: EvaluationEventType, actor=None, meta: dict = None): + from core.apps.evaluation.models import AutoevaluationhistoryModel + + actor_data = cls._build_actor(actor) + AutoevaluationhistoryModel.objects.create( + auto_evaluation=auto_evaluation, + event_type=event_type, + actor_id=actor_data["id"], + actor_full_name=actor_data["full_name"], + actor_role=actor_data["role"], + meta=meta or {}, + ) + + @classmethod + def log_quick(cls, quick_evaluation, event_type: EvaluationEventType, actor=None, meta: dict = None): + from core.apps.evaluation.models import QuickevaluationhistoryModel + + actor_data = cls._build_actor(actor) + QuickevaluationhistoryModel.objects.create( + quick_evaluation=quick_evaluation, + event_type=event_type, + actor_id=actor_data["id"], + actor_full_name=actor_data["full_name"], + actor_role=actor_data["role"], + meta=meta or {}, + ) diff --git a/core/apps/evaluation/signals/__init__.py b/core/apps/evaluation/signals/__init__.py index e1e2f06..bc24d35 100644 --- a/core/apps/evaluation/signals/__init__.py +++ b/core/apps/evaluation/signals/__init__.py @@ -1,6 +1,7 @@ from .auto import * # noqa from .customer import * # noqa from .document import * # noqa +from .history import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa diff --git a/core/apps/evaluation/signals/history.py b/core/apps/evaluation/signals/history.py new file mode 100644 index 0000000..a4a665b --- /dev/null +++ b/core/apps/evaluation/signals/history.py @@ -0,0 +1,210 @@ +from django.db.models.signals import post_save, pre_save +from django.dispatch import receiver + +from core.apps.evaluation.choices.auto import AutoEvaluationStatus +from core.apps.evaluation.choices.history import EvaluationEventType +from core.apps.evaluation.choices.quick import QuickEvaluationStatus +from core.apps.evaluation.models import ( + AutoEvaluationModel, + QuickEvaluationModel, + ValuationDocumentModel, + ValuationModel, +) +from core.apps.evaluation.services.history import EvaluationHistoryService + + +# ───────────────────────────────────────────────────────────────────────────── +# AutoEvaluation — order_created va status_changed +# ───────────────────────────────────────────────────────────────────────────── + +@receiver(pre_save, sender=AutoEvaluationModel) +def auto_eval_pre_save(sender, instance, **kwargs): + """Status o'zgarishini kuzatish uchun eski qiymatni saqlaydi.""" + if instance.pk: + try: + old = AutoEvaluationModel.objects.get(pk=instance.pk) + instance._old_status = old.status + except AutoEvaluationModel.DoesNotExist: + instance._old_status = None + else: + instance._old_status = None + + +@receiver(post_save, sender=AutoEvaluationModel) +def auto_eval_post_save(sender, instance, created, **kwargs): + if created: + actor = None + if instance.valuation_id: + try: + actor = instance.valuation.created_by + except Exception: + pass + EvaluationHistoryService.log_auto( + auto_evaluation=instance, + event_type=EvaluationEventType.ORDER_CREATED, + actor=actor, + ) + else: + old_status = getattr(instance, "_old_status", None) + if old_status is not None and old_status != instance.status: + status_labels = dict(AutoEvaluationStatus.choices) + EvaluationHistoryService.log_auto( + auto_evaluation=instance, + event_type=EvaluationEventType.STATUS_CHANGED, + meta={ + "from_status": old_status, + "from_status_display": status_labels.get(old_status, old_status), + "to_status": instance.status, + "to_status_display": status_labels.get(instance.status, instance.status), + }, + ) + + +# ───────────────────────────────────────────────────────────────────────────── +# Valuation — evaluator_assigned (assigned_to o'zgarganda) +# ───────────────────────────────────────────────────────────────────────────── + +@receiver(pre_save, sender=ValuationModel) +def valuation_pre_save(sender, instance, **kwargs): + """assigned_to o'zgarishini kuzatish uchun eski qiymatni saqlaydi.""" + if instance.pk: + try: + old = ValuationModel.objects.get(pk=instance.pk) + instance._old_assigned_to_id = old.assigned_to_id + except ValuationModel.DoesNotExist: + instance._old_assigned_to_id = None + else: + instance._old_assigned_to_id = None + + +@receiver(post_save, sender=ValuationModel) +def valuation_post_save(sender, instance, created, **kwargs): + if created: + return + old_assigned_id = getattr(instance, "_old_assigned_to_id", None) + if instance.assigned_to_id and old_assigned_id != instance.assigned_to_id: + try: + auto_eval = instance.auto_detail + except AutoEvaluationModel.DoesNotExist: + return + evaluator = instance.assigned_to + evaluator_name = "" + if evaluator: + evaluator_name = evaluator.get_full_name().strip() or str(evaluator.phone) + EvaluationHistoryService.log_auto( + auto_evaluation=auto_eval, + event_type=EvaluationEventType.EVALUATOR_ASSIGNED, + meta={ + "evaluator_id": instance.assigned_to_id, + "evaluator_name": evaluator_name, + }, + ) + + +# ───────────────────────────────────────────────────────────────────────────── +# ValuationDocument — document_uploaded +# ───────────────────────────────────────────────────────────────────────────── + +@receiver(post_save, sender=ValuationDocumentModel) +def document_post_save(sender, instance, created, **kwargs): + if not created: + return + try: + auto_eval = instance.valuation.auto_detail + except (AttributeError, AutoEvaluationModel.DoesNotExist): + return + file_name = "" + if instance.file: + file_name = instance.file.name.split("/")[-1] + EvaluationHistoryService.log_auto( + auto_evaluation=auto_eval, + event_type=EvaluationEventType.DOCUMENT_UPLOADED, + actor=instance.uploaded_by, + meta={ + "document_type": instance.document_type, + "document_type_display": instance.get_document_type_display(), + "file_name": file_name, + "title": instance.title or "", + }, + ) + + +# ───────────────────────────────────────────────────────────────────────────── +# Payment — payment_made (status COMPLETED bo'lganda) +# ───────────────────────────────────────────────────────────────────────────── + +@receiver(pre_save, sender="payment.PaymentModel") +def payment_pre_save(sender, instance, **kwargs): + if instance.pk: + try: + old = sender.objects.get(pk=instance.pk) + instance._old_payment_status = old.status + except sender.DoesNotExist: + instance._old_payment_status = None + else: + instance._old_payment_status = None + + +@receiver(post_save, sender="payment.PaymentModel") +def payment_post_save(sender, instance, created, **kwargs): + old_status = getattr(instance, "_old_payment_status", None) + is_newly_completed = ( + instance.status == "completed" + and (created or old_status != "completed") + ) + if not is_newly_completed: + return + try: + auto_eval = instance.valuation.auto_detail + except (AttributeError, AutoEvaluationModel.DoesNotExist): + return + EvaluationHistoryService.log_auto( + auto_evaluation=auto_eval, + event_type=EvaluationEventType.PAYMENT_MADE, + actor=instance.payer, + meta={ + "amount": str(instance.amount), + "payment_method": instance.payment_method, + "transaction_id": instance.transaction_id or "", + }, + ) + + +# ───────────────────────────────────────────────────────────────────────────── +# QuickEvaluation — order_created va status_changed +# ───────────────────────────────────────────────────────────────────────────── + +@receiver(pre_save, sender=QuickEvaluationModel) +def quick_eval_pre_save(sender, instance, **kwargs): + if instance.pk: + try: + old = QuickEvaluationModel.objects.get(pk=instance.pk) + instance._old_status = old.status + except QuickEvaluationModel.DoesNotExist: + instance._old_status = None + else: + instance._old_status = None + + +@receiver(post_save, sender=QuickEvaluationModel) +def quick_eval_post_save(sender, instance, created, **kwargs): + if created: + EvaluationHistoryService.log_quick( + quick_evaluation=instance, + event_type=EvaluationEventType.ORDER_CREATED, + actor=instance.created_by, + ) + else: + old_status = getattr(instance, "_old_status", None) + if old_status is not None and old_status != instance.status: + status_labels = dict(QuickEvaluationStatus.choices) + EvaluationHistoryService.log_quick( + quick_evaluation=instance, + event_type=EvaluationEventType.STATUS_CHANGED, + meta={ + "from_status": old_status, + "from_status_display": status_labels.get(old_status, old_status), + "to_status": instance.status, + "to_status_display": status_labels.get(instance.status, instance.status), + }, + ) diff --git a/core/apps/evaluation/tests/__init__.py b/core/apps/evaluation/tests/__init__.py index e1e2f06..bc24d35 100644 --- a/core/apps/evaluation/tests/__init__.py +++ b/core/apps/evaluation/tests/__init__.py @@ -1,6 +1,7 @@ from .auto import * # noqa from .customer import * # noqa from .document import * # noqa +from .history import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa diff --git a/core/apps/evaluation/tests/history/__init__.py b/core/apps/evaluation/tests/history/__init__.py new file mode 100644 index 0000000..52db76b --- /dev/null +++ b/core/apps/evaluation/tests/history/__init__.py @@ -0,0 +1,2 @@ +from .test_AutoEvaluationHistory import * # noqa +from .test_QuickEvaluationHistory import * # noqa diff --git a/core/apps/evaluation/tests/history/test_AutoEvaluationHistory.py b/core/apps/evaluation/tests/history/test_AutoEvaluationHistory.py new file mode 100644 index 0000000..dd10b1d --- /dev/null +++ b/core/apps/evaluation/tests/history/test_AutoEvaluationHistory.py @@ -0,0 +1,101 @@ +import pytest +from django.urls import reverse +from rest_framework.test import APIClient + +from core.apps.evaluation.models import AutoevaluationhistoryModel + + +@pytest.fixture +def instance(db): + return AutoevaluationhistoryModel._baker() + + +@pytest.fixture +def api_client(instance): + client = APIClient() + ##client.force_authenticate(user=instance.user) + return client, instance + + +@pytest.fixture +def data(api_client): + client, instance = api_client + return ( + { + "list": reverse("auto-evaluation-history-list"), + "retrieve": reverse("auto-evaluation-history-detail", kwargs={"pk": instance.pk}), + "retrieve-not-found": reverse("auto-evaluation-history-detail", kwargs={"pk": 1000}), + }, + client, + instance, + ) + + +@pytest.mark.django_db +def test_list(data): + urls, client, _ = data + response = client.get(urls["list"]) + data_resp = response.json() + assert response.status_code == 200 + assert data_resp["status"] is True + + +@pytest.mark.django_db +def test_retrieve(data): + urls, client, _ = data + response = client.get(urls["retrieve"]) + data_resp = response.json() + assert response.status_code == 200 + assert data_resp["status"] is True + + +@pytest.mark.django_db +def test_retrieve_not_found(data): + urls, client, _ = data + response = client.get(urls["retrieve-not-found"]) + data_resp = response.json() + assert response.status_code == 404 + assert data_resp["status"] is False + + +# @pytest.mark.django_db +# def test_create(data): +# urls, client, _ = data +# response = client.post(urls["list"], data={"name": "test"}) +# assert response.json()["status"] is True +# assert response.status_code == 201 + + +# @pytest.mark.django_db +# def test_update(data): +# urls, client, _ = data +# response = client.patch(urls["retrieve"], data={"name": "updated"}) +# assert response.json()["status"] is True +# assert response.status_code == 200 +# +# # verify updated value +# response = client.get(urls["retrieve"]) +# assert response.json()["status"] is True +# assert response.status_code == 200 +# assert response.json()["data"]["name"] == "updated" + + +# @pytest.mark.django_db +# def test_partial_update(): +# urls, client, _ = data +# response = client.patch(urls["retrieve"], data={"name": "updated"}) +# assert response.json()["status"] is True +# assert response.status_code == 200 +# +# # verify updated value +# response = client.get(urls["retrieve"]) +# assert response.json()["status"] is True +# assert response.status_code == 200 +# assert response.json()["data"]["name"] == "updated" + + +# @pytest.mark.django_db +# def test_destroy(data): +# urls, client, _ = data +# response = client.delete(urls["retrieve"]) +# assert response.status_code == 204 diff --git a/core/apps/evaluation/tests/history/test_QuickEvaluationHistory.py b/core/apps/evaluation/tests/history/test_QuickEvaluationHistory.py new file mode 100644 index 0000000..3e83356 --- /dev/null +++ b/core/apps/evaluation/tests/history/test_QuickEvaluationHistory.py @@ -0,0 +1,101 @@ +import pytest +from django.urls import reverse +from rest_framework.test import APIClient + +from core.apps.evaluation.models import QuickevaluationhistoryModel + + +@pytest.fixture +def instance(db): + return QuickevaluationhistoryModel._baker() + + +@pytest.fixture +def api_client(instance): + client = APIClient() + ##client.force_authenticate(user=instance.user) + return client, instance + + +@pytest.fixture +def data(api_client): + client, instance = api_client + return ( + { + "list": reverse("quick-evaluation-history-list"), + "retrieve": reverse("quick-evaluation-history-detail", kwargs={"pk": instance.pk}), + "retrieve-not-found": reverse("quick-evaluation-history-detail", kwargs={"pk": 1000}), + }, + client, + instance, + ) + + +@pytest.mark.django_db +def test_list(data): + urls, client, _ = data + response = client.get(urls["list"]) + data_resp = response.json() + assert response.status_code == 200 + assert data_resp["status"] is True + + +@pytest.mark.django_db +def test_retrieve(data): + urls, client, _ = data + response = client.get(urls["retrieve"]) + data_resp = response.json() + assert response.status_code == 200 + assert data_resp["status"] is True + + +@pytest.mark.django_db +def test_retrieve_not_found(data): + urls, client, _ = data + response = client.get(urls["retrieve-not-found"]) + data_resp = response.json() + assert response.status_code == 404 + assert data_resp["status"] is False + + +# @pytest.mark.django_db +# def test_create(data): +# urls, client, _ = data +# response = client.post(urls["list"], data={"name": "test"}) +# assert response.json()["status"] is True +# assert response.status_code == 201 + + +# @pytest.mark.django_db +# def test_update(data): +# urls, client, _ = data +# response = client.patch(urls["retrieve"], data={"name": "updated"}) +# assert response.json()["status"] is True +# assert response.status_code == 200 +# +# # verify updated value +# response = client.get(urls["retrieve"]) +# assert response.json()["status"] is True +# assert response.status_code == 200 +# assert response.json()["data"]["name"] == "updated" + + +# @pytest.mark.django_db +# def test_partial_update(): +# urls, client, _ = data +# response = client.patch(urls["retrieve"], data={"name": "updated"}) +# assert response.json()["status"] is True +# assert response.status_code == 200 +# +# # verify updated value +# response = client.get(urls["retrieve"]) +# assert response.json()["status"] is True +# assert response.status_code == 200 +# assert response.json()["data"]["name"] == "updated" + + +# @pytest.mark.django_db +# def test_destroy(data): +# urls, client, _ = data +# response = client.delete(urls["retrieve"]) +# assert response.status_code == 204 diff --git a/core/apps/evaluation/translation/__init__.py b/core/apps/evaluation/translation/__init__.py index e1e2f06..bc24d35 100644 --- a/core/apps/evaluation/translation/__init__.py +++ b/core/apps/evaluation/translation/__init__.py @@ -1,6 +1,7 @@ from .auto import * # noqa from .customer import * # noqa from .document import * # noqa +from .history import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa diff --git a/core/apps/evaluation/translation/history.py b/core/apps/evaluation/translation/history.py new file mode 100644 index 0000000..8eb7ddf --- /dev/null +++ b/core/apps/evaluation/translation/history.py @@ -0,0 +1,13 @@ +from modeltranslation.translator import TranslationOptions, register + +from core.apps.evaluation.models import AutoevaluationhistoryModel, QuickevaluationhistoryModel + + +@register(AutoevaluationhistoryModel) +class AutoevaluationhistoryTranslation(TranslationOptions): + fields = [] + + +@register(QuickevaluationhistoryModel) +class QuickevaluationhistoryTranslation(TranslationOptions): + fields = [] diff --git a/core/apps/evaluation/urls.py b/core/apps/evaluation/urls.py index be8b883..cf6e9be 100644 --- a/core/apps/evaluation/urls.py +++ b/core/apps/evaluation/urls.py @@ -2,25 +2,29 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter from .views import ( + AutoEvaluationHistoryView, AutoEvaluationView, CustomerView, + DeterminedValueView, + EvaluationPurposeView, EvaluationReportView, EvaluationrequestView, MovablePropertyEvaluationView, + OwnershipFormView, PropertyOwnerView, + PropertyRightsView, + QuickEvaluationHistoryView, QuickEvaluationView, RealEstateEvaluationView, ReferenceitemView, ValuationDocumentView, ValuationView, VehicleView, - EvaluationPurposeView, - DeterminedValueView, - PropertyRightsView, - OwnershipFormView, ) router = DefaultRouter() +router.register("auto-evaluation-history", AutoEvaluationHistoryView, basename="auto-evaluation-history") +router.register("quick-evaluation-history", QuickEvaluationHistoryView, basename="quick-evaluation-history") router.register("determined-value", DeterminedValueView, basename="determined-value") router.register("evaluation-purpose", EvaluationPurposeView, basename="evaluation-purpose") router.register("property-rights", PropertyRightsView, basename="property-rights") diff --git a/core/apps/evaluation/validators/__init__.py b/core/apps/evaluation/validators/__init__.py index e1e2f06..bc24d35 100644 --- a/core/apps/evaluation/validators/__init__.py +++ b/core/apps/evaluation/validators/__init__.py @@ -1,6 +1,7 @@ from .auto import * # noqa from .customer import * # noqa from .document import * # noqa +from .history import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa diff --git a/core/apps/evaluation/validators/history.py b/core/apps/evaluation/validators/history.py new file mode 100644 index 0000000..d78faa9 --- /dev/null +++ b/core/apps/evaluation/validators/history.py @@ -0,0 +1,15 @@ +# from django.core.exceptions import ValidationError + + +class AutoevaluationhistoryValidator: + def __init__(self): ... + + def __call__(self): + return True + + +class QuickevaluationhistoryValidator: + def __init__(self): ... + + def __call__(self): + return True diff --git a/core/apps/evaluation/views/__init__.py b/core/apps/evaluation/views/__init__.py index e1e2f06..bc24d35 100644 --- a/core/apps/evaluation/views/__init__.py +++ b/core/apps/evaluation/views/__init__.py @@ -1,6 +1,7 @@ from .auto import * # noqa from .customer import * # noqa from .document import * # noqa +from .history import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa diff --git a/core/apps/evaluation/views/history.py b/core/apps/evaluation/views/history.py new file mode 100644 index 0000000..20ab8dd --- /dev/null +++ b/core/apps/evaluation/views/history.py @@ -0,0 +1,109 @@ +from django_core.mixins import BaseViewSetMixin +from django_filters.rest_framework import DjangoFilterBackend +from drf_spectacular.utils import OpenApiParameter, extend_schema +from rest_framework.filters import OrderingFilter +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework.viewsets import ReadOnlyModelViewSet + +from core.apps.evaluation.filters.history import ( + AutoevaluationhistoryFilter, + QuickevaluationhistoryFilter, +) +from core.apps.evaluation.models import AutoevaluationhistoryModel, QuickevaluationhistoryModel +from core.apps.evaluation.serializers.history import ( + CreateAutoevaluationhistorySerializer, + CreateQuickevaluationhistorySerializer, + ListAutoevaluationhistorySerializer, + ListQuickevaluationhistorySerializer, + RetrieveAutoevaluationhistorySerializer, + RetrieveQuickevaluationhistorySerializer, +) + + +@extend_schema( + tags=["AutoEvaluationHistory"], + parameters=[ + OpenApiParameter("auto_evaluation", int, description="AutoEvaluation ID bo'yicha filter"), + OpenApiParameter("event_type", str, description="Event turi bo'yicha filter"), + OpenApiParameter("created_from", str, description="Boshlanish sanasi (ISO 8601)"), + OpenApiParameter("created_to", str, description="Tugash sanasi (ISO 8601)"), + ], +) +class AutoEvaluationHistoryView(BaseViewSetMixin, ReadOnlyModelViewSet): + # select_related("auto_evaluation") faqat retrieve uchun — list uchun kerak emas + queryset = AutoevaluationhistoryModel.objects.only( + "id", "auto_evaluation_id", "event_type", + "actor_id", "actor_full_name", "actor_role", + "meta", "created_at", + ) + serializer_class = ListAutoevaluationhistorySerializer + permission_classes = [AllowAny] + pagination_class = None + + filter_backends = [DjangoFilterBackend, OrderingFilter] + filterset_class = AutoevaluationhistoryFilter + ordering_fields = ["created_at"] + ordering = ["created_at"] + + action_permission_classes = {} + action_serializer_class = { + "list": ListAutoevaluationhistorySerializer, + "retrieve": RetrieveAutoevaluationhistorySerializer, + "create": CreateAutoevaluationhistorySerializer, + } + + def list(self, request, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset()) + # Queryset bir marta evaluate qilinadi — COUNT uchun alohida query yo'q + results = list(queryset) + serializer = self.get_serializer(results, many=True) + return Response({ + "count": len(results), + "next": None, + "previous": None, + "results": serializer.data, + }) + + +@extend_schema( + tags=["QuickEvaluationHistory"], + parameters=[ + OpenApiParameter("quick_evaluation", int, description="QuickEvaluation ID bo'yicha filter"), + OpenApiParameter("event_type", str, description="Event turi bo'yicha filter"), + OpenApiParameter("created_from", str, description="Boshlanish sanasi (ISO 8601)"), + OpenApiParameter("created_to", str, description="Tugash sanasi (ISO 8601)"), + ], +) +class QuickEvaluationHistoryView(BaseViewSetMixin, ReadOnlyModelViewSet): + queryset = QuickevaluationhistoryModel.objects.only( + "id", "quick_evaluation_id", "event_type", + "actor_id", "actor_full_name", "actor_role", + "meta", "created_at", + ) + serializer_class = ListQuickevaluationhistorySerializer + permission_classes = [AllowAny] + pagination_class = None + + filter_backends = [DjangoFilterBackend, OrderingFilter] + filterset_class = QuickevaluationhistoryFilter + ordering_fields = ["created_at"] + ordering = ["created_at"] + + action_permission_classes = {} + action_serializer_class = { + "list": ListQuickevaluationhistorySerializer, + "retrieve": RetrieveQuickevaluationhistorySerializer, + "create": CreateQuickevaluationhistorySerializer, + } + + def list(self, request, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset()) + results = list(queryset) + serializer = self.get_serializer(results, many=True) + return Response({ + "count": len(results), + "next": None, + "previous": None, + "results": serializer.data, + })