Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
86 changes: 16 additions & 70 deletions website/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,22 @@ def list(self, request, *args, **kwargs):
total_stars=Coalesce(Sum("repos__stars"), Value(0)),
total_forks=Coalesce(Sum("repos__forks"), Value(0)),
)
freshness = request.query_params.get("freshness")

if freshness is not None:
try:
freshness_val = float(freshness)
if not 0 <= freshness_val <= 100:
return Response(
{"error": "Invalid 'freshness' parameter: must be between 0 and 100"},
status=status.HTTP_400_BAD_REQUEST,
)
projects = projects.filter(freshness__gte=freshness_val)
except (ValueError, TypeError):
return Response(
{"error": "Invalid 'freshness' parameter: must be a valid number"},
status=status.HTTP_400_BAD_REQUEST,
)

stars = request.query_params.get("stars")
forks = request.query_params.get("forks")
Expand Down Expand Up @@ -840,76 +856,6 @@ def search(self, request, *args, **kwargs):
status=200,
)

@action(detail=False, methods=["get"])
def filter(self, request, *args, **kwargs):
freshness = request.query_params.get("freshness", None)
stars = request.query_params.get("stars", None)
forks = request.query_params.get("forks", None)
tags = request.query_params.get("tags", None)

# Annotate Project with aggregated stars and forks from related Repos
projects = Project.objects.annotate(
total_stars=Coalesce(Sum("repos__stars"), 0),
total_forks=Coalesce(Sum("repos__forks"), 0),
)

# Freshness is NOT a DB field (SerializerMethodField)
if freshness:
pass # Safe no-op

# SAFE stars validation
if stars:
try:
stars_int = int(stars)
if stars_int < 0:
return Response(
{"error": "Invalid 'stars' parameter: must be non-negative"},
status=status.HTTP_400_BAD_REQUEST,
)
projects = projects.filter(total_stars__gte=stars_int)
except (ValueError, TypeError):
return Response(
{"error": "Invalid 'stars' parameter: must be an integer"},
status=status.HTTP_400_BAD_REQUEST,
)

# SAFE forks validation
if forks:
try:
forks_int = int(forks)
if forks_int < 0:
return Response(
{"error": "Invalid 'forks' parameter: must be non-negative"},
status=status.HTTP_400_BAD_REQUEST,
)
projects = projects.filter(total_forks__gte=forks_int)
except (ValueError, TypeError):
return Response(
{"error": "Invalid 'forks' parameter: must be an integer"},
status=status.HTTP_400_BAD_REQUEST,
)

if tags:
projects = projects.filter(tags__name__in=tags.split(",")).distinct()

project_data = []
for project in projects:
contributors_data = []
for contributor in project.contributors.all():
contributor_info = ContributorSerializer(contributor)
contributors_data.append(contributor_info.data)

contributors_data.sort(key=lambda x: x["contributions"], reverse=True)

project_info = ProjectSerializer(project).data
project_info["contributors"] = contributors_data
project_data.append(project_info)

return Response(
{"count": len(project_data), "projects": project_data},
status=200,
)


class AuthApiViewset(viewsets.ModelViewSet):
http_method_names = ("delete",)
Expand Down
4 changes: 4 additions & 0 deletions website/management/commands/run_daily.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ def handle(self, *args, **options):
call_command("cron_send_reminders")
except Exception as e:
logger.error("Error sending user reminders", exc_info=True)
try:
call_command("update_project_freshness")
except Exception as e:
logger.error("Error updating project freshness", exc_info=True)
except Exception as e:
logger.error("Error in daily tasks", exc_info=True)
raise
51 changes: 51 additions & 0 deletions website/management/commands/update_project_freshness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import time

from django.core.management.base import BaseCommand
from django.db import transaction

from website.models import Project

BATCH_SIZE = 500


class Command(BaseCommand):
help = "Recalculate and update freshness score for all projects"

def handle(self, *args, **options):
start_time = time.time()

qs = Project.objects.only("id")
total = qs.count()

processed = 0
errors = 0

