from __future__ import annotations from typing import Optional from django.contrib.auth import get_user_model from django.db.models import Sum, F, Max, Q, Window from django.db.models.functions import Rank from django.utils import timezone from api.models import BenefitEvent User = get_user_model() # Default weights per action kind BENEFIT_WEIGHTS = { BenefitEvent.KIND_ACCEPTED_ANSWER: 10, BenefitEvent.KIND_REVIEW_HELPFUL: 3, BenefitEvent.KIND_REFERRAL: 20, BenefitEvent.KIND_RECOMMENDATION: 5, } def log_benefit_event( *, benefactor: User, beneficiary: User, kind: str, points: Optional[int] = None, meta: Optional[dict] = None, ) -> BenefitEvent: if points is None: points = BENEFIT_WEIGHTS.get(kind, 1) event = BenefitEvent( benefactor=benefactor, beneficiary=beneficiary, kind=kind, points=points, meta=meta or {}, ) event.save() return event def _period_start(period: str): period = (period or "").lower() if period in ("all", "all_time", "alltime", ""): return None now = timezone.now() if period in ("week", "weekly", "7d"): return now - timezone.timedelta(days=7) if period in ("month", "monthly", "30d"): return now - timezone.timedelta(days=30) # default fallback: weekly return now - timezone.timedelta(days=7) def build_leaderboard_queryset(period: str = "weekly"): start = _period_start(period) pf = Q() if start is not None: pf = Q(benefit_given__created_at__gte=start) qs = ( User.objects .annotate( points_given=Sum('benefit_given__points', filter=pf), last_help_at=Max('benefit_given__created_at', filter=pf), ) ) if start is not None: qs = qs.filter(benefit_given__created_at__gte=start) qs = ( qs.filter(points_given__gt=0) .annotate( rank=Window( expression=Rank(), order_by=( F('points_given').desc(nulls_last=True), F('last_help_at').asc(nulls_last=True), F('id').asc(), ), ) ) .order_by('rank') ) return qs