Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 49 additions & 39 deletions backend/backend/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ def _is_appointer(self, user):
def get_team_filter(self, team_ids):
return {}

def get_object_team_id(self, obj):
return None
def get_object_team_ids(self, obj):
"""Team ids an object belongs to (a role may now belong to several)."""
return []

def has_module_permission(self, request):
return request.user.is_superuser or self._is_appointer(request.user)
Expand All @@ -61,9 +62,8 @@ def has_view_permission(self, request, obj=None):
obj = self.get_object(request, obj)
if obj is None:
return False
return self.get_object_team_id(obj) in self._get_appointer_team_ids(
request.user
)
appointer_team_ids = set(self._get_appointer_team_ids(request.user))
return bool(set(self.get_object_team_ids(obj)) & appointer_team_ids)

def has_add_permission(self, request):
if request.user.is_superuser:
Expand Down Expand Up @@ -101,6 +101,7 @@ class GroupAdmin(BaseGroupAdmin, ModelAdmin):
list_filter_submit = True


@admin.register(Member)
class MemberAdmin(BaseUserAdmin, ModelAdmin):
"""
Custom admin interface for the Member model.
Expand Down Expand Up @@ -178,29 +179,34 @@ class TeamAdmin(ModelAdmin):

@admin.register(Role)
class RoleAdmin(AppointerTeamScopeMixin, ModelAdmin):
list_display = ("title_en", "title_sv", "team", "role_type", "archived")
list_filter = ("team", "role_type", "archived")
list_display = ("title_en", "title_sv", "display_teams", "role_type", "archived")
list_filter = ("teams", "role_type", "archived")
search_fields = (
"title_en",
"title_sv",
"description_en",
"description_sv",
"team__name_en",
"team__name_sv",
"teams__name_en",
"teams__name_sv",
)
list_filter_submit = True
filter_horizontal = ("teams",)

@admin.display(description=_("Teams"))
def display_teams(self, obj):
return ", ".join(obj.teams.values_list("name_en", flat=True))

def get_team_filter(self, team_ids):
return {"team_id__in": team_ids}
return {"teams__id__in": team_ids}

def get_object_team_id(self, obj):
return obj.team_id
def get_object_team_ids(self, obj):
return list(obj.teams.values_list("id", flat=True))

def formfield_for_foreignkey(self, db_field, request, **kwargs):
if not request.user.is_superuser and db_field.name == "team":
def formfield_for_manytomany(self, db_field, request, **kwargs):
if not request.user.is_superuser and db_field.name == "teams":
team_ids = self._get_appointer_team_ids(request.user)
kwargs["queryset"] = Team.objects.filter(id__in=team_ids)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
return super().formfield_for_manytomany(db_field, request, **kwargs)


@admin.register(Position)
Expand All @@ -213,21 +219,21 @@ class PositionAdmin(AppointerTeamScopeMixin, ModelAdmin):
"term_end",
"appointed",
)
list_filter = ("role__team", "recruitment_start", "term_from")
list_filter = ("role__teams", "recruitment_start", "term_from")
search_fields = ("role__title_en", "role__title_sv", "comment_eng", "comment_sv")
list_filter_submit = True
date_hierarchy = "recruitment_start"

def get_team_filter(self, team_ids):
return {"role__team_id__in": team_ids}
return {"role__teams__id__in": team_ids}

def get_object_team_id(self, obj):
return obj.role.team_id
def get_object_team_ids(self, obj):
return list(obj.role.teams.values_list("id", flat=True))

def formfield_for_foreignkey(self, db_field, request, **kwargs):
if not request.user.is_superuser and db_field.name == "role":
team_ids = self._get_appointer_team_ids(request.user)
kwargs["queryset"] = Role.objects.filter(team_id__in=team_ids)
kwargs["queryset"] = Role.objects.filter(teams__id__in=team_ids).distinct()
return super().formfield_for_foreignkey(db_field, request, **kwargs)

class ReferenceInline(admin.TabularInline):
Expand Down Expand Up @@ -256,7 +262,7 @@ def _has_application_scope(self, request, obj=None):
if obj is None:
return True

return obj.position.role.team_id in team_ids
return obj.position.role.teams.filter(id__in=team_ids).exists()

def has_view_permission(self, request, obj=None):
return self._has_application_scope(request, obj)
Expand All @@ -273,7 +279,7 @@ def has_add_permission(self, request, _obj=None):
@admin.register(Application)
class ApplicationAdmin(AppointerTeamScopeMixin, ModelAdmin):
list_display = ("position", "member", "status", "decision_date")
list_filter = ("status", "position__role__team")
list_filter = ("status", "position__role__teams")
search_fields = (
"position__role__title_en",
"position__role__title_sv",
Expand Down Expand Up @@ -324,22 +330,24 @@ def turn_down_application(self, request, object_id):
return redirect(reverse("admin:backend_application_change", args=[object_id]))

def get_team_filter(self, team_ids):
return {"position__role__team_id__in": team_ids}
return {"position__role__teams__id__in": team_ids}

def get_object_team_id(self, obj):
return obj.position.role.team_id
def get_object_team_ids(self, obj):
return list(obj.position.role.teams.values_list("id", flat=True))

def formfield_for_foreignkey(self, db_field, request, **kwargs):
if not request.user.is_superuser and db_field.name == "position":
team_ids = self._get_appointer_team_ids(request.user)
kwargs["queryset"] = Position.objects.filter(role__team_id__in=team_ids)
kwargs["queryset"] = Position.objects.filter(
role__teams__id__in=team_ids
).distinct()
return super().formfield_for_foreignkey(db_field, request, **kwargs)


@admin.register(Appointment)
class AppointmentAdmin(AppointerTeamScopeMixin, ModelAdmin):
list_display = ("member", "position", "status", "appointed_date", "appointed_by")
list_filter = ("status", "position__role__team")
list_filter = ("status", "position__role__teams")
search_fields = (
"member__name",
"member__email",
Expand All @@ -350,37 +358,39 @@ class AppointmentAdmin(AppointerTeamScopeMixin, ModelAdmin):
date_hierarchy = "appointed_date"

def get_team_filter(self, team_ids):
return {"position__role__team_id__in": team_ids}
return {"position__role__teams__id__in": team_ids}

def get_object_team_id(self, obj):
return obj.position.role.team_id
def get_object_team_ids(self, obj):
return list(obj.position.role.teams.values_list("id", flat=True))

def formfield_for_foreignkey(self, db_field, request, **kwargs):
if not request.user.is_superuser and db_field.name == "position":
team_ids = self._get_appointer_team_ids(request.user)
kwargs["queryset"] = Position.objects.filter(role__team_id__in=team_ids)
kwargs["queryset"] = Position.objects.filter(
role__teams__id__in=team_ids
).distinct()
return super().formfield_for_foreignkey(db_field, request, **kwargs)

def has_add_permission(self, request):
return False

class StudyProgramInline(admin.TabularInline):
model = StudyProgram
extra = 1


@admin.register(Section)
class SectionAdmin(admin.ModelAdmin):
list_display = ("abbreviation", "section_en", "section_sv")
search_fields = ("abbreviation", "section_en", "section_sv")
inlines = [StudyProgramInline]
list_filter_submit = True


@admin.register(StudyProgram)
class StudyProgramAdmin(admin.ModelAdmin):
list_display = ("name_en", "name_sv", "section")
search_fields = ("name_en", "name_sv", "section__abbreviation")
list_filter = ("section",)
list_display = ("name_en", "name_sv", "degree", "display_sections")
search_fields = ("name_en", "name_sv", "sections__abbreviation")
list_filter = ("sections", "degree")
list_filter_submit = True
filter_horizontal = ("sections",)

@admin.display(description=_("Sections"))
def display_sections(self, obj):
return ", ".join(obj.sections.values_list("abbreviation", flat=True))

18 changes: 18 additions & 0 deletions backend/backend/migrations/0010_studyprogram_degree.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.1.7 on 2026-06-12 15:47

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('backend', '0009_add_email_verification_code'),
]

operations = [
migrations.AddField(
model_name='studyprogram',
name='degree',
field=models.CharField(blank=True, choices=[('bsc', 'Bachelor of Science'), ('msc', 'Master of Science'), ('msceng', 'Master of Science in Engineering'), ('be', 'Bachelor of Engineering')], default='', help_text='The degree awarded by this study program', max_length=20, verbose_name='Degree'),
),
]
18 changes: 18 additions & 0 deletions backend/backend/migrations/0011_role_election_email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.1.7 on 2026-06-12 16:05

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('backend', '0010_studyprogram_degree'),
]

operations = [
migrations.AddField(
model_name='role',
name='election_email',
field=models.EmailField(blank=True, default='', help_text='The email address used for nominations/elections to this role', max_length=254, verbose_name='Election email address'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Generated by Django 4.1.7 on 2026-06-12 16:46

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('backend', '0011_role_election_email'),
]

operations = [
migrations.RemoveField(
model_name='role',
name='team',
),
migrations.RemoveField(
model_name='studyprogram',
name='section',
),
migrations.AddField(
model_name='role',
name='teams',
field=models.ManyToManyField(blank=True, help_text='The teams this role belongs to', related_name='roles', to='backend.team', verbose_name='Teams'),
),
migrations.AddField(
model_name='studyprogram',
name='sections',
field=models.ManyToManyField(blank=True, help_text='The sections this study program belongs to', related_name='study_programs', to='backend.section', verbose_name='Sections'),
),
]
58 changes: 45 additions & 13 deletions backend/backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,11 @@ def get_appointer_team_ids(self, reference_date=None):

return list(
Team.objects.filter(
role__positions__appointments__member=self,
role__positions__appointments__status=Appointment.APPOINTED,
role__positions__term_from__lte=reference_date,
role__positions__term_end__gte=reference_date,
role__role_type__in=role_types,
roles__positions__appointments__member=self,
roles__positions__appointments__status=Appointment.APPOINTED,
roles__positions__term_from__lte=reference_date,
roles__positions__term_end__gte=reference_date,
roles__role_type__in=role_types,
)
.values_list("id", flat=True)
.distinct()
Expand Down Expand Up @@ -453,13 +453,27 @@ class StudyProgram(models.Model):
section (ForeignKey): A foreign key to the Section model, representing the section to which the study program belongs.
name_en (CharField): The name of the study program in English.
name_sv (CharField): The name of the study program in Swedish.
degree (CharField): The degree awarded by the study program (e.g. bachelor, master).
"""

