@@ -0,0 +1,16 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import ProfileRevision, SearchProfile
|
||||
|
||||
|
||||
@admin.register(SearchProfile)
|
||||
class SearchProfileAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "user", "is_active", "max_distance_km", "version", "updated_at")
|
||||
list_filter = ("is_active", "locale", "learning_enabled")
|
||||
search_fields = ("name", "user__username", "home_municipality")
|
||||
|
||||
|
||||
@admin.register(ProfileRevision)
|
||||
class ProfileRevisionAdmin(admin.ModelAdmin):
|
||||
list_display = ("profile", "version", "reason", "created_at")
|
||||
readonly_fields = ("profile", "version", "snapshot", "reason", "created_at", "updated_at")
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ProfilesConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.profiles"
|
||||
verbose_name = "Zoekprofielen"
|
||||
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def default_weights() -> dict[str, float]:
|
||||
return {
|
||||
"content": 25.0,
|
||||
"skills": 20.0,
|
||||
"location": 15.0,
|
||||
"conditions": 10.0,
|
||||
"employer": 10.0,
|
||||
"seniority": 10.0,
|
||||
"preferences": 10.0,
|
||||
"ai": 0.0,
|
||||
}
|
||||
|
||||
|
||||
def default_hard_rules() -> dict[str, object]:
|
||||
return {
|
||||
"excluded_title_terms": [],
|
||||
"excluded_employment_types": ["freelance"],
|
||||
"excluded_skills": [],
|
||||
"unknown_is_insufficient": False,
|
||||
}
|
||||
|
||||
|
||||
def default_soft_preferences() -> dict[str, float]:
|
||||
return {
|
||||
"direct_employer": 0.9,
|
||||
"public_sector": 0.7,
|
||||
"limited_first_line_support": 0.8,
|
||||
}
|
||||
|
||||
|
||||
def default_follow_up_weekdays() -> list[int]:
|
||||
return [0, 1, 2, 3, 4]
|
||||
@@ -0,0 +1,80 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django import forms
|
||||
|
||||
from .models import SearchProfile
|
||||
|
||||
|
||||
class SearchProfileForm(forms.ModelForm):
|
||||
desired_titles_text = forms.CharField(
|
||||
label="Gewenste functietitels",
|
||||
required=False,
|
||||
widget=forms.Textarea(attrs={"rows": 4, "placeholder": "Eén titel per regel"}),
|
||||
)
|
||||
excluded_titles_text = forms.CharField(
|
||||
label="Uitgesloten titelwoorden",
|
||||
required=False,
|
||||
widget=forms.Textarea(attrs={"rows": 3, "placeholder": "Eén term per regel"}),
|
||||
)
|
||||
desired_skills_text = forms.CharField(
|
||||
label="Gewenste skills",
|
||||
required=False,
|
||||
widget=forms.Textarea(attrs={"rows": 4, "placeholder": "Eén skill per regel"}),
|
||||
)
|
||||
excluded_skills_text = forms.CharField(
|
||||
label="Uitgesloten skills",
|
||||
required=False,
|
||||
widget=forms.Textarea(attrs={"rows": 3, "placeholder": "Eén skill per regel"}),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SearchProfile
|
||||
fields = [
|
||||
"name",
|
||||
"is_active",
|
||||
"home_postal_code",
|
||||
"home_municipality",
|
||||
"home_latitude",
|
||||
"home_longitude",
|
||||
"max_distance_km",
|
||||
"allowed_employment_types",
|
||||
"preferred_workplace",
|
||||
"preferred_regions",
|
||||
"excluded_regions",
|
||||
"recommendation_threshold",
|
||||
"top_match_threshold",
|
||||
"digest_time",
|
||||
"learning_enabled",
|
||||
]
|
||||
widgets = {
|
||||
"allowed_employment_types": forms.TextInput(
|
||||
attrs={"placeholder": '["full_time", "part_time"]'}
|
||||
),
|
||||
"preferred_workplace": forms.TextInput(attrs={"placeholder": '["hybrid", "on_site"]'}),
|
||||
"preferred_regions": forms.TextInput(attrs={"placeholder": '["Limburg"]'}),
|
||||
"excluded_regions": forms.TextInput(attrs={"placeholder": '["Brussel"]'}),
|
||||
"digest_time": forms.TimeInput(attrs={"type": "time"}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance and self.instance.pk:
|
||||
self.fields["desired_titles_text"].initial = "\n".join(self.instance.desired_titles)
|
||||
self.fields["excluded_titles_text"].initial = "\n".join(self.instance.excluded_titles)
|
||||
self.fields["desired_skills_text"].initial = "\n".join(self.instance.desired_skills)
|
||||
self.fields["excluded_skills_text"].initial = "\n".join(self.instance.excluded_skills)
|
||||
|
||||
@staticmethod
|
||||
def _lines(value: str) -> list[str]:
|
||||
return [line.strip() for line in value.splitlines() if line.strip()]
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save(commit=False)
|
||||
instance.desired_titles = self._lines(self.cleaned_data["desired_titles_text"])
|
||||
instance.excluded_titles = self._lines(self.cleaned_data["excluded_titles_text"])
|
||||
instance.desired_skills = self._lines(self.cleaned_data["desired_skills_text"])
|
||||
instance.excluded_skills = self._lines(self.cleaned_data["excluded_skills_text"])
|
||||
if commit:
|
||||
instance.save()
|
||||
self.save_m2m()
|
||||
return instance
|
||||
@@ -0,0 +1,81 @@
|
||||
# Generated by Django 5.2.16 on 2026-07-20 23:57
|
||||
|
||||
import apps.profiles.defaults
|
||||
import datetime
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SearchProfile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('name', models.CharField(default='Primair profiel', max_length=120)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('locale', models.CharField(default='nl-BE', max_length=10)),
|
||||
('timezone', models.CharField(default='Europe/Brussels', max_length=64)),
|
||||
('home_postal_code', models.CharField(blank=True, max_length=12)),
|
||||
('home_municipality', models.CharField(blank=True, max_length=120)),
|
||||
('home_latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
|
||||
('home_longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)),
|
||||
('max_distance_km', models.PositiveIntegerField(default=45)),
|
||||
('desired_titles', models.JSONField(blank=True, default=list)),
|
||||
('excluded_titles', models.JSONField(blank=True, default=list)),
|
||||
('desired_skills', models.JSONField(blank=True, default=list)),
|
||||
('excluded_skills', models.JSONField(blank=True, default=list)),
|
||||
('allowed_employment_types', models.JSONField(blank=True, default=list)),
|
||||
('preferred_workplace', models.JSONField(blank=True, default=list)),
|
||||
('preferred_regions', models.JSONField(blank=True, default=list)),
|
||||
('excluded_regions', models.JSONField(blank=True, default=list)),
|
||||
('hard_rules', models.JSONField(blank=True, default=apps.profiles.defaults.default_hard_rules)),
|
||||
('soft_preferences', models.JSONField(blank=True, default=apps.profiles.defaults.default_soft_preferences)),
|
||||
('weights', models.JSONField(blank=True, default=apps.profiles.defaults.default_weights)),
|
||||
('recommendation_threshold', models.PositiveSmallIntegerField(default=65)),
|
||||
('top_match_threshold', models.PositiveSmallIntegerField(default=90)),
|
||||
('digest_time', models.TimeField(default=datetime.time(7, 30))),
|
||||
('quiet_hours_start', models.TimeField(default=datetime.time(22, 0))),
|
||||
('quiet_hours_end', models.TimeField(default=datetime.time(7, 0))),
|
||||
('learning_enabled', models.BooleanField(default=True)),
|
||||
('version', models.PositiveIntegerField(default=1)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-is_active', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProfileRevision',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('version', models.PositiveIntegerField()),
|
||||
('snapshot', models.JSONField(default=dict)),
|
||||
('reason', models.CharField(blank=True, max_length=200)),
|
||||
('profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='profiles.searchprofile')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-version'],
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='searchprofile',
|
||||
constraint=models.UniqueConstraint(fields=('user', 'name'), name='unique_profile_name_per_user'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='profilerevision',
|
||||
constraint=models.UniqueConstraint(fields=('profile', 'version'), name='unique_revision_version_per_profile'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("profiles", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="searchprofile",
|
||||
name="ai_scoring_enabled",
|
||||
field=models.BooleanField(default=False),
|
||||
)
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("profiles", "0002_ai_scoring_enabled"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="searchprofile",
|
||||
name="learning_enabled",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 5.2.16 on 2026-07-21 00:00
|
||||
|
||||
import apps.profiles.defaults
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("profiles", "0003_learning_enabled_default_false"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="searchprofile",
|
||||
name="reminders_enabled",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="searchprofile",
|
||||
name="top_match_reminders_enabled",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="searchprofile",
|
||||
name="follow_up_weekdays",
|
||||
field=models.JSONField(default=apps.profiles.defaults.default_follow_up_weekdays, blank=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,157 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import time
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models, transaction
|
||||
|
||||
from apps.core.models import TimeStampedModel
|
||||
|
||||
from .defaults import (
|
||||
default_follow_up_weekdays,
|
||||
default_hard_rules,
|
||||
default_soft_preferences,
|
||||
default_weights,
|
||||
)
|
||||
|
||||
|
||||
class SearchProfile(TimeStampedModel):
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=120, default="Primair profiel")
|
||||
is_active = models.BooleanField(default=True)
|
||||
locale = models.CharField(max_length=10, default="nl-BE")
|
||||
timezone = models.CharField(max_length=64, default="Europe/Brussels")
|
||||
|
||||
home_postal_code = models.CharField(max_length=12, blank=True)
|
||||
home_municipality = models.CharField(max_length=120, blank=True)
|
||||
home_latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||
home_longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||
max_distance_km = models.PositiveIntegerField(default=45)
|
||||
|
||||
desired_titles = models.JSONField(default=list, blank=True)
|
||||
excluded_titles = models.JSONField(default=list, blank=True)
|
||||
desired_skills = models.JSONField(default=list, blank=True)
|
||||
excluded_skills = models.JSONField(default=list, blank=True)
|
||||
allowed_employment_types = models.JSONField(default=list, blank=True)
|
||||
preferred_workplace = models.JSONField(default=list, blank=True)
|
||||
preferred_regions = models.JSONField(default=list, blank=True)
|
||||
excluded_regions = models.JSONField(default=list, blank=True)
|
||||
|
||||
hard_rules = models.JSONField(default=default_hard_rules, blank=True)
|
||||
soft_preferences = models.JSONField(default=default_soft_preferences, blank=True)
|
||||
weights = models.JSONField(default=default_weights, blank=True)
|
||||
recommendation_threshold = models.PositiveSmallIntegerField(default=65)
|
||||
top_match_threshold = models.PositiveSmallIntegerField(default=90)
|
||||
digest_time = models.TimeField(default=time(7, 30))
|
||||
quiet_hours_start = models.TimeField(default=time(22, 0))
|
||||
quiet_hours_end = models.TimeField(default=time(7, 0))
|
||||
reminders_enabled = models.BooleanField(default=True)
|
||||
top_match_reminders_enabled = models.BooleanField(default=False)
|
||||
follow_up_weekdays = models.JSONField(default=default_follow_up_weekdays, blank=True)
|
||||
learning_enabled = models.BooleanField(default=False)
|
||||
ai_scoring_enabled = models.BooleanField(default=False)
|
||||
version = models.PositiveIntegerField(default=1)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-is_active", "name"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=["user", "name"], name="unique_profile_name_per_user")
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def clean(self) -> None:
|
||||
errors: dict[str, str] = {}
|
||||
if self.recommendation_threshold > self.top_match_threshold:
|
||||
errors["top_match_threshold"] = (
|
||||
"De topmatchdrempel moet minstens de aanbevelingsdrempel zijn."
|
||||
)
|
||||
try:
|
||||
weights_valid = (
|
||||
isinstance(self.weights, dict)
|
||||
and bool(self.weights)
|
||||
and all(float(value) >= 0 for value in self.weights.values())
|
||||
and sum(float(value) for value in self.weights.values()) > 0
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
weights_valid = False
|
||||
if not weights_valid:
|
||||
errors["weights"] = "Gewichten moeten numeriek, niet-negatief en samen positief zijn."
|
||||
if self.home_latitude is not None and not (-90 <= float(self.home_latitude) <= 90):
|
||||
errors["home_latitude"] = "Ongeldige breedtegraad."
|
||||
if self.home_longitude is not None and not (-180 <= float(self.home_longitude) <= 180):
|
||||
errors["home_longitude"] = "Ongeldige lengtegraad."
|
||||
if not isinstance(self.follow_up_weekdays, list) or not all(
|
||||
isinstance(day, int) and 0 <= day <= 6 for day in self.follow_up_weekdays
|
||||
):
|
||||
errors["follow_up_weekdays"] = (
|
||||
"Volgweken moet uit geldige weekdagnummers (0-6) bestaan."
|
||||
)
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
def snapshot(self) -> dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"locale": self.locale,
|
||||
"timezone": self.timezone,
|
||||
"home_postal_code": self.home_postal_code,
|
||||
"home_municipality": self.home_municipality,
|
||||
"home_latitude": float(self.home_latitude) if self.home_latitude is not None else None,
|
||||
"home_longitude": float(self.home_longitude)
|
||||
if self.home_longitude is not None
|
||||
else None,
|
||||
"max_distance_km": self.max_distance_km,
|
||||
"desired_titles": self.desired_titles,
|
||||
"excluded_titles": self.excluded_titles,
|
||||
"desired_skills": self.desired_skills,
|
||||
"excluded_skills": self.excluded_skills,
|
||||
"allowed_employment_types": self.allowed_employment_types,
|
||||
"preferred_workplace": self.preferred_workplace,
|
||||
"preferred_regions": self.preferred_regions,
|
||||
"excluded_regions": self.excluded_regions,
|
||||
"hard_rules": self.hard_rules,
|
||||
"soft_preferences": self.soft_preferences,
|
||||
"weights": self.weights,
|
||||
"recommendation_threshold": self.recommendation_threshold,
|
||||
"top_match_threshold": self.top_match_threshold,
|
||||
"reminders_enabled": self.reminders_enabled,
|
||||
"top_match_reminders_enabled": self.top_match_reminders_enabled,
|
||||
"follow_up_weekdays": self.follow_up_weekdays,
|
||||
"digest_time": self.digest_time.isoformat(),
|
||||
"quiet_hours_start": self.quiet_hours_start.isoformat(),
|
||||
"quiet_hours_end": self.quiet_hours_end.isoformat(),
|
||||
"learning_enabled": self.learning_enabled,
|
||||
"ai_scoring_enabled": self.ai_scoring_enabled,
|
||||
"version": self.version,
|
||||
}
|
||||
|
||||
def activate(self) -> None:
|
||||
with transaction.atomic():
|
||||
SearchProfile.objects.filter(user=self.user, is_active=True).exclude(pk=self.pk).update(
|
||||
is_active=False
|
||||
)
|
||||
if not self.is_active:
|
||||
self.is_active = True
|
||||
self.save(update_fields=["is_active", "updated_at"])
|
||||
|
||||
|
||||
class ProfileRevision(TimeStampedModel):
|
||||
profile = models.ForeignKey(SearchProfile, on_delete=models.CASCADE, related_name="revisions")
|
||||
version = models.PositiveIntegerField()
|
||||
snapshot = models.JSONField(default=dict)
|
||||
reason = models.CharField(max_length=200, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-version"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["profile", "version"], name="unique_revision_version_per_profile"
|
||||
)
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.profile} v{self.version}"
|
||||
@@ -0,0 +1,40 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
from .models import ProfileRevision, SearchProfile
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def save_profile_revision(profile: SearchProfile, *, reason: str) -> ProfileRevision:
|
||||
latest = profile.revisions.order_by("-version").first()
|
||||
next_version = (latest.version if latest else 0) + 1
|
||||
if profile.version != next_version:
|
||||
profile.version = next_version
|
||||
profile.save(update_fields=["version", "updated_at"])
|
||||
return ProfileRevision.objects.create(
|
||||
profile=profile,
|
||||
version=next_version,
|
||||
snapshot=profile.snapshot(),
|
||||
reason=reason,
|
||||
)
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def apply_feedback_delta(
|
||||
profile: SearchProfile,
|
||||
feature: str,
|
||||
delta: float,
|
||||
*,
|
||||
min_weight: float = 0.0,
|
||||
max_weight: float = 40.0,
|
||||
) -> SearchProfile:
|
||||
if not profile.learning_enabled:
|
||||
return profile
|
||||
weights = dict(profile.weights)
|
||||
current = float(weights.get(feature, 0.0))
|
||||
weights[feature] = round(max(min_weight, min(max_weight, current + delta)), 2)
|
||||
profile.weights = weights
|
||||
profile.save(update_fields=["weights", "updated_at"])
|
||||
save_profile_revision(profile, reason=f"feedback_delta:{feature}:{delta:+.2f}")
|
||||
return profile
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import ProfileListView, ProfileUpdateView, activate_profile
|
||||
|
||||
urlpatterns = [
|
||||
path("", ProfileListView.as_view(), name="list"),
|
||||
path("<int:pk>/", ProfileUpdateView.as_view(), name="edit"),
|
||||
path("<int:pk>/activate/", activate_profile, name="activate"),
|
||||
]
|
||||
@@ -0,0 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.generic import ListView, UpdateView
|
||||
|
||||
from .forms import SearchProfileForm
|
||||
from .models import SearchProfile
|
||||
from .services import save_profile_revision
|
||||
|
||||
|
||||
class ProfileListView(LoginRequiredMixin, ListView):
|
||||
model = SearchProfile
|
||||
template_name = "profiles/list.html"
|
||||
context_object_name = "profiles"
|
||||
|
||||
def get_queryset(self):
|
||||
return SearchProfile.objects.filter(user=self.request.user)
|
||||
|
||||
|
||||
class ProfileUpdateView(LoginRequiredMixin, UpdateView):
|
||||
model = SearchProfile
|
||||
form_class = SearchProfileForm
|
||||
template_name = "profiles/edit.html"
|
||||
success_url = reverse_lazy("profiles:list")
|
||||
|
||||
def get_queryset(self):
|
||||
return SearchProfile.objects.filter(user=self.request.user)
|
||||
|
||||
def form_valid(self, form):
|
||||
response = super().form_valid(form)
|
||||
if self.object.is_active:
|
||||
self.object.activate()
|
||||
save_profile_revision(self.object, reason="user_edit")
|
||||
messages.success(self.request, "Zoekprofiel opgeslagen.")
|
||||
return response
|
||||
|
||||
|
||||
def activate_profile(request, pk: int):
|
||||
if request.method != "POST":
|
||||
return redirect("profiles:list")
|
||||
profile = get_object_or_404(SearchProfile, pk=pk, user=request.user)
|
||||
profile.activate()
|
||||
messages.success(request, f"{profile.name} is nu het actieve profiel.")
|
||||
return redirect("profiles:list")
|
||||
Reference in New Issue
Block a user