diff --git a/back/admin/admin_tasks/forms.py b/back/admin/admin_tasks/forms.py
index cf0b992b3..b6cb12bd0 100644
--- a/back/admin/admin_tasks/forms.py
+++ b/back/admin/admin_tasks/forms.py
@@ -1,7 +1,11 @@
from django import forms
from django.utils.translation import gettext_lazy as _
-from users.models import User
+from users.selectors import (
+ get_all_managers_and_admins_for_departments_of_user,
+ get_all_managers_and_admins_for_departments_of_user_with_slack,
+ get_all_new_hires_for_departments_of_user,
+)
from .models import AdminTask, AdminTaskComment
@@ -15,39 +19,42 @@ class Meta:
class AdminTaskUpdateForm(forms.ModelForm):
- def __init__(self, *args, **kwargs):
+ def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
instance = getattr(self, "instance", None)
if instance is not None and instance.completed:
for field in self.fields:
self.fields[field].widget.attrs["disabled"] = True
+ self.fields[
+ "assigned_to"
+ ].queryset = get_all_managers_and_admins_for_departments_of_user(user=user)
class Meta:
model = AdminTask
fields = ["name", "assigned_to", "date", "priority"]
+ widgets = {
+ "date": forms.DateInput(attrs={"type": "date"}, format=("%Y-%m-%d")),
+ }
class AdminTaskCreateForm(forms.ModelForm):
comment = forms.CharField(label=_("Comment"), max_length=12500)
- new_hire = forms.ModelChoiceField(
- label=_("New hire"), queryset=User.new_hires.all()
- )
- assigned_to = forms.ModelChoiceField(
- label=_("Assigned to"), queryset=User.admins.all()
- )
- slack_user = forms.ModelChoiceField(
- label=_("Slack user"), queryset=User.managers_and_admins.with_slack()
- )
- date = forms.DateField(
- label=_("Date"),
- required=False,
- widget=forms.DateInput(attrs={"type": "date"}, format=("%Y-%m-%d")),
- )
def __init__(self, *args, **kwargs):
+ user = kwargs.pop("user")
super().__init__(*args, **kwargs)
self.fields["option"].initial = 0
- self.fields["slack_user"].required = False
+ self.fields["new_hire"].queryset = get_all_new_hires_for_departments_of_user(
+ user=user
+ )
+ self.fields[
+ "assigned_to"
+ ].queryset = get_all_managers_and_admins_for_departments_of_user(user=user)
+ self.fields[
+ "slack_user"
+ ].queryset = get_all_managers_and_admins_for_departments_of_user_with_slack(
+ user=user
+ )
class Meta:
model = AdminTask
@@ -62,3 +69,6 @@ class Meta:
"slack_user",
"email",
]
+ widgets = {
+ "date": forms.DateInput(attrs={"type": "date"}, format=("%Y-%m-%d")),
+ }
diff --git a/back/admin/admin_tasks/models.py b/back/admin/admin_tasks/models.py
index 9579ef1b4..ab2c4d93a 100644
--- a/back/admin/admin_tasks/models.py
+++ b/back/admin/admin_tasks/models.py
@@ -1,5 +1,6 @@
from django.conf import settings
from django.db import models
+from django.db.models import Q
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
@@ -13,7 +14,23 @@
)
-class AminTaskManager(models.Manager):
+class FilteredForManagerQuerySet(models.QuerySet):
+ def for_user(self, user):
+ if user.is_manager:
+ return self.filter(
+ Q(new_hire__departments__isnull=True)
+ | Q(new_hire__departments__in=user.departments.all())
+ ).discint()
+ return self
+
+
+class AdminTaskManager(models.Manager):
+ def for_user(self, user):
+ return self.get_queryset().for_user(user)
+
+ def get_queryset(self):
+ return FilteredForManagerQuerySet(self.model, using=self._db)
+
def create_admin_task(
self,
new_hire,
@@ -125,7 +142,7 @@ class Notification(models.IntegerChoices):
help_text=_("Only set if generated based on hardware."),
)
- objects = AminTaskManager()
+ objects = AdminTaskManager()
def get_icon_template(self):
return render_to_string("_admin_task_icon.html")
diff --git a/back/admin/admin_tasks/selectors.py b/back/admin/admin_tasks/selectors.py
new file mode 100644
index 000000000..64f4b0d07
--- /dev/null
+++ b/back/admin/admin_tasks/selectors.py
@@ -0,0 +1,10 @@
+from admin.admin_tasks.models import AdminTask
+from users.models import User
+
+
+def get_admin_tasks_for_department(*, user: User):
+ return AdminTask.objects.for_user(user=user)
+
+
+def get_admin_tasks_for_user(*, user: User):
+ return AdminTask.objects.filter(assigned_to=user)
diff --git a/back/admin/admin_tasks/views.py b/back/admin/admin_tasks/views.py
index 4dad157bb..e9be45691 100644
--- a/back/admin/admin_tasks/views.py
+++ b/back/admin/admin_tasks/views.py
@@ -8,6 +8,11 @@
from django.views.generic.edit import CreateView, UpdateView
from django.views.generic.list import ListView
+from admin.admin_tasks.selectors import (
+ get_admin_tasks_for_department,
+ get_admin_tasks_for_user,
+)
+from misc.mixins import FormWithUserContextMixin
from users.mixins import AdminOrManagerPermMixin
from .forms import AdminTaskCommentForm, AdminTaskCreateForm, AdminTaskUpdateForm
@@ -19,7 +24,7 @@ class MyAdminTasksListView(AdminOrManagerPermMixin, ListView):
paginate_by = settings.ADMINTASK_PAGINATE_BY
def get_queryset(self):
- return AdminTask.objects.filter(assigned_to=self.request.user).select_related(
+ return get_admin_tasks_for_user(user=self.request.user).select_related(
"new_hire", "assigned_to"
)
@@ -36,7 +41,9 @@ class AllAdminTasksListView(AdminOrManagerPermMixin, ListView):
paginate_by = settings.ADMINTASK_PAGINATE_BY
def get_queryset(self):
- return AdminTask.objects.all().select_related("new_hire", "assigned_to")
+ return get_admin_tasks_for_department(user=self.request.user).select_related(
+ "new_hire", "assigned_to"
+ )
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -47,7 +54,8 @@ def get_context_data(self, **kwargs):
class AdminTaskCompleteView(AdminOrManagerPermMixin, BaseDetailView):
- model = AdminTask
+ def get_queryset(self):
+ return get_admin_tasks_for_department(user=self.request.user)
def post(self, request, *args, **kwargs):
admin_task = self.get_object()
@@ -55,7 +63,9 @@ def post(self, request, *args, **kwargs):
return redirect("admin_tasks:detail", pk=admin_task.id)
-class AdminTasksCreateView(AdminOrManagerPermMixin, SuccessMessageMixin, CreateView):
+class AdminTasksCreateView(
+ AdminOrManagerPermMixin, FormWithUserContextMixin, SuccessMessageMixin, CreateView
+):
template_name = "admin_tasks_create.html"
form_class = AdminTaskCreateForm
model = AdminTask
@@ -70,7 +80,8 @@ def form_valid(self, form):
comment_by=self.request.user,
)
# Send message to person that got assigned to this
- if self.request.user.id != form.cleaned_data["assigned_to"].id:
+ assigned_to = form.cleaned_data.get("assigned_to", None)
+ if assigned_to and self.request.user.id != assigned_to.id:
self.object.send_notification_new_assigned()
# Send notification based on extra notification option
@@ -90,24 +101,33 @@ class AdminTasksUpdateView(AdminOrManagerPermMixin, SuccessMessageMixin, UpdateV
model = AdminTask
success_message = _("Task has been updated")
+ def get_form_kwargs(self):
+ kwargs = super().get_form_kwargs()
+ kwargs["user"] = self.get_object().new_hire
+ return kwargs
+
+ def get_queryset(self):
+ return get_admin_tasks_for_department(user=self.request.user)
+
def get_success_url(self):
return self.request.path
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- task = get_object_or_404(AdminTask, pk=self.kwargs.get("pk"))
- context["object"] = task
- context["title"] = _("Task: %(name)s") % {"name": task.name}
+ context["title"] = _("Task: %(name)s") % {"name": self.object.name}
context["subtitle"] = _("Tasks")
context["comment_form"] = AdminTaskCommentForm
return context
def form_valid(self, form):
# send email/bot message to newly assigned person
- initial_assigned_to = AdminTask.objects.get(id=form.instance.id).assigned_to
+ task = self.get_object()
+ initial_assigned_to = task.assigned_to
form.save()
+ assigned_to = form.cleaned_data.get("assigned_to", None)
if (
- form.cleaned_data["assigned_to"] != initial_assigned_to
+ assigned_to
+ and form.cleaned_data["assigned_to"] != initial_assigned_to
and form.cleaned_data["assigned_to"] != self.request.user
):
form.instance.send_notification_new_assigned()
@@ -125,11 +145,13 @@ class AdminTasksCommentCreateView(
success_message = _("Comment has been posted")
def get_success_url(self):
- task = get_object_or_404(AdminTask, pk=self.kwargs.get("pk"))
- return reverse("admin_tasks:detail", args=[task.id])
+ return reverse("admin_tasks:detail", args=[self.object.admin_task.id])
def form_valid(self, form):
- task = get_object_or_404(AdminTask, pk=self.kwargs.get("pk"))
+ task = get_object_or_404(
+ get_admin_tasks_for_department(user=self.request.user),
+ pk=self.kwargs.get("pk"),
+ )
# Can't post comments when item is completed
if task.completed:
raise Http404
diff --git a/back/admin/appointments/factories.py b/back/admin/appointments/factories.py
index 56902329b..59866bb0e 100644
--- a/back/admin/appointments/factories.py
+++ b/back/admin/appointments/factories.py
@@ -3,11 +3,15 @@
from pytest_factoryboy import register
from admin.appointments.models import Appointment
+from misc.mixins import DepartmentsPostGenerationMixin
@register
-class AppointmentFactory(factory.django.DjangoModelFactory):
+class AppointmentFactory(
+ factory.django.DjangoModelFactory, DepartmentsPostGenerationMixin
+):
name = FuzzyText()
class Meta:
model = Appointment
+ skip_postgeneration_save = True
diff --git a/back/admin/appointments/forms.py b/back/admin/appointments/forms.py
index a7d51cc0d..8d2aaf5d5 100644
--- a/back/admin/appointments/forms.py
+++ b/back/admin/appointments/forms.py
@@ -4,11 +4,12 @@
from django.utils.translation import gettext_lazy as _
from admin.templates.forms import MultiSelectField, TagModelForm, WYSIWYGField
+from misc.mixins import FilterDepartmentsFieldByUserMixin
from .models import Appointment
-class AppointmentForm(TagModelForm):
+class AppointmentForm(FilterDepartmentsFieldByUserMixin, TagModelForm):
content = WYSIWYGField()
date = forms.DateField(
label=_("Date"),
@@ -27,6 +28,7 @@ def __init__(self, *args, **kwargs):
Div(
Div(
Field("name"),
+ Field("departments"),
MultiSelectField("tags"),
Field("fixed_date"),
Div(
@@ -62,7 +64,16 @@ def clean(self):
class Meta:
model = Appointment
- exclude = ("template",)
+ fields = (
+ "fixed_date",
+ "on_day",
+ "time",
+ "date",
+ "name",
+ "tags",
+ "content",
+ "departments",
+ )
widgets = {
"time": forms.TimeInput(attrs={"type": "time", "step": 300}),
}
diff --git a/back/admin/appointments/migrations/0009_alter_appointment_options_appointment_departments.py b/back/admin/appointments/migrations/0009_alter_appointment_options_appointment_departments.py
new file mode 100644
index 000000000..c379c1e4c
--- /dev/null
+++ b/back/admin/appointments/migrations/0009_alter_appointment_options_appointment_departments.py
@@ -0,0 +1,26 @@
+# Generated by Django 5.2.6 on 2025-09-23 02:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("appointments", "0008_auto_20220221_1338"),
+ ("users", "0043_remove_user_department_user_departments_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="appointment",
+ options={"ordering": ("name",)},
+ ),
+ migrations.AddField(
+ model_name="appointment",
+ name="departments",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="Leave empty to make it available for all managers/admins",
+ to="users.department",
+ ),
+ ),
+ ]
diff --git a/back/admin/appointments/selectors.py b/back/admin/appointments/selectors.py
new file mode 100644
index 000000000..804dcf3e8
--- /dev/null
+++ b/back/admin/appointments/selectors.py
@@ -0,0 +1,6 @@
+from admin.appointments.models import Appointment
+from users.models import User
+
+
+def get_appointment_templates_for_user(*, user: User):
+ return Appointment.templates.for_user(user=user)
diff --git a/back/admin/appointments/views.py b/back/admin/appointments/views.py
index 4d5c1887d..2ed7e6b0c 100644
--- a/back/admin/appointments/views.py
+++ b/back/admin/appointments/views.py
@@ -5,17 +5,20 @@
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.views.generic.list import ListView
+from admin.appointments.selectors import get_appointment_templates_for_user
+from misc.mixins import FormWithUserContextMixin
from users.mixins import AdminOrManagerPermMixin
from .forms import AppointmentForm
-from .models import Appointment
class AppointmentListView(AdminOrManagerPermMixin, ListView):
template_name = "templates.html"
- queryset = Appointment.templates.all().order_by("name")
paginate_by = settings.APPOINTMENT_PAGINATE_BY
+ def get_queryset(self):
+ return get_appointment_templates_for_user(user=self.request.user)
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Appointment items")
@@ -24,7 +27,9 @@ def get_context_data(self, **kwargs):
return context
-class AppointmentCreateView(AdminOrManagerPermMixin, SuccessMessageMixin, CreateView):
+class AppointmentCreateView(
+ AdminOrManagerPermMixin, FormWithUserContextMixin, SuccessMessageMixin, CreateView
+):
template_name = "template_update.html"
form_class = AppointmentForm
success_url = reverse_lazy("appointments:list")
@@ -37,13 +42,17 @@ def get_context_data(self, **kwargs):
return context
-class AppointmentUpdateView(AdminOrManagerPermMixin, SuccessMessageMixin, UpdateView):
+class AppointmentUpdateView(
+ AdminOrManagerPermMixin, FormWithUserContextMixin, SuccessMessageMixin, UpdateView
+):
template_name = "template_update.html"
form_class = AppointmentForm
success_url = reverse_lazy("appointments:list")
- queryset = Appointment.templates.all()
success_message = _("Appointment item has been updated")
+ def get_queryset(self):
+ return get_appointment_templates_for_user(user=self.request.user)
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Update appointment item")
@@ -52,6 +61,8 @@ def get_context_data(self, **kwargs):
class AppointmentDeleteView(AdminOrManagerPermMixin, SuccessMessageMixin, DeleteView):
- queryset = Appointment.objects.all()
success_url = reverse_lazy("appointments:list")
success_message = _("Appointment item has been removed")
+
+ def get_queryset(self):
+ return get_appointment_templates_for_user(user=self.request.user)
diff --git a/back/admin/badges/factories.py b/back/admin/badges/factories.py
index 9d74bebfa..857359df4 100644
--- a/back/admin/badges/factories.py
+++ b/back/admin/badges/factories.py
@@ -3,10 +3,11 @@
from pytest_factoryboy import register
from admin.badges.models import Badge
+from misc.mixins import DepartmentsPostGenerationMixin
@register
-class BadgeFactory(factory.django.DjangoModelFactory):
+class BadgeFactory(factory.django.DjangoModelFactory, DepartmentsPostGenerationMixin):
name = FuzzyText()
content = {
"time": 0,
@@ -18,3 +19,4 @@ class BadgeFactory(factory.django.DjangoModelFactory):
class Meta:
model = Badge
+ skip_postgeneration_save = True
diff --git a/back/admin/badges/forms.py b/back/admin/badges/forms.py
index 8008b2c4b..e1c668c90 100644
--- a/back/admin/badges/forms.py
+++ b/back/admin/badges/forms.py
@@ -7,11 +7,12 @@
UploadField,
WYSIWYGField,
)
+from misc.mixins import FilterDepartmentsFieldByUserMixin
from .models import Badge
-class BadgeForm(TagModelForm):
+class BadgeForm(FilterDepartmentsFieldByUserMixin, TagModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
@@ -20,6 +21,7 @@ def __init__(self, *args, **kwargs):
Div(
Div(
Field("name"),
+ Field("departments"),
MultiSelectField("tags"),
UploadField("image", extra_context={"file": self.instance.image}),
css_class="col-4",
@@ -31,4 +33,4 @@ def __init__(self, *args, **kwargs):
class Meta:
model = Badge
- exclude = ("template",)
+ fields = ("image", "content", "name", "tags", "departments")
diff --git a/back/admin/badges/migrations/0011_alter_badge_options_badge_departments.py b/back/admin/badges/migrations/0011_alter_badge_options_badge_departments.py
new file mode 100644
index 000000000..9cce52d9a
--- /dev/null
+++ b/back/admin/badges/migrations/0011_alter_badge_options_badge_departments.py
@@ -0,0 +1,26 @@
+# Generated by Django 5.2.6 on 2025-09-23 02:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("badges", "0010_auto_20220221_1338"),
+ ("users", "0043_remove_user_department_user_departments_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="badge",
+ options={"ordering": ("name",)},
+ ),
+ migrations.AddField(
+ model_name="badge",
+ name="departments",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="Leave empty to make it available for all managers/admins",
+ to="users.department",
+ ),
+ ),
+ ]
diff --git a/back/admin/badges/selectors.py b/back/admin/badges/selectors.py
new file mode 100644
index 000000000..0f19cfb42
--- /dev/null
+++ b/back/admin/badges/selectors.py
@@ -0,0 +1,6 @@
+from admin.badges.models import Badge
+from users.models import User
+
+
+def get_badge_templates_for_user(*, user: User):
+ return Badge.templates.for_user(user=user)
diff --git a/back/admin/badges/views.py b/back/admin/badges/views.py
index 66ff0cd28..61fbd4d37 100644
--- a/back/admin/badges/views.py
+++ b/back/admin/badges/views.py
@@ -5,17 +5,20 @@
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.views.generic.list import ListView
+from admin.badges.selectors import get_badge_templates_for_user
+from misc.mixins import FormWithUserContextMixin
from users.mixins import AdminOrManagerPermMixin
from .forms import BadgeForm
-from .models import Badge
class BadgeListView(AdminOrManagerPermMixin, ListView):
template_name = "templates.html"
- queryset = Badge.templates.all().order_by("name")
paginate_by = settings.BADGE_PAGINATE_BY
+ def get_queryset(self):
+ return get_badge_templates_for_user(user=self.request.user)
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = "Badge items"
@@ -24,7 +27,9 @@ def get_context_data(self, **kwargs):
return context
-class BadgeCreateView(AdminOrManagerPermMixin, SuccessMessageMixin, CreateView):
+class BadgeCreateView(
+ AdminOrManagerPermMixin, FormWithUserContextMixin, SuccessMessageMixin, CreateView
+):
template_name = "template_update.html"
form_class = BadgeForm
success_url = reverse_lazy("badges:list")
@@ -37,13 +42,17 @@ def get_context_data(self, **kwargs):
return context
-class BadgeUpdateView(AdminOrManagerPermMixin, SuccessMessageMixin, UpdateView):
+class BadgeUpdateView(
+ AdminOrManagerPermMixin, FormWithUserContextMixin, SuccessMessageMixin, UpdateView
+):
template_name = "template_update.html"
form_class = BadgeForm
success_url = reverse_lazy("badges:list")
- queryset = Badge.templates.all()
success_message = _("Badge item has been updated")
+ def get_queryset(self):
+ return get_badge_templates_for_user(user=self.request.user)
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Update badge item")
@@ -52,6 +61,8 @@ def get_context_data(self, **kwargs):
class BadgeDeleteView(AdminOrManagerPermMixin, SuccessMessageMixin, DeleteView):
- queryset = Badge.objects.all()
success_url = reverse_lazy("badges:list")
success_message = _("badge item has been removed")
+
+ def get_queryset(self):
+ return get_badge_templates_for_user(user=self.request.user)
diff --git a/back/admin/hardware/factories.py b/back/admin/hardware/factories.py
index a0d2ec537..18871f551 100644
--- a/back/admin/hardware/factories.py
+++ b/back/admin/hardware/factories.py
@@ -3,10 +3,13 @@
from pytest_factoryboy import register
from admin.hardware.models import Hardware
+from misc.mixins import DepartmentsPostGenerationMixin
@register
-class HardwareFactory(factory.django.DjangoModelFactory):
+class HardwareFactory(
+ factory.django.DjangoModelFactory, DepartmentsPostGenerationMixin
+):
name = FuzzyText()
content = {
"time": 0,
@@ -20,3 +23,4 @@ class HardwareFactory(factory.django.DjangoModelFactory):
class Meta:
model = Hardware
+ skip_postgeneration_save = True
diff --git a/back/admin/hardware/forms.py b/back/admin/hardware/forms.py
index 8cf335ae6..df3516514 100644
--- a/back/admin/hardware/forms.py
+++ b/back/admin/hardware/forms.py
@@ -4,9 +4,10 @@
from admin.hardware.models import Hardware
from admin.templates.forms import MultiSelectField, TagModelForm, WYSIWYGField
+from misc.mixins import FilterDepartmentsFieldByUserMixin
-class HardwareForm(TagModelForm):
+class HardwareForm(FilterDepartmentsFieldByUserMixin, TagModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
@@ -24,6 +25,7 @@ def __init__(self, *args, **kwargs):
Div(
Div(
Field("name"),
+ Field("departments"),
Field("person_type"),
Div(
Field("assigned_to"),
@@ -43,7 +45,14 @@ def __init__(self, *args, **kwargs):
class Meta:
model = Hardware
- exclude = ("template",)
+ fields = (
+ "name",
+ "person_type",
+ "assigned_to",
+ "tags",
+ "content",
+ "departments",
+ )
def clean(self):
cleaned_data = super().clean()
diff --git a/back/admin/hardware/migrations/0002_alter_hardware_options_hardware_departments.py b/back/admin/hardware/migrations/0002_alter_hardware_options_hardware_departments.py
new file mode 100644
index 000000000..af4e4b1f8
--- /dev/null
+++ b/back/admin/hardware/migrations/0002_alter_hardware_options_hardware_departments.py
@@ -0,0 +1,26 @@
+# Generated by Django 5.2.6 on 2025-09-23 02:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("hardware", "0001_initial"),
+ ("users", "0043_remove_user_department_user_departments_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="hardware",
+ options={"ordering": ("name",)},
+ ),
+ migrations.AddField(
+ model_name="hardware",
+ name="departments",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="Leave empty to make it available for all managers/admins",
+ to="users.department",
+ ),
+ ),
+ ]
diff --git a/back/admin/hardware/selectors.py b/back/admin/hardware/selectors.py
new file mode 100644
index 000000000..eec0484e0
--- /dev/null
+++ b/back/admin/hardware/selectors.py
@@ -0,0 +1,6 @@
+from admin.hardware.models import Hardware
+from users.models import User
+
+
+def get_hardware_templates_for_user(*, user: User):
+ return Hardware.templates.for_user(user=user)
diff --git a/back/admin/hardware/views.py b/back/admin/hardware/views.py
index e4af98eac..d622effc1 100644
--- a/back/admin/hardware/views.py
+++ b/back/admin/hardware/views.py
@@ -6,15 +6,18 @@
from django.views.generic.list import ListView
from admin.hardware.forms import HardwareForm
-from admin.hardware.models import Hardware
+from admin.hardware.selectors import get_hardware_templates_for_user
+from misc.mixins import FormWithUserContextMixin
from users.mixins import AdminOrManagerPermMixin
class HardwareListView(AdminOrManagerPermMixin, ListView):
template_name = "templates.html"
- queryset = Hardware.templates.all().order_by("name").defer("content")
paginate_by = settings.HARDWARE_PAGINATE_BY
+ def get_queryset(self):
+ return get_hardware_templates_for_user(user=self.request.user)
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Hardware items")
@@ -23,7 +26,9 @@ def get_context_data(self, **kwargs):
return context
-class HardwareCreateView(AdminOrManagerPermMixin, SuccessMessageMixin, CreateView):
+class HardwareCreateView(
+ AdminOrManagerPermMixin, FormWithUserContextMixin, SuccessMessageMixin, CreateView
+):
template_name = "template_update.html"
form_class = HardwareForm
success_url = reverse_lazy("hardware:list")
@@ -36,13 +41,17 @@ def get_context_data(self, **kwargs):
return context
-class HardwareUpdateView(AdminOrManagerPermMixin, SuccessMessageMixin, UpdateView):
+class HardwareUpdateView(
+ AdminOrManagerPermMixin, FormWithUserContextMixin, SuccessMessageMixin, UpdateView
+):
template_name = "template_update.html"
form_class = HardwareForm
success_url = reverse_lazy("hardware:list")
- queryset = Hardware.templates.all()
success_message = _("Hardware item has been updated")
+ def get_queryset(self):
+ return get_hardware_templates_for_user(user=self.request.user)
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Update hardware item")
@@ -51,6 +60,8 @@ def get_context_data(self, **kwargs):
class HardwareDeleteView(AdminOrManagerPermMixin, SuccessMessageMixin, DeleteView):
- queryset = Hardware.objects.all()
success_url = reverse_lazy("hardware:list")
success_message = _("Hardware item has been removed")
+
+ def get_queryset(self):
+ return get_hardware_templates_for_user(user=self.request.user)
diff --git a/back/admin/integrations/builder_views.py b/back/admin/integrations/builder_views.py
index 62f0a9495..09d932836 100644
--- a/back/admin/integrations/builder_views.py
+++ b/back/admin/integrations/builder_views.py
@@ -48,7 +48,7 @@ def get_success_url(self):
class IntegrationBuilderCreateView(AdminPermMixin, CreateView):
template_name = "token_create.html"
model = Integration
- fields = ["name", "manifest_type"]
+ fields = ["name", "manifest_type", "departments"]
def form_valid(self, form):
form.instance.integration = Integration.Type.CUSTOM
diff --git a/back/admin/integrations/forms.py b/back/admin/integrations/forms.py
index ababe915e..ea79444c7 100644
--- a/back/admin/integrations/forms.py
+++ b/back/admin/integrations/forms.py
@@ -11,6 +11,7 @@
from admin.integrations.models import Integration
from admin.integrations.utils import get_value_from_notation
from admin.sequences.models import IntegrationConfig
+from misc.mixins import FilterDepartmentsFieldByUserMixin
class IntegrationConfigForm(forms.ModelForm):
@@ -172,14 +173,14 @@ def __init__(self, *args, indent, **kwargs):
super().__init__(*args, indent=4, **kwargs)
-class IntegrationForm(forms.ModelForm):
+class IntegrationForm(FilterDepartmentsFieldByUserMixin, forms.ModelForm):
"""Form used to register a new integration through the settings"""
manifest = forms.JSONField(encoder=PrettyJSONEncoder, required=False, initial=dict)
class Meta:
model = Integration
- fields = ("name", "manifest_type", "manifest")
+ fields = ("name", "manifest_type", "departments", "manifest")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
diff --git a/back/admin/integrations/migrations/0027_integration_departments.py b/back/admin/integrations/migrations/0027_integration_departments.py
new file mode 100644
index 000000000..2fadd2bb1
--- /dev/null
+++ b/back/admin/integrations/migrations/0027_integration_departments.py
@@ -0,0 +1,22 @@
+# Generated by Django 5.2.6 on 2025-09-23 02:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("integrations", "0026_alter_integration_integration"),
+ ("users", "0043_remove_user_department_user_departments_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="integration",
+ name="departments",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="Leave empty to make it available to all managers",
+ to="users.department",
+ ),
+ ),
+ ]
diff --git a/back/admin/integrations/models.py b/back/admin/integrations/models.py
index 0e229e211..432cffc6e 100644
--- a/back/admin/integrations/models.py
+++ b/back/admin/integrations/models.py
@@ -42,7 +42,7 @@
from admin.integrations.utils import get_value_from_notation
from misc.fernet_fields import EncryptedTextField
from misc.fields import EncryptedJSONField
-from organization.models import Notification
+from organization.models import FilteredForManagerQuerySet, Notification
from organization.utils import has_manager_or_buddy_tags, send_email_with_notification
@@ -120,21 +120,28 @@ def pretty_post_data(self):
class IntegrationManager(models.Manager):
def get_queryset(self):
- return super().get_queryset()
+ return FilteredForManagerQuerySet(self.model, using=self._db)
- def sequence_integration_options(self):
+ def for_user(self, user):
+ return self.get_queryset().for_user(user)
+
+ def sequence_integration_options(self, user):
# any webhooks and account provisioning
- return self.get_queryset().filter(
- integration=Integration.Type.CUSTOM,
- manifest_type__in=[
- Integration.ManifestType.WEBHOOK,
- Integration.ManifestType.MANUAL_USER_PROVISIONING,
- ],
+ return (
+ self.get_queryset()
+ .for_user(user=user)
+ .filter(
+ integration=Integration.Type.CUSTOM,
+ manifest_type__in=[
+ Integration.ManifestType.WEBHOOK,
+ Integration.ManifestType.MANUAL_USER_PROVISIONING,
+ ],
+ )
)
- def account_provision_options(self):
+ def account_provision_options(self, user):
# only account provisioning (no general webhooks)
- return self.get_queryset().filter(
+ return self.get_queryset().for_user(user=user).filter(
integration=Integration.Type.CUSTOM,
manifest_type=Integration.ManifestType.WEBHOOK,
manifest__exists__isnull=False,
@@ -144,7 +151,7 @@ def account_provision_options(self):
)
def import_users_options(self):
- # only import user items
+ # only import user items - admin function
return (
self.get_queryset()
.filter(
@@ -155,7 +162,7 @@ def import_users_options(self):
)
-class IntegrationInactiveManager(models.Manager):
+class IntegrationInactiveManager(IntegrationManager):
def get_queryset(self):
return super().get_queryset().filter(is_active=False)
@@ -178,6 +185,11 @@ class ManifestType(models.IntegerChoices):
default=True, help_text="If inactive, it's a test/debug integration"
)
integration = models.IntegerField(choices=Type.choices)
+ departments = models.ManyToManyField(
+ "users.Department",
+ blank=True,
+ help_text=_("Leave empty to make it available to all managers"),
+ )
manifest_type = models.IntegerField(
choices=ManifestType.choices, null=True, blank=True
)
diff --git a/back/admin/integrations/views.py b/back/admin/integrations/views.py
index 832bfc033..0d58aadf4 100644
--- a/back/admin/integrations/views.py
+++ b/back/admin/integrations/views.py
@@ -16,13 +16,16 @@
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.views.generic.list import ListView
+from misc.mixins import FormWithUserContextMixin
from users.mixins import AdminOrManagerPermMixin, AdminPermMixin
from .forms import IntegrationExtraArgsForm, IntegrationForm
from .models import Integration, IntegrationTracker
-class IntegrationCreateView(AdminPermMixin, CreateView, SuccessMessageMixin):
+class IntegrationCreateView(
+ AdminPermMixin, FormWithUserContextMixin, CreateView, SuccessMessageMixin
+):
template_name = "token_create.html"
form_class = IntegrationForm
success_message = _("Integration has been created!")
@@ -40,7 +43,9 @@ def form_valid(self, form):
return super().form_valid(form)
-class IntegrationUpdateView(AdminPermMixin, UpdateView, SuccessMessageMixin):
+class IntegrationUpdateView(
+ AdminPermMixin, FormWithUserContextMixin, UpdateView, SuccessMessageMixin
+):
template_name = "token_create.html"
form_class = IntegrationForm
queryset = Integration.objects.filter(integration=Integration.Type.CUSTOM)
diff --git a/back/admin/introductions/factories.py b/back/admin/introductions/factories.py
index 8e6343b00..0d5bf3bec 100644
--- a/back/admin/introductions/factories.py
+++ b/back/admin/introductions/factories.py
@@ -3,13 +3,17 @@
from pytest_factoryboy import register
from admin.introductions.models import Introduction
+from misc.mixins import DepartmentsPostGenerationMixin
from users.factories import EmployeeFactory
@register
-class IntroductionFactory(factory.django.DjangoModelFactory):
+class IntroductionFactory(
+ factory.django.DjangoModelFactory, DepartmentsPostGenerationMixin
+):
name = FuzzyText()
intro_person = factory.SubFactory(EmployeeFactory)
class Meta:
model = Introduction
+ skip_postgeneration_save = True
diff --git a/back/admin/introductions/forms.py b/back/admin/introductions/forms.py
index 4b85d666d..26a61fba1 100644
--- a/back/admin/introductions/forms.py
+++ b/back/admin/introductions/forms.py
@@ -2,19 +2,26 @@
from crispy_forms.layout import Div, Field, Layout
from admin.templates.forms import MultiSelectField, TagModelForm
+from misc.mixins import FilterDepartmentsFieldByUserMixin
+from users.selectors import get_all_users_for_departments_of_user
from .models import Introduction
-class IntroductionForm(TagModelForm):
+class IntroductionForm(FilterDepartmentsFieldByUserMixin, TagModelForm):
def __init__(self, *args, **kwargs):
+ user = kwargs.get("user")
super().__init__(*args, **kwargs)
+ self.fields["intro_person"].queryset = get_all_users_for_departments_of_user(
+ user=user
+ )
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.layout = Layout(
Div(
Div(
Field("name"),
+ Field("departments"),
MultiSelectField("tags"),
Field("intro_person"),
css_class="col-12",
@@ -25,4 +32,4 @@ def __init__(self, *args, **kwargs):
class Meta:
model = Introduction
- exclude = ("template",)
+ fields = ("name", "intro_person", "departments", "tags")
diff --git a/back/admin/introductions/migrations/0006_alter_introduction_options_introduction_departments.py b/back/admin/introductions/migrations/0006_alter_introduction_options_introduction_departments.py
new file mode 100644
index 000000000..c4f3fb09b
--- /dev/null
+++ b/back/admin/introductions/migrations/0006_alter_introduction_options_introduction_departments.py
@@ -0,0 +1,26 @@
+# Generated by Django 5.2.6 on 2025-09-23 02:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("introductions", "0005_auto_20220221_1338"),
+ ("users", "0043_remove_user_department_user_departments_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="introduction",
+ options={"ordering": ("name",)},
+ ),
+ migrations.AddField(
+ model_name="introduction",
+ name="departments",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="Leave empty to make it available for all managers/admins",
+ to="users.department",
+ ),
+ ),
+ ]
diff --git a/back/admin/introductions/selectors.py b/back/admin/introductions/selectors.py
new file mode 100644
index 000000000..c1d472ced
--- /dev/null
+++ b/back/admin/introductions/selectors.py
@@ -0,0 +1,6 @@
+from admin.introductions.models import Introduction
+from users.models import User
+
+
+def get_intro_templates_for_user(*, user: User):
+ return Introduction.templates.for_user(user=user)
diff --git a/back/admin/introductions/templates/intro_update.html b/back/admin/introductions/templates/intro_update.html
index e82005085..6a0806c86 100644
--- a/back/admin/introductions/templates/intro_update.html
+++ b/back/admin/introductions/templates/intro_update.html
@@ -34,7 +34,11 @@
{% translate "Preview" %}
- {% include "_colleague_intro.html" with intro_person=object.intro_person %}
+ {% if not object.intro_person %}
+
{% trans "No colleague selected yet" %}
+ {% else %}
+ {% include "_colleague_intro.html" with intro_person=object.intro_person %}
+ {% endif %}
@@ -46,10 +50,11 @@ {% translate "Preview" %}
{% endblock %}
{% block extra_js %}
-
{% endblock %}
diff --git a/back/admin/introductions/views.py b/back/admin/introductions/views.py
index bd8aa06cd..0997508e9 100644
--- a/back/admin/introductions/views.py
+++ b/back/admin/introductions/views.py
@@ -6,18 +6,21 @@
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.views.generic.list import ListView
+from admin.introductions.selectors import get_intro_templates_for_user
+from misc.mixins import FormWithUserContextMixin
from users.mixins import AdminOrManagerPermMixin
-from users.models import User
+from users.selectors import get_all_users_for_departments_of_user
from .forms import IntroductionForm
-from .models import Introduction
class IntroductionListView(AdminOrManagerPermMixin, ListView):
template_name = "templates.html"
- queryset = Introduction.templates.all().order_by("name")
paginate_by = settings.INTRO_PAGINATE_BY
+ def get_queryset(self):
+ return get_intro_templates_for_user(user=self.request.user)
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Introduction items")
@@ -26,7 +29,9 @@ def get_context_data(self, **kwargs):
return context
-class IntroductionCreateView(AdminOrManagerPermMixin, SuccessMessageMixin, CreateView):
+class IntroductionCreateView(
+ AdminOrManagerPermMixin, FormWithUserContextMixin, SuccessMessageMixin, CreateView
+):
template_name = "intro_update.html"
form_class = IntroductionForm
success_url = reverse_lazy("introductions:list")
@@ -41,7 +46,9 @@ def get_context_data(self, **kwargs):
class IntroductionColleaguePreviewView(AdminOrManagerPermMixin, DetailView):
template_name = "_colleague_intro.html"
- model = User
+
+ def get_queryset(self):
+ return get_all_users_for_departments_of_user(user=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -49,13 +56,17 @@ def get_context_data(self, **kwargs):
return context
-class IntroductionUpdateView(AdminOrManagerPermMixin, SuccessMessageMixin, UpdateView):
+class IntroductionUpdateView(
+ AdminOrManagerPermMixin, FormWithUserContextMixin, SuccessMessageMixin, UpdateView
+):
template_name = "intro_update.html"
form_class = IntroductionForm
success_url = reverse_lazy("introductions:list")
- queryset = Introduction.templates.all()
success_message = _("Introduction item has been updated")
+ def get_queryset(self):
+ return get_intro_templates_for_user(user=self.request.user)
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Update introduction item")
@@ -64,6 +75,8 @@ def get_context_data(self, **kwargs):
class IntroductionDeleteView(AdminOrManagerPermMixin, SuccessMessageMixin, DeleteView):
- queryset = Introduction.objects.all()
success_url = reverse_lazy("introductions:list")
success_message = _("Introduction item has been removed")
+
+ def get_queryset(self):
+ return get_intro_templates_for_user(user=self.request.user)
diff --git a/back/admin/people/access_views.py b/back/admin/people/access_views.py
index f542eaa46..da0f8ec20 100644
--- a/back/admin/people/access_views.py
+++ b/back/admin/people/access_views.py
@@ -11,45 +11,58 @@
from admin.integrations.forms import IntegrationExtraUserInfoForm
from admin.integrations.models import Integration
-from users.mixins import IsAdminOrNewHireManagerMixin
+from admin.people.selectors import get_colleagues_for_user, get_new_hires_for_user
+from users.mixins import AdminOrManagerPermMixin
from users.models import IntegrationUser
-class NewHireAccessView(IsAdminOrNewHireManagerMixin, DetailView):
+class NewHireAccessView(AdminOrManagerPermMixin, DetailView):
template_name = "new_hire_access.html"
- model = get_user_model()
+
+ def get_queryset(self):
+ return get_new_hires_for_user(user=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = self.object.full_name
context["subtitle"] = _("new hire")
context["loading"] = True
- context["integrations"] = Integration.objects.account_provision_options()
+ context["integrations"] = Integration.objects.account_provision_options(
+ user=self.request.user
+ )
return context
-class ColleagueAccessView(IsAdminOrNewHireManagerMixin, DetailView):
+class ColleagueAccessView(AdminOrManagerPermMixin, DetailView):
template_name = "colleague_access.html"
- model = get_user_model()
+
+ def get_queryset(self):
+ return get_colleagues_for_user(user=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = self.object.full_name
context["subtitle"] = _("Employee")
context["loading"] = True
- context["integrations"] = Integration.objects.account_provision_options()
+ context["integrations"] = Integration.objects.account_provision_options(
+ user=self.request.user
+ )
return context
-class UserDeleteView(IsAdminOrNewHireManagerMixin, SuccessMessageMixin, DeleteView):
+class UserDeleteView(AdminOrManagerPermMixin, SuccessMessageMixin, DeleteView):
template_name = "user_delete.html"
- queryset = get_user_model().objects.all()
success_url = reverse_lazy("people:new_hires")
success_message = _("User has been removed")
+ def get_queryset(self):
+ return get_colleagues_for_user(user=self.request.user)
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- provision_options = Integration.objects.account_provision_options()
+ provision_options = Integration.objects.account_provision_options(
+ user=self.request.user
+ )
context["automated_provisioned_items"] = provision_options.exclude(
manifest_type=Integration.ManifestType.MANUAL_USER_PROVISIONING
)
@@ -63,9 +76,11 @@ def form_valid(self, form):
return super().form_valid(form)
-class UserRevokeAllAccessView(IsAdminOrNewHireManagerMixin, SuccessMessageMixin, View):
+class UserRevokeAllAccessView(AdminOrManagerPermMixin, SuccessMessageMixin, View):
def post(self, request, *args, **kwargs):
- user = get_object_or_404(get_user_model(), id=self.kwargs.get("pk", -1))
+ user = get_object_or_404(
+ get_new_hires_for_user(user=self.request.user), id=self.kwargs.get("pk", -1)
+ )
for integration in Integration.objects.filter(
manifest_type=Integration.ManifestType.WEBHOOK,
manifest__revoke__isnull=False,
@@ -80,9 +95,11 @@ def post(self, request, *args, **kwargs):
return redirect("people:delete", user.id)
-class UserCheckAccessView(IsAdminOrNewHireManagerMixin, DetailView):
+class UserCheckAccessView(AdminOrManagerPermMixin, DetailView):
template_name = "_user_access_card.html"
- model = get_user_model()
+
+ def get_queryset(self):
+ return get_colleagues_for_user(user=self.request.user)
def get_template_names(self):
if "compact" in self.request.path:
@@ -102,9 +119,11 @@ def get_context_data(self, **kwargs):
return context
-class UserGiveAccessView(IsAdminOrNewHireManagerMixin, DetailView):
+class UserGiveAccessView(AdminOrManagerPermMixin, DetailView):
template_name = "give_user_access.html"
- model = get_user_model()
+
+ def get_queryset(self):
+ return get_colleagues_for_user(user=self.request.user)
def post(self, request, *args, **kwargs):
integration = get_object_or_404(
@@ -113,7 +132,6 @@ def post(self, request, *args, **kwargs):
user = self.get_object()
integration_config_form = integration.config_form(request.POST)
-
user_details_form = IntegrationExtraUserInfoForm(
data=request.POST,
instance=user,
@@ -167,12 +185,14 @@ def get_context_data(self, **kwargs):
return context
-class UserToggleAccessView(IsAdminOrNewHireManagerMixin, View):
+class UserToggleAccessView(AdminOrManagerPermMixin, View):
def post(self, request, *args, **kwargs):
integration = get_object_or_404(
Integration, id=self.kwargs.get("integration_id", -1)
)
- user = get_object_or_404(get_user_model(), id=self.kwargs.get("pk", -1))
+ user = get_object_or_404(
+ get_new_hires_for_user(user=self.request.user), id=self.kwargs.get("pk", -1)
+ )
# user added/revoked access manually, so just log it being revoked/added
if integration.skip_user_provisioning:
diff --git a/back/admin/people/forms.py b/back/admin/people/forms.py
index 07d93e625..0c7384800 100644
--- a/back/admin/people/forms.py
+++ b/back/admin/people/forms.py
@@ -9,13 +9,13 @@
from admin.integrations.models import Integration
from admin.sequences.models import Sequence
+from admin.sequences.selectors import get_onboarding_sequences_for_user
from admin.templates.forms import (
- ModelChoiceFieldWithCreate,
MultiSelectField,
UploadField,
)
+from misc.mixins import FilterDepartmentsFieldByUserMixin
from organization.models import Organization
-from users.models import Department
class NewHireAddForm(forms.ModelForm):
@@ -39,15 +39,11 @@ class NewHireAddForm(forms.ModelForm):
required=True,
widget=forms.DateInput(attrs={"type": "date"}, format=("%Y-%m-%d")),
)
- department = ModelChoiceFieldWithCreate(
- queryset=Department.objects.all(), to_field_name="name", required=False
- )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["buddy"].required = False
self.fields["manager"].required = False
- self.fields["department"].required = False
self.fields["profile_image"].required = False
self.fields["language"].initial = Organization.object.get().language
self.fields["timezone"].initial = Organization.object.get().timezone
@@ -96,7 +92,7 @@ def __init__(self, *args, **kwargs):
css_class="row",
),
Div(
- Div(Field("department", css_class="add"), css_class="col-6"),
+ Div(Field("departments"), css_class="col-6"),
Div(
UploadField(
"profile_image",
@@ -143,7 +139,7 @@ class Meta:
"language",
"buddy",
"manager",
- "department",
+ "departments",
"profile_image",
)
@@ -164,14 +160,10 @@ class NewHireProfileForm(forms.ModelForm):
required=False,
label=_("Manager"),
)
- department = ModelChoiceFieldWithCreate(
- queryset=Department.objects.all(), to_field_name="name", required=False
- )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
- self.fields["department"].required = False
self.fields["profile_image"].required = False
if self.instance is not None:
# Fallback option: we are now filtering on admins and managers, but people
@@ -206,7 +198,7 @@ def __init__(self, *args, **kwargs):
css_class="row",
),
Div(
- Div(Field("department", css_class="add"), css_class="col-6"),
+ Div(Field("departments"), css_class="col-6"),
Div(
UploadField(
"profile_image",
@@ -242,15 +234,12 @@ class Meta:
"language",
"buddy",
"manager",
- "department",
+ "departments",
"profile_image",
)
-class ColleagueUpdateForm(forms.ModelForm):
- department = ModelChoiceFieldWithCreate(
- queryset=Department.objects.all(), to_field_name="name", required=False
- )
+class ColleagueUpdateForm(FilterDepartmentsFieldByUserMixin, forms.ModelForm):
birthday = forms.DateField(
widget=forms.DateInput(attrs={"type": "date"}, format=("%Y-%m-%d")),
required=False,
@@ -269,7 +258,7 @@ def __init__(self, *args, **kwargs):
Div(
Div(Field("email"), css_class="col-12"),
Div(Field("position"), css_class="col-12"),
- Div(Field("department", css_class="add"), css_class="col-12"),
+ Div(Field("departments"), css_class="col-12"),
Div(Field("phone"), css_class="col-12"),
Div(Field("birthday"), css_class="col-12"),
Div(Field("message"), css_class="col-12"),
@@ -291,7 +280,7 @@ class Meta:
"first_name",
"last_name",
"position",
- "department",
+ "departments",
"birthday",
"email",
"phone",
@@ -305,11 +294,7 @@ class Meta:
)
-class ColleagueCreateForm(forms.ModelForm):
- department = ModelChoiceFieldWithCreate(
- queryset=Department.objects.all(), to_field_name="name", required=False
- )
-
+class ColleagueCreateForm(FilterDepartmentsFieldByUserMixin, forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
@@ -325,7 +310,7 @@ def __init__(self, *args, **kwargs):
Div(
Div(Field("email"), css_class="col-12"),
Div(Field("position"), css_class="col-12"),
- Div(Field("department", css_class="add"), css_class="col-12"),
+ Div(Field("departments"), css_class="col-12"),
Div(Field("phone"), css_class="col-12"),
Div(Field("message"), css_class="col-12"),
Div(Field("facebook"), css_class="col-12"),
@@ -346,7 +331,7 @@ class Meta:
"first_name",
"last_name",
"position",
- "department",
+ "departments",
"email",
"phone",
"message",
@@ -363,9 +348,14 @@ class OnboardingSequenceChoiceForm(forms.Form):
sequences = forms.ModelMultipleChoiceField(
label=_("Select sequences you want to add "),
widget=forms.CheckboxSelectMultiple,
- queryset=Sequence.onboarding.all(),
+ queryset=Sequence.objects.none(),
)
+ def __init__(self, *args, **kwargs):
+ user = kwargs.pop("user")
+ super().__init__(*args, **kwargs)
+ self.fields["sequences"].queryset = get_onboarding_sequences_for_user(user=user)
+
class OffboardingSequenceChoiceForm(forms.ModelForm):
termination_date = forms.DateField(
diff --git a/back/admin/people/new_hire_views.py b/back/admin/people/new_hire_views.py
index 2519df8ab..cf7384f76 100644
--- a/back/admin/people/new_hire_views.py
+++ b/back/admin/people/new_hire_views.py
@@ -16,10 +16,14 @@
from twilio.rest import Client
from admin.admin_tasks.models import AdminTask
+from admin.admin_tasks.selectors import get_admin_tasks_for_user
from admin.integrations.forms import IntegrationExtraUserInfoForm
from admin.notes.models import Note
+from admin.people.selectors import get_colleagues_for_user, get_new_hires_for_user
from admin.sequences.models import Condition, Sequence
+from admin.sequences.selectors import get_sequences_for_user
from admin.templates.utils import get_templates_model, get_user_field
+from misc.mixins import FormWithUserContextMixin
from organization.models import Notification, Organization, WelcomeMessage
from slack_bot.slack_resource import SlackResource
from slack_bot.slack_to_do import SlackToDo
@@ -31,10 +35,7 @@
send_new_hire_preboarding,
send_reminder_email,
)
-from users.mixins import (
- AdminOrManagerPermMixin,
- IsAdminOrNewHireManagerMixin,
-)
+from users.mixins import AdminOrManagerPermMixin
from users.models import NewHireWelcomeMessage, PreboardingUser, ResourceUser, ToDoUser
from .forms import (
@@ -51,10 +52,7 @@ class NewHireListView(AdminOrManagerPermMixin, ListView):
paginate_by = settings.NEW_HIRE_PAGINATE_BY
def get_queryset(self):
- all_new_hires = get_user_model().new_hires.all().order_by("-start_day")
- if self.request.user.is_admin:
- return all_new_hires
- return all_new_hires.filter(manager=self.request.user)
+ return get_new_hires_for_user(user=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -150,12 +148,14 @@ def form_valid(self, form):
return super().form_valid(form)
-class NewHireSendPreboardingNotificationView(IsAdminOrNewHireManagerMixin, FormView):
+class NewHireSendPreboardingNotificationView(AdminOrManagerPermMixin, FormView):
template_name = "trigger_preboarding_notification.html"
form_class = PreboardingSendForm
def form_valid(self, form):
- new_hire = get_object_or_404(get_user_model(), id=self.kwargs.get("pk", -1))
+ new_hire = get_object_or_404(
+ get_new_hires_for_user(user=self.request.user), id=self.kwargs.get("pk", -1)
+ )
if form.cleaned_data["send_type"] == "email":
send_new_hire_preboarding(new_hire, form.cleaned_data["email"])
else:
@@ -175,19 +175,25 @@ def form_valid(self, form):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user_id = self.kwargs.get("pk", -1)
- new_hire = get_object_or_404(get_user_model(), id=user_id)
+ new_hire = get_object_or_404(
+ get_new_hires_for_user(user=self.request.user), id=user_id
+ )
context["title"] = new_hire.full_name
context["subtitle"] = "new hire"
return context
-class NewHireAddSequenceView(IsAdminOrNewHireManagerMixin, FormView):
+class NewHireAddSequenceView(
+ AdminOrManagerPermMixin, FormWithUserContextMixin, FormView
+):
template_name = "new_hire_add_sequence.html"
form_class = OnboardingSequenceChoiceForm
def form_valid(self, form):
user_id = self.kwargs.get("pk", -1)
- new_hire = get_object_or_404(get_user_model(), id=user_id)
+ new_hire = get_object_or_404(
+ get_new_hires_for_user(user=self.request.user), id=user_id
+ )
sequences = Sequence.objects.filter(id__in=form.cleaned_data["sequences"])
new_hire.add_sequences(sequences)
messages.success(
@@ -233,16 +239,22 @@ def form_valid(self, form):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user_id = self.kwargs.get("pk", -1)
- new_hire = get_object_or_404(get_user_model(), id=user_id)
+ new_hire = get_object_or_404(
+ get_new_hires_for_user(user=self.request.user), id=user_id
+ )
context["title"] = new_hire.full_name
context["subtitle"] = "new hire"
return context
-class NewHireRemoveSequenceView(IsAdminOrNewHireManagerMixin, View):
+class NewHireRemoveSequenceView(AdminOrManagerPermMixin, View):
def post(self, request, pk, sequence_pk, *args, **kwargs):
- sequence = get_object_or_404(Sequence, id=sequence_pk)
- new_hire = get_object_or_404(get_user_model(), id=pk)
+ sequence = get_object_or_404(
+ get_sequences_for_user(user=self.request.user), id=sequence_pk
+ )
+ new_hire = get_object_or_404(
+ get_new_hires_for_user(user=self.request.user), id=pk
+ )
new_hire.remove_sequence(sequence)
# Update user amount and completed
@@ -253,12 +265,16 @@ def post(self, request, pk, sequence_pk, *args, **kwargs):
return redirect("people:new_hire", pk=new_hire.id)
-class NewHireTriggerConditionView(IsAdminOrNewHireManagerMixin, TemplateView):
+class NewHireTriggerConditionView(AdminOrManagerPermMixin, TemplateView):
template_name = "_trigger_sequence_items.html"
def post(self, request, pk, condition_pk, *args, **kwargs):
condition = get_object_or_404(Condition, id=condition_pk)
- new_hire = get_object_or_404(get_user_model(), id=pk)
+ if condition.sequence not in get_sequences_for_user(user=self.request.user):
+ raise Http404
+ new_hire = get_object_or_404(
+ get_colleagues_for_user(user=self.request.user), id=pk
+ )
condition.process_condition(new_hire, skip_notification=True)
# Update user amount completed
@@ -271,6 +287,8 @@ def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
condition_id = self.kwargs.get("condition_pk", -1)
condition = get_object_or_404(Condition, id=condition_id)
+ if condition.sequence not in get_sequences_for_user(user=self.request.user):
+ raise Http404
context["completed"] = True
context["condition"] = condition
# not relevant, still needed for processing the template
@@ -280,17 +298,21 @@ def get_context_data(self, **kwargs):
return context
-class NewHireSendLoginEmailView(IsAdminOrNewHireManagerMixin, View):
+class NewHireSendLoginEmailView(AdminOrManagerPermMixin, View):
def post(self, request, pk, *args, **kwargs):
- new_hire = get_object_or_404(get_user_model(), id=pk)
+ new_hire = get_object_or_404(
+ get_new_hires_for_user(user=self.request.user), id=pk
+ )
send_new_hire_credentials(new_hire.id)
messages.success(request, _("Sent email to new hire"))
return redirect("people:new_hire", pk=new_hire.id)
-class NewHireSequenceView(IsAdminOrNewHireManagerMixin, DetailView):
+class NewHireSequenceView(AdminOrManagerPermMixin, DetailView):
template_name = "new_hire_detail.html"
- model = get_user_model()
+
+ def get_queryset(self):
+ return get_new_hires_for_user(user=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -329,12 +351,14 @@ def get_context_data(self, **kwargs):
return context
-class NewHireProfileView(SuccessMessageMixin, IsAdminOrNewHireManagerMixin, UpdateView):
+class NewHireProfileView(SuccessMessageMixin, AdminOrManagerPermMixin, UpdateView):
template_name = "new_hire_profile.html"
- model = get_user_model()
form_class = NewHireProfileForm
success_message = _("New hire has been updated")
+ def get_queryset(self):
+ return get_new_hires_for_user(user=self.request.user)
+
def get_success_url(self):
return self.request.path
@@ -346,11 +370,9 @@ def get_context_data(self, **kwargs):
return context
-class NewHireMigrateToNormalAccountView(IsAdminOrNewHireManagerMixin, View):
+class NewHireMigrateToNormalAccountView(AdminOrManagerPermMixin, View):
def post(self, request, pk, *args, **kwargs):
- user = get_object_or_404(
- get_user_model(), id=pk, role=get_user_model().Role.NEWHIRE
- )
+ user = get_object_or_404(get_new_hires_for_user(user=request.user), id=pk)
user.role = 3
user.save()
messages.info(request, _("New hire is now a normal account."))
@@ -359,14 +381,16 @@ def post(self, request, pk, *args, **kwargs):
class NewHireExtraInfoUpdateView(
UpdateView,
- IsAdminOrNewHireManagerMixin,
+ AdminOrManagerPermMixin,
SuccessMessageMixin,
):
template_name = "token_create.html"
form_class = IntegrationExtraUserInfoForm
- queryset = get_user_model().new_hires.all()
success_message = _("Extra info has been added!")
+ def get_queryset(self):
+ return get_new_hires_for_user(user=self.request.user)
+
def get_success_url(self):
user = get_object_or_404(get_user_model(), pk=self.kwargs.get("pk"))
return reverse("people:new_hire", args=[user.id])
@@ -381,7 +405,7 @@ def get_context_data(self, **kwargs):
class NewHireNotesView(
- IsAdminOrNewHireManagerMixin,
+ AdminOrManagerPermMixin,
SuccessMessageMixin,
CreateView,
):
@@ -396,14 +420,18 @@ def get_success_url(self):
return self.request.path
def form_valid(self, form):
- new_hire = get_object_or_404(get_user_model(), pk=self.kwargs.get("pk"))
+ new_hire = get_object_or_404(
+ get_new_hires_for_user(user=self.request.user), pk=self.kwargs.get("pk")
+ )
form.instance.admin = self.request.user
form.instance.new_hire = new_hire
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- new_hire = get_object_or_404(get_user_model(), pk=self.kwargs.get("pk"))
+ new_hire = get_object_or_404(
+ get_new_hires_for_user(user=self.request.user), pk=self.kwargs.get("pk")
+ )
context["object"] = new_hire
context["title"] = new_hire.full_name
context["subtitle"] = _("new hire")
@@ -415,11 +443,13 @@ def get_context_data(self, **kwargs):
return context
-class NewHireWelcomeMessagesView(IsAdminOrNewHireManagerMixin, ListView):
+class NewHireWelcomeMessagesView(AdminOrManagerPermMixin, ListView):
template_name = "new_hire_welcome_messages.html"
def get_queryset(self):
- new_hire = get_object_or_404(get_user_model(), pk=self.kwargs.get("pk"))
+ new_hire = get_object_or_404(
+ get_new_hires_for_user(user=self.request.user), pk=self.kwargs.get("pk")
+ )
return (
NewHireWelcomeMessage.objects.filter(new_hire=new_hire)
.order_by("-id")
@@ -428,19 +458,23 @@ def get_queryset(self):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- new_hire = get_object_or_404(get_user_model(), pk=self.kwargs.get("pk"))
+ new_hire = get_object_or_404(
+ get_new_hires_for_user(user=self.request.user), pk=self.kwargs.get("pk")
+ )
context["object"] = new_hire
context["title"] = new_hire.full_name
context["subtitle"] = _("new hire")
return context
-class NewHireAdminTasksView(IsAdminOrNewHireManagerMixin, TemplateView):
+class NewHireAdminTasksView(AdminOrManagerPermMixin, TemplateView):
template_name = "new_hire_admin_tasks.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- new_hire = get_object_or_404(get_user_model(), pk=self.kwargs.get("pk"))
+ new_hire = get_object_or_404(
+ get_new_hires_for_user(user=self.request.user), pk=self.kwargs.get("pk")
+ )
context["object"] = new_hire
context["title"] = new_hire.full_name
context["subtitle"] = _("new hire")
@@ -453,9 +487,11 @@ def get_context_data(self, **kwargs):
return context
-class NewHireFormsView(IsAdminOrNewHireManagerMixin, DetailView):
+class NewHireFormsView(AdminOrManagerPermMixin, DetailView):
template_name = "new_hire_forms.html"
- model = get_user_model()
+
+ def get_queryset(self):
+ return get_new_hires_for_user(user=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -476,9 +512,11 @@ def get_context_data(self, **kwargs):
return context
-class NewHireProgressView(IsAdminOrNewHireManagerMixin, DetailView):
+class NewHireProgressView(AdminOrManagerPermMixin, DetailView):
template_name = "new_hire_progress.html"
- model = get_user_model()
+
+ def get_queryset(self):
+ return get_new_hires_for_user(user=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -496,8 +534,11 @@ def get_context_data(self, **kwargs):
return context
-class NewHireRemindView(IsAdminOrNewHireManagerMixin, View):
+class NewHireRemindView(AdminOrManagerPermMixin, View):
def post(self, request, pk, template_type, template_pk, *args, **kwargs):
+ get_object_or_404(
+ get_new_hires_for_user(user=self.request.user), pk=self.kwargs.get("pk")
+ )
if template_type not in ["todouser", "resourceuser"]:
raise Http404
@@ -530,11 +571,14 @@ def post(self, request, pk, template_type, template_pk, *args, **kwargs):
return redirect("people:new_hire_progress", pk=template_user_obj.user.id)
-class NewHireReopenTaskView(IsAdminOrNewHireManagerMixin, FormView):
+class NewHireReopenTaskView(AdminOrManagerPermMixin, FormView):
template_name = "new_hire_reopen_task.html"
form_class = RemindMessageForm
def dispatch(self, *args, **kwargs):
+ get_object_or_404(
+ get_new_hires_for_user(user=self.request.user), pk=self.kwargs.get("pk")
+ )
if self.request.user.is_authenticated:
template_type = self.kwargs.get("template_type", "")
if template_type not in ["todouser", "resourceuser"]:
@@ -590,9 +634,11 @@ def form_valid(self, form):
return redirect("people:new_hire_progress", pk=template_user_obj.user.id)
-class NewHireCourseAnswersView(IsAdminOrNewHireManagerMixin, DetailView):
+class NewHireCourseAnswersView(AdminOrManagerPermMixin, DetailView):
template_name = "new_hire_course_answers.html"
- model = get_user_model()
+
+ def get_queryset(self):
+ return get_new_hires_for_user(user=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -605,9 +651,11 @@ def get_context_data(self, **kwargs):
return context
-class NewHireTasksView(IsAdminOrNewHireManagerMixin, DetailView):
+class NewHireTasksView(AdminOrManagerPermMixin, DetailView):
template_name = "new_hire_tasks.html"
- model = get_user_model()
+
+ def get_queryset(self):
+ return get_new_hires_for_user(user=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -616,9 +664,11 @@ def get_context_data(self, **kwargs):
return context
-class NewHireTaskListView(IsAdminOrNewHireManagerMixin, DetailView):
+class NewHireTaskListView(AdminOrManagerPermMixin, DetailView):
template_name = "new_hire_add_task.html"
- model = get_user_model()
+
+ def get_queryset(self):
+ return get_new_hires_for_user(user=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -630,18 +680,18 @@ def get_context_data(self, **kwargs):
"name": self.object.full_name
}
context["subtitle"] = _("new hire")
- context["object_list"] = templates_model.templates.defer_content().all()
+ context["object_list"] = templates_model.templates.all()
context["user_items"] = getattr(
self.object, get_user_field(self.kwargs.get("type", ""))
).values_list("id", flat=True)
return context
-class NewHireToggleTaskView(IsAdminOrNewHireManagerMixin, TemplateView):
+class NewHireToggleTaskView(AdminOrManagerPermMixin, TemplateView):
template_name = "_toggle_button_new_hire_template.html"
def post(self, request, pk, template_id, type):
- user = get_object_or_404(get_user_model(), id=pk)
+ user = get_object_or_404(get_new_hires_for_user(user=self.request.user), id=pk)
templates_model = get_templates_model(type)
if templates_model is None:
@@ -670,16 +720,21 @@ def post(self, request, pk, template_id, type):
return self.render_to_response(context)
-class NewHireDeleteView(IsAdminOrNewHireManagerMixin, SuccessMessageMixin, DeleteView):
+class NewHireDeleteView(AdminOrManagerPermMixin, SuccessMessageMixin, DeleteView):
template_name = "new_hire_delete.html"
- queryset = get_user_model().new_hires.all()
success_url = reverse_lazy("people:new_hires")
success_message = _("New hire has been removed")
+ def get_queryset(self):
+ return get_new_hires_for_user(user=self.request.user)
+
-class CompleteAdminTaskView(IsAdminOrNewHireManagerMixin, DetailView):
+class CompleteAdminTaskView(AdminOrManagerPermMixin, DetailView):
def post(self, request, pk, admin_task_pk, *args, **kwargs):
- task = get_object_or_404(AdminTask, id=admin_task_pk)
+ get_object_or_404(get_new_hires_for_user(user=self.request.user), id=pk)
+ task = get_object_or_404(
+ get_admin_tasks_for_user(user=self.request.user), id=admin_task_pk
+ )
task.mark_completed()
messages.success(request, _("The admin task was successfully completed"))
diff --git a/back/admin/people/selectors.py b/back/admin/people/selectors.py
new file mode 100644
index 000000000..d6054a838
--- /dev/null
+++ b/back/admin/people/selectors.py
@@ -0,0 +1,33 @@
+from django.db.models import Q
+
+from users.models import User
+
+
+def get_new_hires_for_user(*, user: User):
+ if user.is_admin:
+ return User.new_hires.all()
+ elif user.is_manager:
+ return User.new_hires.filter(
+ Q(departments__isnull=True) | Q(departments__in=user.departments.all())
+ ).distinct()
+ return user.objects.none()
+
+
+def get_colleagues_for_user(*, user: User):
+ if user.is_admin:
+ return User.objects.all()
+ elif user.is_manager:
+ return User.objects.filter(
+ Q(departments__isnull=True) | Q(departments__in=user.departments.all())
+ ).distinct()
+ return User.objects.none()
+
+
+def get_offboarding_colleagues_for_user(*, user: User):
+ if user.is_admin:
+ return User.offboarding.all()
+ elif user.is_manager:
+ return User.offboarding.filter(
+ Q(departments__isnull=True) | Q(departments__in=user.departments.all())
+ ).distinct()
+ return User.objects.none()
diff --git a/back/admin/people/templates/colleagues.html b/back/admin/people/templates/colleagues.html
index c17e693d7..8e19d313c 100644
--- a/back/admin/people/templates/colleagues.html
+++ b/back/admin/people/templates/colleagues.html
@@ -33,6 +33,7 @@
| {% translate "Name" %} |
{% translate "Position" %} |
+ {% translate "Departments" %} |
|
@@ -45,6 +46,13 @@
{{ colleague.position }}
|
+
+ {% for department in colleague.departments.all %}
+ {{ department }}
+ {% empty %}
+ {% trans "Not in a department" %}
+ {% endfor %}
+ |
{% if slack_active %}
{% include "_toggle_colleague_access.html" with url_name="people:connect-to-slack" slack=True exists=colleague.has_slack_account %}
diff --git a/back/admin/people/templates/department_create.html b/back/admin/people/templates/department_create.html
new file mode 100644
index 000000000..9fc2113f1
--- /dev/null
+++ b/back/admin/people/templates/department_create.html
@@ -0,0 +1,23 @@
+{% extends 'admin_base.html' %}
+{% load i18n %}
+{% load crispy_forms_tags %}
+
+{% block content %}
+
+{% endblock %}
+
diff --git a/back/admin/people/templates/departments.html b/back/admin/people/templates/departments.html
new file mode 100644
index 000000000..41e73db04
--- /dev/null
+++ b/back/admin/people/templates/departments.html
@@ -0,0 +1,40 @@
+{% extends 'admin_base.html' %}
+{% load i18n %}
+
+{% block actions %}
+
+ {% trans "Add" %}
+
+{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+ | {% translate "Name" %} |
+
+
+
+ {% for department in object_list %}
+
+ |
+ {{ department.name }}
+ |
+
+ {% empty %}
+
+ |
+ {% trans "You haven't created any departments yet" %}
+ |
+
+ {% endfor %}
+
+
+
+ {% include "_paginator.html" %}
+
+
+{% endblock %}
diff --git a/back/admin/people/templates/new_hires.html b/back/admin/people/templates/new_hires.html
index 50cbeef34..931a961cc 100644
--- a/back/admin/people/templates/new_hires.html
+++ b/back/admin/people/templates/new_hires.html
@@ -11,6 +11,7 @@
| {% translate "Name" %} |
{% translate "Start date" %} |
{% translate "Position" %} |
+ {% translate "Departments" %} |
{% translate "Progress" %} |
|
@@ -28,7 +29,14 @@
{{ new_hire.position }}
-
+ {% for department in new_hire.departments.all %}
+ {{ department.name }}
+ {% empty %}
+ Not in any department
+ {% endfor %}
+ |
+
+
diff --git a/back/admin/people/tests.py b/back/admin/people/tests.py
index 6759f079a..deac9e53b 100644
--- a/back/admin/people/tests.py
+++ b/back/admin/people/tests.py
@@ -1286,9 +1286,15 @@ def test_new_hire_course_answers_list(
@pytest.mark.django_db
def test_new_hire_reopen_todo(
- client, settings, manager_factory, to_do_user_factory, mailoutbox
+ client,
+ settings,
+ manager_factory,
+ to_do_user_factory,
+ department_factory,
+ mailoutbox,
):
manager1 = manager_factory()
+ manager2 = manager_factory(departments=[department_factory()])
client.force_login(manager1)
to_do_user1 = to_do_user_factory()
@@ -1300,16 +1306,14 @@ def test_new_hire_reopen_todo(
response = client.get(url, follow=True)
assert response.status_code == 404
- # not a valid user (admin or manager of new hire)
+ # not a valid user (different department)
url = reverse(
- "people:new_hire_reopen", args=[to_do_user1.user.id, "todouser", to_do_user1.id]
+ "people:new_hire_reopen", args=[manager2.id, "todouser", to_do_user1.id]
)
response = client.get(url, follow=True)
- assert response.status_code == 403
-
- to_do_user1.user.manager = manager1
- to_do_user1.user.save()
+ assert response.status_code == 404
+ # now use the normal user
url = reverse(
"people:new_hire_reopen", args=[to_do_user1.user.id, "todouser", to_do_user1.id]
)
diff --git a/back/admin/people/urls.py b/back/admin/people/urls.py
index 830aba164..bb5d1866b 100644
--- a/back/admin/people/urls.py
+++ b/back/admin/people/urls.py
@@ -228,4 +228,14 @@
views.ColleagueImportAddUsersView.as_view(),
name="import-create",
),
+ path(
+ "colleagues/departments/",
+ views.DepartmentListView.as_view(),
+ name="departments",
+ ),
+ path(
+ "colleagues/departments/create/",
+ views.DepartmentCreateView.as_view(),
+ name="department_create",
+ ),
]
diff --git a/back/admin/people/views.py b/back/admin/people/views.py
index d2e3fbdd9..38b798469 100644
--- a/back/admin/people/views.py
+++ b/back/admin/people/views.py
@@ -16,6 +16,7 @@
from admin.admin_tasks.models import AdminTask
from admin.hardware.models import Hardware
+from admin.hardware.selectors import get_hardware_templates_for_user
from admin.integrations.exceptions import (
DataIsNotJSONError,
FailedPaginatedResponseError,
@@ -23,19 +24,28 @@
)
from admin.integrations.models import Integration
from admin.integrations.sync_userinfo import SyncUsers
+from admin.people.selectors import (
+ get_colleagues_for_user,
+ get_offboarding_colleagues_for_user,
+)
from admin.people.serializers import UserImportSerializer
from admin.resources.models import Resource
+from admin.resources.selectors import get_resource_templates_for_user
from admin.sequences.models import Condition, Sequence
from api.permissions import AdminPermission
+from misc.mixins import FormWithUserContextMixin
from organization.models import Organization, WelcomeMessage
from slack_bot.utils import Slack, actions, button, paragraph
from users.emails import email_new_admin_cred
from users.mixins import (
AdminOrManagerPermMixin,
AdminPermMixin,
- IsAdminOrNewHireManagerMixin,
)
-from users.models import ToDoUser
+from users.models import Department, ToDoUser
+from users.selectors import (
+ get_all_offboarding_users_for_departments_of_user,
+ get_available_departments_for_user,
+)
from .forms import (
ColleagueCreateForm,
@@ -50,10 +60,12 @@
class ColleagueListView(AdminOrManagerPermMixin, ListView):
template_name = "colleagues.html"
- queryset = get_user_model().objects.all()
paginate_by = settings.COLLEAGUE_PAGINATE_BY
ordering = ["first_name", "last_name"]
+ def get_queryset(self):
+ return get_colleagues_for_user(user=self.request.user)
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Colleagues")
@@ -68,10 +80,12 @@ def get_context_data(self, **kwargs):
class OffboardingColleagueListView(AdminOrManagerPermMixin, ListView):
template_name = "offboarding.html"
- queryset = get_user_model().offboarding.all()
paginate_by = settings.OFFBOARDING_USERS_PAGINATE_BY
ordering = ["termination_date", "email"]
+ def get_queryset(self):
+ return get_offboarding_colleagues_for_user(user=self.request.user)
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Employees who are about to leave the company")
@@ -79,7 +93,9 @@ def get_context_data(self, **kwargs):
return context
-class ColleagueCreateView(AdminOrManagerPermMixin, SuccessMessageMixin, CreateView):
+class ColleagueCreateView(
+ AdminOrManagerPermMixin, FormWithUserContextMixin, SuccessMessageMixin, CreateView
+):
template_name = "colleague_create.html"
model = get_user_model()
form_class = ColleagueCreateForm
@@ -97,12 +113,17 @@ def get_context_data(self, **kwargs):
return context
-class ColleagueUpdateView(AdminOrManagerPermMixin, SuccessMessageMixin, UpdateView):
+class ColleagueUpdateView(
+ AdminOrManagerPermMixin, FormWithUserContextMixin, SuccessMessageMixin, UpdateView
+):
template_name = "colleague_update.html"
model = get_user_model()
form_class = ColleagueUpdateForm
success_message = _("Employee has been updated")
+ def get_queryset(self):
+ return get_colleagues_for_user(user=self.request.user)
+
def get_success_url(self):
return self.request.path
@@ -116,7 +137,9 @@ def get_context_data(self, **kwargs):
class ColleagueHardwareView(AdminOrManagerPermMixin, DetailView):
template_name = "add_hardware.html"
- model = get_user_model()
+
+ def get_queryset(self):
+ return get_colleagues_for_user(user=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -132,8 +155,12 @@ class ColleagueToggleHardwareView(AdminOrManagerPermMixin, View):
def post(self, request, pk, template_id, *args, **kwargs):
context = {}
- user = get_object_or_404(get_user_model(), id=pk)
- hardware = get_object_or_404(Hardware, id=template_id, template=True)
+ user = get_object_or_404(get_colleagues_for_user(user=request.user), id=pk)
+ hardware = get_object_or_404(
+ get_hardware_templates_for_user(user=request.user),
+ id=template_id,
+ template=True,
+ )
if user.hardware.filter(id=hardware.id).exists():
user.hardware.remove(hardware)
else:
@@ -146,7 +173,9 @@ def post(self, request, pk, template_id, *args, **kwargs):
class ColleagueResourceView(AdminOrManagerPermMixin, DetailView):
template_name = "add_resources.html"
- model = get_user_model()
+
+ def get_queryset(self):
+ return get_colleagues_for_user(user=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -155,7 +184,7 @@ def get_context_data(self, **kwargs):
"name": new_hire.full_name
}
context["subtitle"] = _("Employee")
- context["object_list"] = Resource.templates.all()
+ context["object_list"] = Resource.templates.for_user(user=self.request.user)
return context
@@ -164,8 +193,10 @@ class ColleagueToggleResourceView(AdminOrManagerPermMixin, View):
def post(self, request, pk, template_id, *args, **kwargs):
context = {}
- user = get_object_or_404(get_user_model(), id=pk)
- resource = get_object_or_404(Resource, id=template_id, template=True)
+ user = get_object_or_404(get_colleagues_for_user(user=self.request.user), id=pk)
+ resource = get_object_or_404(
+ get_resource_templates_for_user(user=self.request.user), id=template_id
+ )
if user.resources.filter(id=resource.id).exists():
user.resources.remove(resource)
else:
@@ -239,7 +270,7 @@ class ColleagueGiveSlackAccessView(AdminOrManagerPermMixin, View):
def post(self, request, pk, *args, **kwargs):
context = {}
- user = get_object_or_404(get_user_model(), pk=pk)
+ user = get_object_or_404(get_colleagues_for_user(user=self.request.user), pk=pk)
context["colleague"] = user
context["slack"] = True
context["url_name"] = "people:connect-to-slack"
@@ -295,7 +326,9 @@ class ColleagueTogglePortalAccessView(AdminOrManagerPermMixin, View):
def post(self, request, pk, *args, **kwargs):
context = {}
user = get_object_or_404(
- get_user_model(), pk=pk, role=get_user_model().Role.OTHER
+ get_colleagues_for_user(user=self.request.user),
+ pk=pk,
+ role=get_user_model().Role.OTHER,
)
context["colleague"] = user
context["url_name"] = "people:toggle-portal-access"
@@ -314,10 +347,14 @@ def post(self, request, pk, *args, **kwargs):
return render(request, self.template_name, context)
-class AddOffboardingSequenceView(AdminPermMixin, SuccessMessageMixin, UpdateView):
+class AddOffboardingSequenceView(
+ AdminOrManagerPermMixin, SuccessMessageMixin, UpdateView
+):
template_name = "add_offboarding_sequence.html"
form_class = OffboardingSequenceChoiceForm
- model = get_user_model()
+
+ def get_queryset(self):
+ return get_colleagues_for_user(user=self.request.user)
def dispatch(self, *args, **kwargs):
# raise "login required" before 404
@@ -376,9 +413,11 @@ def form_valid(self, form):
return redirect("people:colleagues")
-class ColleagueOffboardingSequenceView(IsAdminOrNewHireManagerMixin, DetailView):
+class ColleagueOffboardingSequenceView(AdminOrManagerPermMixin, DetailView):
template_name = "offboarding_detail.html"
- queryset = get_user_model().offboarding.all()
+
+ def get_queryset(self):
+ return get_all_offboarding_users_for_departments_of_user(user=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -496,3 +535,33 @@ def create(self, request, *args, **kwargs):
"Admins and managers will receive an email shortly."
)
return HttpResponse(f" {success_message} ")
+
+
+class DepartmentListView(AdminOrManagerPermMixin, ListView):
+ template_name = "departments.html"
+ paginate_by = 20
+
+ def get_queryset(self):
+ return get_available_departments_for_user(user=self.request.user)
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["title"] = _("Roles and departments")
+ context["subtitle"] = _("people")
+ return context
+
+
+class DepartmentCreateView(AdminOrManagerPermMixin, SuccessMessageMixin, CreateView):
+ template_name = "department_create.html"
+ model = Department
+ fields = [
+ "name",
+ ]
+ success_message = _("Department has been created")
+ success_url = reverse_lazy("people:departments")
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["title"] = _("Roles and departments")
+ context["subtitle"] = _("people")
+ return context
diff --git a/back/admin/preboarding/factories.py b/back/admin/preboarding/factories.py
index ef7c059a5..040edcf82 100644
--- a/back/admin/preboarding/factories.py
+++ b/back/admin/preboarding/factories.py
@@ -3,11 +3,15 @@
from pytest_factoryboy import register
from admin.preboarding.models import Preboarding
+from misc.mixins import DepartmentsPostGenerationMixin
@register
-class PreboardingFactory(factory.django.DjangoModelFactory):
+class PreboardingFactory(
+ factory.django.DjangoModelFactory, DepartmentsPostGenerationMixin
+):
name = FuzzyText()
class Meta:
model = Preboarding
+ skip_postgeneration_save = True
diff --git a/back/admin/preboarding/forms.py b/back/admin/preboarding/forms.py
index ef1abb49d..8b512e48a 100644
--- a/back/admin/preboarding/forms.py
+++ b/back/admin/preboarding/forms.py
@@ -2,11 +2,12 @@
from crispy_forms.layout import Div, Field, Layout
from admin.templates.forms import MultiSelectField, TagModelForm, WYSIWYGField
+from misc.mixins import FilterDepartmentsFieldByUserMixin
from .models import Preboarding
-class PreboardingForm(TagModelForm):
+class PreboardingForm(FilterDepartmentsFieldByUserMixin, TagModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
@@ -15,6 +16,7 @@ def __init__(self, *args, **kwargs):
Div(
Div(
Field("name"),
+ Field("departments"),
MultiSelectField("tags"),
css_class="col-4",
),
@@ -25,4 +27,4 @@ def __init__(self, *args, **kwargs):
class Meta:
model = Preboarding
- exclude = ("template", "picture")
+ fields = ("content", "name", "tags", "departments")
diff --git a/back/admin/preboarding/migrations/0015_alter_preboarding_options_preboarding_departments.py b/back/admin/preboarding/migrations/0015_alter_preboarding_options_preboarding_departments.py
new file mode 100644
index 000000000..38a6256a7
--- /dev/null
+++ b/back/admin/preboarding/migrations/0015_alter_preboarding_options_preboarding_departments.py
@@ -0,0 +1,26 @@
+# Generated by Django 5.2.6 on 2025-09-23 02:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("preboarding", "0014_remove_preboarding_form"),
+ ("users", "0043_remove_user_department_user_departments_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="preboarding",
+ options={"ordering": ("name",)},
+ ),
+ migrations.AddField(
+ model_name="preboarding",
+ name="departments",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="Leave empty to make it available for all managers/admins",
+ to="users.department",
+ ),
+ ),
+ ]
diff --git a/back/admin/preboarding/selectors.py b/back/admin/preboarding/selectors.py
new file mode 100644
index 000000000..56e933053
--- /dev/null
+++ b/back/admin/preboarding/selectors.py
@@ -0,0 +1,6 @@
+from admin.preboarding.models import Preboarding
+from users.models import User
+
+
+def get_preboarding_templates_for_user(*, user: User):
+ return Preboarding.templates.for_user(user=user)
diff --git a/back/admin/preboarding/views.py b/back/admin/preboarding/views.py
index 55103d841..a12ed2e84 100644
--- a/back/admin/preboarding/views.py
+++ b/back/admin/preboarding/views.py
@@ -5,17 +5,20 @@
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.views.generic.list import ListView
+from admin.preboarding.selectors import get_preboarding_templates_for_user
+from misc.mixins import FormWithUserContextMixin
from users.mixins import AdminOrManagerPermMixin
from .forms import PreboardingForm
-from .models import Preboarding
class PreboardingListView(AdminOrManagerPermMixin, ListView):
template_name = "templates.html"
- queryset = Preboarding.templates.all().order_by("name")
paginate_by = settings.PREBOARDING_PAGINATE_BY
+ def get_queryset(self):
+ return get_preboarding_templates_for_user(user=self.request.user)
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Preboarding items")
@@ -24,7 +27,9 @@ def get_context_data(self, **kwargs):
return context
-class PreboardingCreateView(AdminOrManagerPermMixin, SuccessMessageMixin, CreateView):
+class PreboardingCreateView(
+ AdminOrManagerPermMixin, FormWithUserContextMixin, SuccessMessageMixin, CreateView
+):
template_name = "template_update.html"
form_class = PreboardingForm
success_url = reverse_lazy("preboarding:list")
@@ -37,13 +42,17 @@ def get_context_data(self, **kwargs):
return context
-class PreboardingUpdateView(AdminOrManagerPermMixin, SuccessMessageMixin, UpdateView):
+class PreboardingUpdateView(
+ AdminOrManagerPermMixin, FormWithUserContextMixin, SuccessMessageMixin, UpdateView
+):
template_name = "template_update.html"
form_class = PreboardingForm
success_url = reverse_lazy("preboarding:list")
- queryset = Preboarding.templates.all()
success_message = _("Preboarding item has been updated")
+ def get_queryset(self):
+ return get_preboarding_templates_for_user(user=self.request.user)
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Update preboarding item")
@@ -52,6 +61,8 @@ def get_context_data(self, **kwargs):
class PreboardingDeleteView(AdminOrManagerPermMixin, SuccessMessageMixin, DeleteView):
- queryset = Preboarding.objects.all()
success_url = reverse_lazy("preboarding:list")
success_message = _("Sequence item has been removed")
+
+ def get_queryset(self):
+ return get_preboarding_templates_for_user(user=self.request.user)
diff --git a/back/admin/resources/factories.py b/back/admin/resources/factories.py
index 7e2b41746..46f3f50a0 100644
--- a/back/admin/resources/factories.py
+++ b/back/admin/resources/factories.py
@@ -3,6 +3,7 @@
from pytest_factoryboy import register
from admin.resources.models import Category, Chapter, Resource
+from misc.mixins import DepartmentsPostGenerationMixin
@register
@@ -30,7 +31,9 @@ class Meta:
@register
-class ResourceFactory(factory.django.DjangoModelFactory):
+class ResourceFactory(
+ factory.django.DjangoModelFactory, DepartmentsPostGenerationMixin
+):
name = FuzzyText()
category = factory.SubFactory(CategoryFactory)
diff --git a/back/admin/resources/forms.py b/back/admin/resources/forms.py
index 0e42c459a..84d9a1e2e 100644
--- a/back/admin/resources/forms.py
+++ b/back/admin/resources/forms.py
@@ -10,6 +10,7 @@
MultiSelectField,
TagModelForm,
)
+from misc.mixins import FilterDepartmentsFieldByUserMixin
from .models import Category, Chapter, Resource
from .serializers import ChapterSerializer
@@ -19,7 +20,7 @@ class ChapterField(FieldWithExtraContext):
template = "chapter_field.html"
-class ResourceForm(TagModelForm):
+class ResourceForm(FilterDepartmentsFieldByUserMixin, TagModelForm):
category = ModelChoiceFieldWithCreate(
label=_("Category"),
queryset=Category.objects.all(),
@@ -88,9 +89,13 @@ def __init__(self, *args, **kwargs):
Field("name"),
css_class="col-6",
),
+ Div(
+ MultiSelectField("departments"),
+ css_class="col-3",
+ ),
Div(
MultiSelectField("tags"),
- css_class="col-6",
+ css_class="col-3",
),
css_class="row",
),
@@ -134,7 +139,15 @@ def __init__(self, *args, **kwargs):
class Meta:
model = Resource
- fields = ("name", "tags", "category", "course", "on_day", "remove_on_complete")
+ fields = (
+ "name",
+ "tags",
+ "category",
+ "course",
+ "on_day",
+ "remove_on_complete",
+ "departments",
+ )
help_texts = {
"course": _("When enabled, new hires will have to walk through this"),
"remove_on_complete": _(
diff --git a/back/admin/resources/migrations/0017_alter_resource_options_resource_departments.py b/back/admin/resources/migrations/0017_alter_resource_options_resource_departments.py
new file mode 100644
index 000000000..98b5d3881
--- /dev/null
+++ b/back/admin/resources/migrations/0017_alter_resource_options_resource_departments.py
@@ -0,0 +1,26 @@
+# Generated by Django 5.2.6 on 2025-09-23 02:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("resources", "0016_alter_chapter_type"),
+ ("users", "0043_remove_user_department_user_departments_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="resource",
+ options={"ordering": ("name",)},
+ ),
+ migrations.AddField(
+ model_name="resource",
+ name="departments",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="Leave empty to make it available for all managers/admins",
+ to="users.department",
+ ),
+ ),
+ ]
diff --git a/back/admin/resources/models.py b/back/admin/resources/models.py
index bd24106ce..f9a79f564 100644
--- a/back/admin/resources/models.py
+++ b/back/admin/resources/models.py
@@ -11,7 +11,7 @@
from misc.fields import ContentJSONField
from misc.mixins import ContentMixin
-from organization.models import BaseItem, Notification
+from organization.models import BaseItem, Notification, ObjectsManager
class Category(models.Model):
@@ -21,7 +21,7 @@ def __str__(self):
return self.name
-class ResourceManager(models.Manager):
+class ResourceManager(ObjectsManager):
def search(self, u, query):
query = SearchQuery(query)
vector = (
diff --git a/back/admin/resources/selectors.py b/back/admin/resources/selectors.py
new file mode 100644
index 000000000..f49af4a84
--- /dev/null
+++ b/back/admin/resources/selectors.py
@@ -0,0 +1,6 @@
+from admin.resources.models import Resource
+from users.models import User
+
+
+def get_resource_templates_for_user(*, user: User):
+ return Resource.templates.for_user(user=user)
diff --git a/back/admin/resources/views.py b/back/admin/resources/views.py
index c60a163a4..1be3769f6 100644
--- a/back/admin/resources/views.py
+++ b/back/admin/resources/views.py
@@ -5,17 +5,20 @@
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.views.generic.list import ListView
+from admin.resources.selectors import get_resource_templates_for_user
+from misc.mixins import FormWithUserContextMixin
from users.mixins import AdminOrManagerPermMixin
from .forms import ResourceForm
-from .models import Resource
class ResourceListView(AdminOrManagerPermMixin, ListView):
template_name = "templates.html"
- queryset = Resource.templates.all().order_by("name")
paginate_by = settings.RESOURCE_PAGINATE_BY
+ def get_queryset(self):
+ return get_resource_templates_for_user(user=self.request.user)
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Resource items")
@@ -24,7 +27,9 @@ def get_context_data(self, **kwargs):
return context
-class ResourceCreateView(AdminOrManagerPermMixin, SuccessMessageMixin, CreateView):
+class ResourceCreateView(
+ AdminOrManagerPermMixin, FormWithUserContextMixin, SuccessMessageMixin, CreateView
+):
template_name = "resource_update.html"
form_class = ResourceForm
success_url = reverse_lazy("resources:list")
@@ -37,13 +42,17 @@ def get_context_data(self, **kwargs):
return context
-class ResourceUpdateView(AdminOrManagerPermMixin, SuccessMessageMixin, UpdateView):
+class ResourceUpdateView(
+ AdminOrManagerPermMixin, FormWithUserContextMixin, SuccessMessageMixin, UpdateView
+):
template_name = "resource_update.html"
form_class = ResourceForm
success_url = reverse_lazy("resources:list")
- queryset = Resource.templates.all()
success_message = _("Resource item has been updated")
+ def get_queryset(self):
+ return get_resource_templates_for_user(user=self.request.user)
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Update resource item")
@@ -52,6 +61,8 @@ def get_context_data(self, **kwargs):
class ResourceDeleteView(AdminOrManagerPermMixin, SuccessMessageMixin, DeleteView):
- queryset = Resource.objects.all()
success_url = reverse_lazy("resources:list")
success_message = _("Resource item has been removed")
+
+ def get_queryset(self):
+ return get_resource_templates_for_user(user=self.request.user)
diff --git a/back/admin/sequences/forms.py b/back/admin/sequences/forms.py
index ead464300..e6dbd5900 100644
--- a/back/admin/sequences/forms.py
+++ b/back/admin/sequences/forms.py
@@ -6,6 +6,8 @@
from admin.templates.forms import MultiSelectField, WYSIWYGField
from admin.to_do.models import ToDo
+from admin.to_do.selectors import get_to_do_templates_for_user
+from misc.mixins import FilterDepartmentsFieldByUserMixin
from users.models import User
from .models import (
@@ -15,23 +17,33 @@
PendingEmailMessage,
PendingSlackMessage,
PendingTextMessage,
+ Sequence,
)
+class DepartmentForm(FilterDepartmentsFieldByUserMixin, forms.ModelForm):
+ class Meta:
+ model = Sequence
+ fields = [
+ "departments",
+ ]
+
+
class ConditionForm(forms.ModelForm):
condition_to_do = forms.ModelMultipleChoiceField(
- queryset=ToDo.templates.defer_content().all(),
+ queryset=ToDo.objects.none(),
to_field_name="id",
required=False,
)
condition_admin_tasks = forms.ModelMultipleChoiceField(
- queryset=PendingAdminTask.objects.all(),
+ queryset=PendingAdminTask.objects.none(),
to_field_name="id",
required=False,
)
def __init__(self, *args, **kwargs):
sequence = kwargs.pop("sequence")
+ user = kwargs.pop("user")
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
@@ -53,7 +65,9 @@ def __init__(self, *args, **kwargs):
)
self.fields["time"].required = False
self.fields["days"].required = False
- self.fields["condition_to_do"].required = False
+ self.fields["condition_to_do"].queryset = get_to_do_templates_for_user(
+ user=user
+ )
pending_tasks = PendingAdminTask.objects.filter(condition__sequence=sequence)
self.fields["condition_admin_tasks"].queryset = pending_tasks
# Remove last option, which will only be one of
@@ -174,7 +188,7 @@ class PendingAdminTaskForm(forms.ModelForm):
widget=forms.DateInput(attrs={"type": "date"}, format=("%Y-%m-%d")),
)
- def __init__(self, *args, **kwargs):
+ def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["option"].initial = PendingAdminTask.Notification.NO
self.fields["comment"].required = True
@@ -230,7 +244,7 @@ def clean(self):
class PendingSlackMessageForm(forms.ModelForm):
- def __init__(self, *args, **kwargs):
+ def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
@@ -278,7 +292,7 @@ class Meta:
class PendingTextMessageForm(forms.ModelForm):
- def __init__(self, *args, **kwargs):
+ def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
@@ -318,7 +332,7 @@ class Meta:
class PendingEmailMessageForm(forms.ModelForm):
- def __init__(self, *args, **kwargs):
+ def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
diff --git a/back/admin/sequences/migrations/0046_alter_sequence_options_sequence_departments.py b/back/admin/sequences/migrations/0046_alter_sequence_options_sequence_departments.py
new file mode 100644
index 000000000..42a2eeb61
--- /dev/null
+++ b/back/admin/sequences/migrations/0046_alter_sequence_options_sequence_departments.py
@@ -0,0 +1,26 @@
+# Generated by Django 5.2.6 on 2025-09-23 02:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("sequences", "0045_alter_condition_condition_type"),
+ ("users", "0043_remove_user_department_user_departments_and_more"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="sequence",
+ options={"ordering": ("name",)},
+ ),
+ migrations.AddField(
+ model_name="sequence",
+ name="departments",
+ field=models.ManyToManyField(
+ blank=True,
+ help_text="Leave empty to make it available to all managers",
+ to="users.department",
+ ),
+ ),
+ ]
diff --git a/back/admin/sequences/models.py b/back/admin/sequences/models.py
index 9e5ba3416..fe72f95dc 100644
--- a/back/admin/sequences/models.py
+++ b/back/admin/sequences/models.py
@@ -19,17 +19,25 @@
from admin.to_do.models import ToDo
from misc.fields import ContentJSONField, EncryptedJSONField
from misc.mixins import ContentMixin
-from organization.models import Notification
+from organization.models import FilteredForManagerQuerySet, Notification
from slack_bot.models import SlackChannel
from slack_bot.utils import Slack
-class OnboardingSequenceManager(models.Manager):
+class SequenceManager(models.Manager):
+ def get_queryset(self):
+ return FilteredForManagerQuerySet(self.model, using=self._db)
+
+ def for_user(self, user):
+ return self.get_queryset().for_user(user)
+
+
+class OnboardingSequenceManager(SequenceManager):
def get_queryset(self):
return super().get_queryset().filter(category=Sequence.Category.ONBOARDING)
-class OffboardingSequenceManager(models.Manager):
+class OffboardingSequenceManager(SequenceManager):
def get_queryset(self):
return super().get_queryset().filter(category=Sequence.Category.OFFBOARDING)
@@ -42,11 +50,19 @@ class Category(models.IntegerChoices):
name = models.CharField(verbose_name=_("Name"), max_length=240)
auto_add = models.BooleanField(default=False)
category = models.IntegerField(choices=Category.choices)
+ departments = models.ManyToManyField(
+ "users.Department",
+ blank=True,
+ help_text=_("Leave empty to make it available to all managers"),
+ )
- objects = models.Manager()
+ objects = SequenceManager()
onboarding = OnboardingSequenceManager()
offboarding = OffboardingSequenceManager()
+ class Meta:
+ ordering = ("name",)
+
def __str__(self):
return self.name
@@ -269,6 +285,10 @@ def for_new_hire(self):
person_type=ExternalMessage.PersonType.NEWHIRE
)
+ def for_user(self, user):
+ # just return all as we filter on sequence
+ return self.get_queryset()
+
def for_admins(self):
return self.get_queryset().exclude(
person_type=ExternalMessage.PersonType.NEWHIRE
@@ -461,6 +481,12 @@ class Meta:
proxy = True
+class PendingAdminTaskManager(models.Manager):
+ def for_user(self, user):
+ # just return all as we filter on sequence
+ return self.get_queryset()
+
+
class PendingAdminTask(models.Model):
class PersonType(models.IntegerChoices):
NEWHIRE = 0, _("New hire/User to be offboarded")
@@ -520,6 +546,8 @@ class Notification(models.IntegerChoices):
),
)
+ objects = PendingAdminTaskManager()
+
def __str__(self):
return self.name
diff --git a/back/admin/sequences/offboarding_views.py b/back/admin/sequences/offboarding_views.py
index c964369d4..ae24f7b15 100644
--- a/back/admin/sequences/offboarding_views.py
+++ b/back/admin/sequences/offboarding_views.py
@@ -3,7 +3,9 @@
from django.utils.translation import gettext as _
from django.views.generic.list import ListView
-from admin.sequences.models import Sequence
+from admin.sequences.selectors import (
+ get_offboarding_sequences_for_user,
+)
from users.mixins import AdminOrManagerPermMixin
@@ -13,9 +15,11 @@ class OffboardingSequenceListView(AdminOrManagerPermMixin, ListView):
"""
template_name = "templates.html"
- queryset = Sequence.offboarding.all().order_by("name")
paginate_by = settings.SEQUENCE_PAGINATE_BY
+ def get_queryset(self):
+ return get_offboarding_sequences_for_user(user=self.request.user)
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Offboarding sequence items")
diff --git a/back/admin/sequences/selectors.py b/back/admin/sequences/selectors.py
new file mode 100644
index 000000000..34c111ed6
--- /dev/null
+++ b/back/admin/sequences/selectors.py
@@ -0,0 +1,14 @@
+from admin.sequences.models import Sequence
+from users.models import User
+
+
+def get_sequences_for_user(*, user: User):
+ return Sequence.objects.for_user(user=user)
+
+
+def get_onboarding_sequences_for_user(*, user: User):
+ return Sequence.onboarding.for_user(user=user)
+
+
+def get_offboarding_sequences_for_user(*, user: User):
+ return Sequence.offboarding.for_user(user=user)
diff --git a/back/admin/sequences/templates/_sequence_templates_list.html b/back/admin/sequences/templates/_sequence_templates_list.html
index f96ae47d5..0a91b21bb 100644
--- a/back/admin/sequences/templates/_sequence_templates_list.html
+++ b/back/admin/sequences/templates/_sequence_templates_list.html
@@ -1,11 +1,9 @@
{% load i18n %}
{% translate "Templates" %}
- {% if sequence.is_onboarding %}
- {% endif %}
diff --git a/back/admin/sequences/templates/sequence.html b/back/admin/sequences/templates/sequence.html
index ff93baf5e..91b8470ef 100644
--- a/back/admin/sequences/templates/sequence.html
+++ b/back/admin/sequences/templates/sequence.html
@@ -1,5 +1,5 @@
{% extends 'admin_base.html' %}
-{% load static %}
+{% load static crispy_forms_tags %}
{% load i18n %}
{% block actions %}
@@ -30,22 +30,29 @@
-
-
-
-
+
+
{% endblock %}
{% block extra_css %}
|