236 lines
8.3 KiB
Python
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,
|
|
},
|
|
)
|