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 %} +
+
+
+
+

{% translate "New department" %}

+
+
+
+ {% csrf_token %} + {{ form|crispy }} + +
+
+
+
+
+{% 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 %} +
+
+
+ + + + + + + + {% for department in object_list %} + + + + {% empty %} + + + + {% endfor %} + +
{% translate "Name" %}
+ {{ department.name }} +
+ {% trans "You haven't created any departments yet" %} +
+
+ {% 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 @@
-
- -
-
- - -
-
+
+
+
+ +
+
+ + +
+
+
+
+
+ {{ department_form|crispy }} +
    @@ -181,6 +188,8 @@
+
+ {% endblock %} {% block extra_css %}