self.stdout.write(f"Starting freshness update for {total} projects")

for offset in range(0, total, BATCH_SIZE):
batch_ids = list(qs.values_list("id", flat=True)[offset : offset + BATCH_SIZE])

for project_id in batch_ids:
try:
with transaction.atomic():
project = Project.objects.select_for_update().get(pk=project_id)

project.freshness = project.calculate_freshness()
project.save(update_fields=["freshness"])
processed += 1

except Exception as e:
errors += 1
self.stderr.write(f"[ERROR] Project ID {project_id}: {str(e)}")

self.stdout.write(
f"Progress: {min(offset + BATCH_SIZE, total)}/{total} attempted "
f"({processed} successful, {errors} errors)"
)

duration = round(time.time() - start_time, 2)

self.stdout.write(self.style.SUCCESS("Freshness update completed"))
self.stdout.write(f"Processed: {processed}")
self.stdout.write(f"Errors: {errors}")
self.stdout.write(f"Execution time: {duration}s")
17 changes: 17 additions & 0 deletions website/migrations/0264_project_freshness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.2.9 on 2025-12-17 12:06

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("website", "0263_githubissue_githubissue_pr_merged_idx_and_more"),
]

operations = [
migrations.AddField(
model_name="project",
name="freshness",
field=models.DecimalField(db_index=True, decimal_places=2, default=0.0, max_digits=5),
),
]
48 changes: 47 additions & 1 deletion website/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from django.core.files.storage import default_storage
from django.core.validators import MaxValueValidator, MinValueValidator, URLValidator
from django.db import models, transaction
from django.db.models import Count, F
from django.db.models import Count, F, Q
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from django.urls import reverse
Expand Down Expand Up @@ -1396,6 +1396,52 @@ class Project(models.Model):
logo = models.ImageField(upload_to="project_logos", null=True, blank=True, max_length=255)
created = models.DateTimeField(auto_now_add=True) # Standardized field name
modified = models.DateTimeField(auto_now=True) # Standardized field name
freshness = models.DecimalField(max_digits=5, decimal_places=2, default=0.0, db_index=True)

def calculate_freshness(self):
"""
Calculate freshness using a Bumper-style activity decay model,
based on GitHub commit recency.
"""
now = timezone.now()

last_7_days = now - timedelta(days=7)
last_30_days = now - timedelta(days=30)
last_90_days = now - timedelta(days=90)

counts = self.repos.filter(
is_archived=False,
last_commit_date__isnull=False,
).aggregate(
active_7=Count(
"id",
filter=Q(last_commit_date__gte=last_7_days),
),
active_30=Count(
"id",
filter=Q(
last_commit_date__lt=last_7_days,
last_commit_date__gte=last_30_days,
),
),
active_90=Count(
"id",
filter=Q(
last_commit_date__lt=last_30_days,
last_commit_date__gte=last_90_days,
),
),
)

raw_score = counts["active_7"] * 1.0 + counts["active_30"] * 0.6 + counts["active_90"] * 0.3

if raw_score == 0:
return 0.0

MAX_SCORE = 20 # ~20 actively maintained repos = fully fresh
freshness = min((raw_score / MAX_SCORE) * 100, 100)

return round(freshness, 2)

def save(self, *args, **kwargs):
# Always ensure a valid slug exists before saving
Expand Down
5 changes: 1 addition & 4 deletions website/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ class Meta:


class ProjectSerializer(serializers.ModelSerializer):
freshness = serializers.SerializerMethodField()
freshness = serializers.DecimalField(max_digits=5, decimal_places=2, read_only=True)

total_stars = serializers.IntegerField(read_only=True)
total_forks = serializers.IntegerField(read_only=True)
Expand All @@ -140,9 +140,6 @@ class Meta:
fields = "__all__"
read_only_fields = ("slug", "contributors")

def get_freshness(self, obj):
return obj.fetch_freshness()


class ContributorSerializer(serializers.ModelSerializer):
class Meta:
Expand Down
Loading
Loading