section = models.ForeignKey(
BSC = "bsc"
MSC = "msc"
MSCENG = "msceng"
BE = "be"

DEGREE_CHOICES = (
(BSC, _("Bachelor of Science")),
(MSC, _("Master of Science")),
(MSCENG, _("Master of Science in Engineering")),
(BE, _("Bachelor of Engineering")),
)

sections = models.ManyToManyField(
"Section",
related_name="study_programs",
on_delete=models.CASCADE,
blank=False,
verbose_name=_("Sections"),
help_text=_("The sections this study program belongs to"),
blank=True,
)

name_en = models.CharField(
Expand All @@ -475,6 +489,15 @@ class StudyProgram(models.Model):
help_text=_("Enter the name of the section in Swedish"),
)

degree = models.CharField(
max_length=20,
choices=DEGREE_CHOICES,
verbose_name=_("Degree"),
help_text=_("The degree awarded by this study program"),
blank=True,
default="",
)

def __str__(self):
return self.name_en

Expand Down Expand Up @@ -680,11 +703,12 @@ class Role(models.Model):
(INVOLVED, _("Involved")),
)

team = models.ForeignKey(
teams = models.ManyToManyField(
"Team",
related_name="role",
on_delete=models.CASCADE,
blank=False,
related_name="roles",
verbose_name=_("Teams"),
help_text=_("The teams this role belongs to"),
blank=True,
)

role_type = models.CharField(
Expand Down Expand Up @@ -776,6 +800,13 @@ def appointer_role_types():
help_text=_("The email address for the current position holder"),
blank=False,
)

election_email = models.EmailField(
verbose_name=_("Election email address"),
help_text=_("The email address used for nominations/elections to this role"),
blank=True,
default="",
)
# ------ Administrator settings ------
# panels = [MultiFieldPanel([
# FieldRowPanel([
Expand All @@ -796,7 +827,8 @@ def appointer_role_types():
# ])]

def __str__(self):
return f"{self.title_en} ({self.team})"
team_names = ", ".join(self.teams.values_list("name_en", flat=True))
return f"{self.title_en} ({team_names})" if team_names else self.title_en


def _sync_staff_status(member, reference_date=None):
Expand Down
Loading
Loading