Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions estela-api/api/serializers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ class ProjectSerializer(serializers.ModelSerializer):
container_image = serializers.CharField(
read_only=True, help_text="Path of the project's container image."
)
created = serializers.SerializerMethodField()
last_modified = serializers.SerializerMethodField()

def get_created(self, obj):
value = obj.created
if value is None:
return None
return value.isoformat()

def get_last_modified(self, obj):
value = obj.last_modified
if value is None:
return None
return value.isoformat()

class Meta:
model = Project
Expand All @@ -64,6 +78,8 @@ class Meta:
"env_vars",
"data_status",
"data_expiry_days",
"created",
"last_modified",
)


Expand Down
49 changes: 48 additions & 1 deletion estela-api/api/views/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from django.conf import settings
from django.core.paginator import Paginator
from django.db.models import OuterRef, Subquery
from django.shortcuts import get_object_or_404
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
Expand Down Expand Up @@ -55,12 +56,58 @@ def get_parameters(self, request):
)
return page, page_size

ALLOWED_ORDERING_FIELDS = {
"name", "-name",
"category", "-category",
"framework", "-framework",
"created", "-created",
"last_modified", "-last_modified",
"role", "-role",
}

def get_queryset(self):
return (

queryset = (
Project.objects.filter(deleted=False)
if self.request.user.is_superuser or self.request.user.is_staff
else self.request.user.project_set.filter(deleted=False)
)
search = self.request.query_params.get("search", "")
if search:
queryset = queryset.filter(name__icontains=search)
ordering = self.request.query_params.get("ordering", "")
if ordering in self.ALLOWED_ORDERING_FIELDS:
field = ordering.lstrip("-")
prefix = "-" if ordering.startswith("-") else ""
if field == "created":
queryset = queryset.order_by(f"{prefix}created")
elif field == "last_modified":
queryset = queryset.order_by(f"{prefix}last_modified")
elif field == "role":
role_subquery = Subquery(
Permission.objects.filter(
project=OuterRef("pk"),
user=self.request.user,
).values("permission")[:1]
)
queryset = queryset.annotate(
user_role=role_subquery
).order_by(f"{prefix}user_role")
else:
queryset = queryset.order_by(ordering)
return queryset

@swagger_auto_schema(
manual_parameters=[
openapi.Parameter("page", openapi.IN_QUERY, type=openapi.TYPE_INTEGER, required=False),
openapi.Parameter("page_size", openapi.IN_QUERY, type=openapi.TYPE_INTEGER, required=False),
openapi.Parameter("search", openapi.IN_QUERY, type=openapi.TYPE_STRING, required=False),
openapi.Parameter("ordering", openapi.IN_QUERY, type=openapi.TYPE_STRING, required=False,
description="Order by: name, category, framework, created, last_modified, role (prefix with - for desc)"),
],
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)

def perform_create(self, serializer):
instance = serializer.save()
Expand Down
18 changes: 18 additions & 0 deletions estela-api/core/migrations/0040_auto_20260623_0025.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.1.14 on 2026-06-23 00:25

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0039_userprofile'),
]

operations = [
migrations.AlterField(
model_name='deploy',
name='status',
field=models.CharField(choices=[('SUCCESS', 'Success'), ('BUILDING', 'Building'), ('DOWNLOADING', 'Downloading'), ('FAILURE', 'Failure'), ('CANCELED', 'Canceled')], default='BUILDING', help_text='Deploy status.', max_length=12),
),
]
42 changes: 42 additions & 0 deletions estela-api/core/migrations/0041_auto_20260623_0041.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Generated by Django 3.1.14 on 2026-06-23 00:41

from django.db import migrations, models
from django.db.models import Min, Max


def populate_dates(apps, schema_editor):
Project = apps.get_model("core", "Project")
Spider = apps.get_model("core", "Spider")
Deploy = apps.get_model("core", "Deploy")
SpiderJob = apps.get_model("core", "SpiderJob")
for project in Project.objects.all():
spider_ids = Spider.objects.filter(project=project).values_list("sid", flat=True)
first_deploy = Deploy.objects.filter(spiders__in=spider_ids).aggregate(d=Min("created"))["d"]
last_deploy = Deploy.objects.filter(spiders__in=spider_ids).aggregate(d=Max("created"))["d"]
last_job = SpiderJob.objects.filter(spider__in=spider_ids).aggregate(j=Max("created"))["j"]
if project.created is None or (first_deploy and first_deploy < project.created):
project.created = first_deploy
last_dates = [d for d in [last_deploy, last_job] if d is not None]
project.last_modified = max(last_dates) if last_dates else project.created
project.save(update_fields=["created", "last_modified"])


class Migration(migrations.Migration):

dependencies = [
('core', '0040_auto_20260623_0025'),
]

