from django.contrib import messages from django.db.models import Count, Q from django.core.paginator import Paginator from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone from .forms import JobPostingForm, JobSourceForm from .models import JobPosting, JobSource PAGE_META = { "project_name": "Dijon Job Aggregator", "project_description": "Pilot dashboard for collecting, reviewing, and searching job offers around Dijon.", } def _meta(title, description=None): return { **PAGE_META, "page_title": title, "meta_description": description or PAGE_META["project_description"], } def home(request): """Landing dashboard with search, source stats, and latest offers.""" latest_jobs = JobPosting.objects.select_related("source").filter(is_active=True)[:6] sources_by_family = JobSource.objects.values("family").annotate(total=Count("id")).order_by("family") context = { **_meta("Dijon Job Aggregator — Pilot Dashboard"), "total_jobs": JobPosting.objects.count(), "active_jobs": JobPosting.objects.filter(is_active=True).count(), "source_count": JobSource.objects.count(), "sources_by_family": sources_by_family, "latest_jobs": latest_jobs, "today": timezone.localdate(), } return render(request, "core/index.html", context) def source_create(request): if request.method == "POST": form = JobSourceForm(request.POST) if form.is_valid(): source = form.save() messages.success(request, "Source registered. You can now attach job offers to it.") return redirect("source_success", pk=source.pk) else: form = JobSourceForm(initial={"status": JobSource.Status.PLANNED}) return render( request, "core/source_form.html", {**_meta("Register a Source", "Add a job portal, agency, or careers page to the Dijon aggregation pipeline."), "form": form}, ) def source_success(request, pk): source = get_object_or_404(JobSource.objects.annotate(job_count=Count("jobs")), pk=pk) return render( request, "core/source_success.html", {**_meta("Source Registered", "Connector source confirmation for the Dijon Job Aggregator."), "source": source}, ) def job_create(request): if not JobSource.objects.exists(): messages.info(request, "Create your first source before adding a job offer.") return redirect("source_create") if request.method == "POST": form = JobPostingForm(request.POST) if form.is_valid(): job = form.save() messages.success(request, "Job offer added to the review queue.") return redirect("job_success", pk=job.pk) else: form = JobPostingForm() return render( request, "core/job_form.html", {**_meta("Add a Job Offer", "Create a normalized job offer linked to a registered source."), "form": form}, ) def job_success(request, pk): job = get_object_or_404(JobPosting.objects.select_related("source"), pk=pk) return render( request, "core/job_success.html", {**_meta("Offer Added", "Confirmation page for a newly captured Dijon job offer."), "job": job}, ) def job_list(request): query = request.GET.get("q", "").strip() contract = request.GET.get("contract", "").strip() family = request.GET.get("family", "").strip() source_status = request.GET.get("source_status", "").strip() sort = request.GET.get("sort", "newest").strip() job_sort_map = { "newest": "-created_at", "oldest": "created_at", "company_az": "company", "company_za": "-company", } order_by = job_sort_map.get(sort, "-created_at") jobs_qs = JobPosting.objects.select_related("source").filter(is_active=True) if query: jobs_qs = jobs_qs.filter( Q(title__icontains=query) | Q(company__icontains=query) | Q(location__icontains=query) | Q(description__icontains=query) ) if contract: jobs_qs = jobs_qs.filter(contract_type=contract) if family: jobs_qs = jobs_qs.filter(source__family=family) if source_status: jobs_qs = jobs_qs.filter(source__status=source_status) jobs_qs = jobs_qs.order_by(order_by) paginator = Paginator(jobs_qs, 9) page_number = request.GET.get("page") jobs = paginator.get_page(page_number) filters = request.GET.copy() filters.pop("page", None) current_filters_qs = filters.urlencode() context = { **_meta("Search Job Offers", "Search normalized job offers collected for Dijon and nearby opportunities."), "jobs": jobs, "query": query, "contract": contract, "family": family, "source_status": source_status, "sort": sort, "contract_choices": JobPosting.ContractType.choices, "family_choices": JobSource.Family.choices, "source_status_choices": JobSource.Status.choices, "result_count": jobs_qs.count(), "current_filters_qs": current_filters_qs, } return render(request, "core/job_list.html", context) def job_detail(request, pk): job = get_object_or_404(JobPosting.objects.select_related("source"), pk=pk) related_jobs = ( JobPosting.objects.select_related("source") .filter(is_active=True, company__iexact=job.company) .exclude(pk=job.pk)[:3] ) return render( request, "core/job_detail.html", {**_meta(job.title, f"{job.title} at {job.company} — normalized offer from {job.source.name}."), "job": job, "related_jobs": related_jobs}, ) def ops_dashboard(request): query = request.GET.get("q", "").strip() family = request.GET.get("family", "").strip() status = request.GET.get("status", "").strip() sort = request.GET.get("sort", "family_name").strip() source_sort_map = { "family_name": ("family", "name"), "name_az": ("name",), "name_za": ("-name",), "status": ("status", "name"), "offers_high": ("-job_total", "name"), "offers_low": ("job_total", "name"), } source_order_by = source_sort_map.get(sort, ("family", "name")) sources_qs = JobSource.objects.annotate(job_total=Count("jobs")).order_by(*source_order_by) if query: sources_qs = sources_qs.filter(Q(name__icontains=query) | Q(owner__icontains=query) | Q(url__icontains=query)) if family: sources_qs = sources_qs.filter(family=family) if status: sources_qs = sources_qs.filter(status=status) paginator = Paginator(sources_qs, 8) page_number = request.GET.get("page") sources = paginator.get_page(page_number) filters = request.GET.copy() filters.pop("page", None) current_filters_qs = filters.urlencode() recent_jobs = ( JobPosting.objects.select_related("source") .order_by("-created_at")[:8] ) family_breakdown = ( JobSource.objects.values("family") .annotate(total=Count("id"), jobs=Count("jobs")) .order_by("family") ) status_breakdown = ( JobSource.objects.values("status") .annotate(total=Count("id")) .order_by("status") ) needs_attention = sources_qs.filter(status=JobSource.Status.ERROR).count() stale_sources = sources_qs.filter(last_checked_at__isnull=True).count() return render( request, "core/ops_dashboard.html", { **_meta( "Ops dashboard · Dijon Job Aggregator", "Monitor source readiness, intake recency, and connector-family coverage for the Dijon job aggregator pilot.", ), "sources": sources, "recent_jobs": recent_jobs, "family_breakdown": family_breakdown, "status_breakdown": status_breakdown, "needs_attention": needs_attention, "stale_sources": stale_sources, "active_jobs": JobPosting.objects.filter(is_active=True).count(), "total_sources": sources_qs.count(), "query": query, "family": family, "status": status, "sort": sort, "family_choices": JobSource.Family.choices, "status_choices": JobSource.Status.choices, "current_filters_qs": current_filters_qs, }, )