From 12c6cefb2219fb2b47b56dd78ea22f78e7e79008 Mon Sep 17 00:00:00 2001 From: PouyaKhajavi Date: Sat, 8 Aug 2026 11:33:32 +0330 Subject: [PATCH] feat(ads): implement serializers, views, and routing for Ads and Evaluations --- backend/ads/migrations/0001_initial.py | 74 ++++++++++++++++++++++++++ backend/ads/serializers.py | 51 ++++++++++++++++++ backend/ads/urls.py | 11 ++-- backend/ads/views.py | 27 +++++++--- 4 files changed, 153 insertions(+), 10 deletions(-) create mode 100644 backend/ads/migrations/0001_initial.py create mode 100644 backend/ads/serializers.py diff --git a/backend/ads/migrations/0001_initial.py b/backend/ads/migrations/0001_initial.py new file mode 100644 index 0000000..d84089e --- /dev/null +++ b/backend/ads/migrations/0001_initial.py @@ -0,0 +1,74 @@ +# Generated by Django 6.0.8 on 2026-08-08 07:53 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('crawler', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Ad', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('divar_token', models.CharField(max_length=50, unique=True)), + ('title', models.CharField(max_length=255)), + ('description', models.TextField()), + ('price', models.CharField(blank=True, max_length=100, null=True)), + ('category', models.CharField(blank=True, max_length=100, null=True)), + ('images', models.JSONField(default=list)), + ('url', models.URLField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('published_at', models.DateTimeField(blank=True, null=True)), + ], + options={ + 'indexes': [models.Index(fields=['created_at'], name='ads_ad_created_9f5b83_idx')], + }, + ), + migrations.CreateModel( + name='AdEvaluation', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('is_flagged', models.BooleanField(default=False)), + ('reason', models.TextField(blank=True, null=True)), + ('confidence', models.FloatField(blank=True, null=True)), + ('extracted_fields', models.JSONField(default=dict)), + ('evaluated_at', models.DateTimeField(auto_now_add=True)), + ('ad', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='evaluations', to='ads.ad')), + ('crawl_task', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='evaluations', to='crawler.crawltask')), + ], + ), + migrations.CreateModel( + name='NotificationLog', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('channel_id', models.CharField(max_length=100)), + ('sent_at', models.DateTimeField(auto_now_add=True)), + ('status', models.CharField(choices=[('SENT', 'Sent'), ('FAILED', 'Failed')], max_length=20)), + ('error_message', models.TextField(blank=True, null=True)), + ('evaluation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='ads.adevaluation')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddIndex( + model_name='adevaluation', + index=models.Index(fields=['is_flagged'], name='ads_adevalu_is_flag_1e9b55_idx'), + ), + migrations.AddIndex( + model_name='adevaluation', + index=models.Index(fields=['evaluated_at'], name='ads_adevalu_evaluat_9e55c2_idx'), + ), + migrations.AddConstraint( + model_name='adevaluation', + constraint=models.UniqueConstraint(fields=('crawl_task', 'ad'), name='unique_crawl_task_ad'), + ), + ] diff --git a/backend/ads/serializers.py b/backend/ads/serializers.py new file mode 100644 index 0000000..f95e1c4 --- /dev/null +++ b/backend/ads/serializers.py @@ -0,0 +1,51 @@ +from rest_framework import serializers +from .models import Ad, AdEvaluation, NotificationLog + +class AdSerializer(serializers.ModelSerializer): + class Meta: + model = Ad + fields = [ + 'id', + 'divar_token', + 'title', + 'description', + 'price', + 'category', + 'images', + 'url', + 'created_at', + 'published_at', + ] + read_only_fields = ['id', 'created_at'] + + +class AdEvaluationSerializer(serializers.ModelSerializer): + ad = AdSerializer(read_only=True) + + class Meta: + model = AdEvaluation + fields = [ + 'id', + 'crawl_task', + 'ad', + 'is_flagged', + 'reason', + 'confidence', + 'extracted_fields', + 'evaluated_at', + ] + read_only_fields = ['id', 'evaluated_at'] + + +class NotificationLogSerializer(serializers.ModelSerializer): + class Meta: + model = NotificationLog + fields = [ + 'id', + 'evaluation', + 'channel_id', + 'sent_at', + 'status', + 'error_message', + ] + read_only_fields = ['id', 'sent_at'] diff --git a/backend/ads/urls.py b/backend/ads/urls.py index a4a9518..e826485 100644 --- a/backend/ads/urls.py +++ b/backend/ads/urls.py @@ -1,6 +1,11 @@ -from django.urls import path -from .views import AdsView +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import AdEvaluationViewSet + +router = DefaultRouter() +router.register(r'', AdEvaluationViewSet, basename='ads') urlpatterns = [ - path('', AdsView.as_view()), + path('', include(router.urls)), ] + diff --git a/backend/ads/views.py b/backend/ads/views.py index 6a8dac0..d43c9ce 100644 --- a/backend/ads/views.py +++ b/backend/ads/views.py @@ -1,10 +1,23 @@ -from django.shortcuts import render -from rest_framework.views import APIView -from rest_framework.response import Response +from rest_framework import viewsets +from .models import AdEvaluation +from .serializers import AdEvaluationSerializer -# Create your views here. +class AdEvaluationViewSet(viewsets.ReadOnlyModelViewSet): + """ + ViewSet for listing and viewing Ad Evaluations. + Supports filtering by crawl_task (UUID) and is_flagged (boolean). + """ + serializer_class = AdEvaluationSerializer -class AdsView(APIView): - def get(self, request): - return Response({'message': 'Hello from Ads'}) + def get_queryset(self): + queryset = AdEvaluation.objects.all().select_related('ad', 'crawl_task').order_by('-evaluated_at') + crawl_task_id = self.request.query_params.get('crawl_task') + is_flagged = self.request.query_params.get('is_flagged') + if crawl_task_id: + queryset = queryset.filter(crawl_task_id=crawl_task_id) + if is_flagged is not None: + is_flagged_bool = is_flagged.lower() in ('true', '1', 't') + queryset = queryset.filter(is_flagged=is_flagged_bool) + + return queryset