operations = [
migrations.AddField(
model_name='project',
name='created',
field=models.DateTimeField(auto_now_add=True, help_text='Project creation date.', null=True),
),
migrations.AddField(
model_name='project',
name='last_modified',
field=models.DateTimeField(help_text='Date of last activity (deploy or job).', null=True),
),
migrations.RunPython(populate_dates, migrations.RunPython.noop),
]
6 changes: 6 additions & 0 deletions estela-api/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ class Project(models.Model):
deleted = models.BooleanField(
default=False, help_text="Whether the project was deleted."
)
created = models.DateTimeField(
auto_now_add=True, help_text="Project creation date.", null=True
)
last_modified = models.DateTimeField(
null=True, help_text="Date of last activity (deploy or job)."
)

class Meta:
ordering = ["name"]
Expand Down
15 changes: 14 additions & 1 deletion estela-api/core/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,26 @@
from django.db.models.signals import post_save
from django.dispatch import receiver

from django.utils import timezone

from core.cronjob import disable_cronjob
from core.models import Spider, SpiderCronJob, SpiderJob, UserProfile
from core.models import Deploy, Project, Spider, SpiderCronJob, SpiderJob, UserProfile
from core.tasks import get_chain_to_process_usage_data, record_job_coverage_event

logger = logging.getLogger(__name__)


@receiver(post_save, sender=SpiderJob, dispatch_uid="update_project_last_modified_on_job")
def update_project_last_modified_on_job(sender, instance, **kwargs):
Project.objects.filter(pid=instance.spider.project_id).update(last_modified=timezone.now())


@receiver(post_save, sender=Deploy, dispatch_uid="update_project_last_modified_on_deploy")
def update_project_last_modified_on_deploy(sender, instance, **kwargs):
project_ids = instance.spiders.values_list("project_id", flat=True).distinct()
Project.objects.filter(pid__in=project_ids).update(last_modified=timezone.now())


@receiver(post_save, sender=Spider, dispatch_uid="disable_cronjobs_on_spider_delete")
def disable_cronjobs_on_spider_delete(sender, instance, **kwargs):

Expand Down
80 changes: 78 additions & 2 deletions estela-api/docs/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -387,14 +387,22 @@ paths:
parameters:
- name: page
in: query
description: A page number within the paginated result set.
required: false
type: integer
- name: page_size
in: query
description: Number of results to return per page.
required: false
type: integer
- name: search
in: query
required: false
type: string
- name: ordering
in: query
description: 'Order by: name, category, framework, created, last_modified,
role (prefix with - for desc)'
required: false
type: string
responses:
'200':
description: ''
Expand Down Expand Up @@ -698,6 +706,28 @@ paths:
in: path
required: true
type: string
/api/projects/{pid}/deploys/{did}/logs:
get:
operationId: api_projects_deploys_logs
description: ''
parameters: []
responses:
'200':
description: ''
schema:
$ref: '#/definitions/Deploy'
tags:
- api
parameters:
- name: did
in: path
description: A unique integer value identifying this deploy.
required: true
type: integer
- name: pid
in: path
required: true
type: string
/api/projects/{pid}/jobs:
get:
operationId: api_projects_jobs
Expand Down Expand Up @@ -1316,6 +1346,32 @@ paths:
in: path
required: true
type: string
/api/projects/{pid}/spiders/{sid}/jobs/{jid}/error_logs:
get:
operationId: api_projects_spiders_jobs_error_logs
description: ''
parameters: []
responses:
'200':
description: ''
schema:
$ref: '#/definitions/SpiderJob'
tags:
- api
parameters:
- name: jid
in: path
description: A unique integer value identifying this job.
required: true
type: integer
- name: pid
in: path
required: true
type: string
- name: sid
in: path
required: true
type: string
/api/projects/{pid}/usage:
get:
operationId: api_projects_usage
Expand Down Expand Up @@ -1968,6 +2024,14 @@ definitions:
type: integer
maximum: 65535
minimum: 0
created:
title: Created
type: string
readOnly: true
last_modified:
title: Last modified
type: string
readOnly: true
ProjectUpdate:
type: object
properties:
Expand Down Expand Up @@ -2334,6 +2398,12 @@ definitions:
type: string
maxLength: 1000
minLength: 1
error_reason:
title: Error reason
description: Error logs to persist in deploy_logs (Mongo) on failure.
type: string
maxLength: 200000
x-nullable: true
SpiderJob:
description: Project jobs.
type: object
Expand Down Expand Up @@ -2797,6 +2867,12 @@ definitions:
title: Proxy usage data
description: Proxy Usage data.
type: string
error_reason:
title: Error reason
description: Error logs to persist in job_logs (Mongo) on failure.
type: string
maxLength: 200000
x-nullable: true
UsageRecord:
required:
- processing_time
Expand Down
Loading
Loading