Publish curated VacatureRadar source snapshot
This commit is contained in:
commit
e29774cb7f
338 files changed
+32699
No files matched your search
@@ -0,0 +1,417 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Count, F, Q
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views.generic import DetailView, ListView, TemplateView, UpdateView
|
||||
|
||||
from apps.profiles.models import SearchProfile
|
||||
|
||||
from .forms import ApplicationForm
|
||||
from .models import Application, Employer, Feedback, JobPosting, ScoreRun
|
||||
from .services.activity import ACTIVITY_FILTERS, build_activity_page
|
||||
from .services.applications import (
|
||||
build_application_export,
|
||||
build_print_html,
|
||||
change_application_status,
|
||||
delete_application_dossier,
|
||||
track_application_changes,
|
||||
)
|
||||
from .services.cockpit import annotate_latest_profile_score, build_job_intelligence
|
||||
from .services.employer_intelligence import (
|
||||
build_employer_detail,
|
||||
employer_index_queryset,
|
||||
filter_employers,
|
||||
)
|
||||
from .services.feedback import record_feedback
|
||||
from .services.relevance import it_relevance_query
|
||||
from .services.skill_demand import build_skill_demand_report
|
||||
|
||||
|
||||
class SkillInsightsView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "jobs/skill_insights.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
profile = SearchProfile.objects.filter(user=self.request.user, is_active=True).first()
|
||||
context["active_profile"] = profile
|
||||
context["report"] = (
|
||||
build_skill_demand_report(
|
||||
profile,
|
||||
category=self.request.GET.get("category", "all"),
|
||||
coverage=self.request.GET.get("coverage", "all"),
|
||||
)
|
||||
if profile
|
||||
else None
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class ActivityListView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "activity/list.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
event_type = self.request.GET.get("type", "all")
|
||||
valid_types = {value for value, _label in ACTIVITY_FILTERS}
|
||||
if event_type not in valid_types:
|
||||
event_type = "all"
|
||||
context.update(
|
||||
{
|
||||
"event_page": build_activity_page(
|
||||
user=self.request.user,
|
||||
event_type=event_type,
|
||||
query=self.request.GET.get("q", ""),
|
||||
page=self.request.GET.get("page", 1),
|
||||
),
|
||||
"activity_filters": ACTIVITY_FILTERS,
|
||||
"current_type": event_type,
|
||||
}
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class EmployerListView(LoginRequiredMixin, ListView):
|
||||
model = Employer
|
||||
template_name = "employers/list.html"
|
||||
context_object_name = "employers"
|
||||
paginate_by = 40
|
||||
|
||||
def get_queryset(self):
|
||||
self.profile = SearchProfile.objects.filter(user=self.request.user, is_active=True).first()
|
||||
return filter_employers(
|
||||
employer_index_queryset(user=self.request.user, profile=self.profile),
|
||||
query=self.request.GET.get("q", ""),
|
||||
channel=self.request.GET.get("channel", ""),
|
||||
active_only=self.request.GET.get("active") == "1",
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
employers = list(context["employers"])
|
||||
selected_id = self.request.GET.get("selected", "")
|
||||
selected = next((item for item in employers if str(item.pk) == selected_id), None)
|
||||
if selected is None and employers:
|
||||
selected = employers[0]
|
||||
context.update(
|
||||
{
|
||||
"selected_employer": selected,
|
||||
"selected_detail": build_employer_detail(
|
||||
employer=selected, user=self.request.user, profile=self.profile
|
||||
)
|
||||
if selected
|
||||
else None,
|
||||
"active_profile": self.profile,
|
||||
"employer_total": Employer.objects.count(),
|
||||
"employer_active_total": Employer.objects.filter(
|
||||
jobs__status=JobPosting.Status.ACTIVE
|
||||
)
|
||||
.distinct()
|
||||
.count(),
|
||||
}
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class EmployerDetailView(LoginRequiredMixin, DetailView):
|
||||
model = Employer
|
||||
template_name = "employers/detail.html"
|
||||
context_object_name = "employer"
|
||||
|
||||
def get_queryset(self):
|
||||
self.profile = SearchProfile.objects.filter(user=self.request.user, is_active=True).first()
|
||||
return employer_index_queryset(user=self.request.user, profile=self.profile)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update(
|
||||
build_employer_detail(
|
||||
employer=self.object, user=self.request.user, profile=self.profile
|
||||
)
|
||||
)
|
||||
context["active_profile"] = self.profile
|
||||
return context
|
||||
|
||||
|
||||
class JobListView(LoginRequiredMixin, ListView):
|
||||
model = JobPosting
|
||||
template_name = "jobs/list.html"
|
||||
context_object_name = "jobs"
|
||||
paginate_by = 20
|
||||
|
||||
VALID_RECOMMENDATIONS = {
|
||||
ScoreRun.Recommendation.STRONG,
|
||||
ScoreRun.Recommendation.POSSIBLE,
|
||||
ScoreRun.Recommendation.WEAK,
|
||||
ScoreRun.Recommendation.HIDDEN,
|
||||
}
|
||||
VALID_MATCH_FILTERS = {*VALID_RECOMMENDATIONS, "radar", "all"}
|
||||
VALID_SORTS = {"match", "newest", "closing"}
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = JobPosting.objects.select_related("employer").annotate(
|
||||
duplicate_count=Count("duplicates", distinct=True)
|
||||
)
|
||||
profile = SearchProfile.objects.filter(user=self.request.user, is_active=True).first()
|
||||
queryset = annotate_latest_profile_score(queryset, profile)
|
||||
query = self.request.GET.get("q", "").strip()
|
||||
status = self.request.GET.get("status", "active").strip()
|
||||
workplace = self.request.GET.get("workplace", "").strip()
|
||||
channel = self.request.GET.get("channel", "").strip()
|
||||
focus = self.request.GET.get("focus", "it").strip()
|
||||
recommendation = self.request.GET.get("match", "radar").strip()
|
||||
sort = self.request.GET.get("sort", "match").strip()
|
||||
if recommendation not in self.VALID_MATCH_FILTERS:
|
||||
recommendation = "radar"
|
||||
if query:
|
||||
queryset = queryset.filter(
|
||||
Q(original_title__icontains=query)
|
||||
| Q(employer__name__icontains=query)
|
||||
| Q(description_text__icontains=query)
|
||||
| Q(raw_location__icontains=query)
|
||||
)
|
||||
if status:
|
||||
queryset = queryset.filter(status=status)
|
||||
if workplace:
|
||||
queryset = queryset.filter(workplace_type=workplace)
|
||||
if channel == "direct":
|
||||
queryset = queryset.filter(direct_employer=True, recruiter=False)
|
||||
elif channel == "recruiter":
|
||||
queryset = queryset.filter(recruiter=True)
|
||||
if focus != "all":
|
||||
queryset = queryset.filter(it_relevance_query())
|
||||
if profile and recommendation == "radar":
|
||||
queryset = queryset.filter(
|
||||
Q(
|
||||
match_recommendation__in=[
|
||||
ScoreRun.Recommendation.STRONG,
|
||||
ScoreRun.Recommendation.POSSIBLE,
|
||||
ScoreRun.Recommendation.WEAK,
|
||||
]
|
||||
)
|
||||
| Q(match_recommendation__isnull=True)
|
||||
)
|
||||
elif profile and recommendation in self.VALID_RECOMMENDATIONS:
|
||||
queryset = queryset.filter(match_recommendation=recommendation)
|
||||
if sort not in self.VALID_SORTS:
|
||||
sort = "match"
|
||||
if sort == "newest":
|
||||
return queryset.order_by("-first_seen", F("match_score").desc(nulls_last=True))
|
||||
if sort == "closing":
|
||||
return queryset.order_by(F("valid_through").asc(nulls_last=True), "-first_seen")
|
||||
return queryset.order_by(F("match_score").desc(nulls_last=True), "-first_seen")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
active_jobs = JobPosting.objects.filter(status=JobPosting.Status.ACTIVE)
|
||||
it_jobs = active_jobs.filter(it_relevance_query())
|
||||
context.update(
|
||||
{
|
||||
"active_profile": SearchProfile.objects.filter(
|
||||
user=self.request.user, is_active=True
|
||||
).first(),
|
||||
"active_job_count": active_jobs.count(),
|
||||
"it_job_count": it_jobs.count(),
|
||||
"filtered_non_it_count": active_jobs.exclude(it_relevance_query()).count(),
|
||||
"current_focus": self.request.GET.get("focus", "it"),
|
||||
"current_match": (
|
||||
self.request.GET.get("match", "radar")
|
||||
if self.request.GET.get("match", "radar") in self.VALID_MATCH_FILTERS
|
||||
else "radar"
|
||||
),
|
||||
"current_sort": self.request.GET.get("sort", "match"),
|
||||
"current_channel": self.request.GET.get("channel", ""),
|
||||
}
|
||||
)
|
||||
jobs = list(context["jobs"])
|
||||
selected_id = self.request.GET.get("selected", "")
|
||||
selected_job = next((job for job in jobs if str(job.pk) == selected_id), None)
|
||||
if selected_job is None and jobs:
|
||||
selected_job = jobs[0]
|
||||
context["selected_job"] = selected_job
|
||||
if selected_job is not None:
|
||||
context["selected_intelligence"] = build_job_intelligence(
|
||||
job=selected_job,
|
||||
profile=context["active_profile"],
|
||||
user=self.request.user,
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class JobDetailView(LoginRequiredMixin, DetailView):
|
||||
model = JobPosting
|
||||
template_name = "jobs/detail.html"
|
||||
context_object_name = "job"
|
||||
|
||||
def get_queryset(self):
|
||||
return JobPosting.objects.select_related("employer", "duplicate_of").prefetch_related(
|
||||
"source_aliases__source", "provenance", "versions", "feedback"
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
profile = SearchProfile.objects.filter(user=self.request.user, is_active=True).first()
|
||||
context.update(
|
||||
build_job_intelligence(job=self.object, profile=profile, user=self.request.user)
|
||||
)
|
||||
context["latest_feedback"] = Feedback.objects.filter(
|
||||
user=self.request.user, job=self.object
|
||||
).first()
|
||||
return context
|
||||
|
||||
|
||||
@login_required
|
||||
def job_feedback(request, pk):
|
||||
job = get_object_or_404(JobPosting, pk=pk)
|
||||
if request.method != "POST":
|
||||
return redirect("jobs:detail", pk=pk)
|
||||
action = request.POST.get("action", "")
|
||||
valid_actions = {choice for choice, _ in Feedback.Action.choices}
|
||||
if action not in valid_actions:
|
||||
messages.error(request, "Ongeldige actie.")
|
||||
return redirect("jobs:detail", pk=pk)
|
||||
record_feedback(
|
||||
user=request.user,
|
||||
job=job,
|
||||
action=action,
|
||||
reason=request.POST.get("reason", ""),
|
||||
)
|
||||
messages.success(request, "Actie opgeslagen.")
|
||||
next_url = request.POST.get("next")
|
||||
if next_url and url_has_allowed_host_and_scheme(
|
||||
next_url,
|
||||
allowed_hosts={request.get_host()},
|
||||
require_https=request.is_secure(),
|
||||
):
|
||||
return redirect(next_url)
|
||||
return redirect(reverse("jobs:detail", kwargs={"pk": pk}))
|
||||
|
||||
|
||||
class ApplicationListView(LoginRequiredMixin, ListView):
|
||||
model = Application
|
||||
template_name = "applications/list.html"
|
||||
context_object_name = "applications"
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Application.objects.filter(user=self.request.user)
|
||||
.select_related("job", "job__employer")
|
||||
.prefetch_related("timeline_events")
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
applications = list(context["applications"])
|
||||
context["applications"] = applications
|
||||
context["pipeline_columns"] = [
|
||||
{
|
||||
"status": status,
|
||||
"label": label,
|
||||
"applications": [item for item in applications if item.status == status],
|
||||
}
|
||||
for status, label in Application.Status.choices
|
||||
]
|
||||
context["status_choices"] = Application.Status.choices
|
||||
context["open_count"] = sum(
|
||||
item.status not in {Application.Status.REJECTED, Application.Status.WITHDRAWN}
|
||||
for item in applications
|
||||
)
|
||||
context["follow_up_count"] = sum(
|
||||
bool(item.follow_up_date and item.follow_up_date <= timezone.localdate())
|
||||
for item in applications
|
||||
)
|
||||
context["today"] = timezone.localdate()
|
||||
return context
|
||||
|
||||
|
||||
class ApplicationUpdateView(LoginRequiredMixin, UpdateView):
|
||||
model = Application
|
||||
form_class = ApplicationForm
|
||||
template_name = "applications/edit.html"
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Application.objects.filter(user=self.request.user)
|
||||
.select_related("job", "job__employer")
|
||||
.prefetch_related("timeline_events")
|
||||
)
|
||||
|
||||
def form_valid(self, form):
|
||||
previous = (
|
||||
Application.objects.filter(pk=self.object.pk)
|
||||
.values("status", "notes", "contact_name", "contact_email")
|
||||
.get()
|
||||
)
|
||||
response = super().form_valid(form)
|
||||
track_application_changes(
|
||||
application=self.object,
|
||||
user=self.request.user,
|
||||
previous=previous,
|
||||
current={
|
||||
"status": self.object.status,
|
||||
"notes": self.object.notes,
|
||||
"contact_name": self.object.contact_name,
|
||||
"contact_email": self.object.contact_email,
|
||||
},
|
||||
)
|
||||
return response
|
||||
|
||||
def get_success_url(self):
|
||||
messages.success(self.request, "Sollicitatiedossier opgeslagen.")
|
||||
return reverse("jobs:applications")
|
||||
|
||||
|
||||
@login_required
|
||||
def application_export(request, pk: int):
|
||||
application = get_object_or_404(
|
||||
Application.objects.filter(user=request.user).select_related("job"), pk=pk
|
||||
)
|
||||
zip_payload, filename = build_application_export(application)
|
||||
response = HttpResponse(zip_payload, content_type="application/zip")
|
||||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||
return response
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def application_status(request, pk: int):
|
||||
application = get_object_or_404(Application, pk=pk, user=request.user)
|
||||
try:
|
||||
change_application_status(
|
||||
application=application,
|
||||
user=request.user,
|
||||
status=request.POST.get("status", ""),
|
||||
)
|
||||
except ValueError as exc:
|
||||
messages.error(request, str(exc))
|
||||
else:
|
||||
messages.success(request, "Pipelinestatus bijgewerkt.")
|
||||
return redirect("jobs:applications")
|
||||
|
||||
|
||||
@login_required
|
||||
def application_print(request, pk: int):
|
||||
application = get_object_or_404(
|
||||
Application.objects.filter(user=request.user).select_related("job"), pk=pk
|
||||
)
|
||||
return HttpResponse(build_print_html(application), content_type="text/html; charset=utf-8")
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def application_delete(request, pk: int):
|
||||
if Application.objects.filter(pk=pk).exclude(user=request.user).exists():
|
||||
raise Http404
|
||||
deleted = delete_application_dossier(application_id=pk, user=request.user)
|
||||
if deleted:
|
||||
messages.success(request, "Sollicitatiedossier verwijderd.")
|
||||
else:
|
||||
messages.info(request, "Geen dossier verwijderd.")
|
||||
return redirect("jobs:applications")
|
||||
Reference in new issue
Block a user