40240-vm/core/views.py
Flatlogic Bot 6baa5f4e3d Knb
2026-06-09 23:18:34 +00:00

236 lines
8.3 KiB
Python

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,
},
)