Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 4.2.30 on 2026-05-02 09:14

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('academics', '0003_remove_departments_institution_colleges_and_more'),
]

operations = [
migrations.AlterField(
model_name='colleges',
name='id',
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='departments',
name='id',
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='institutions',
name='id',
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='programmes',
name='id',
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]
6 changes: 0 additions & 6 deletions logify-backend/apps/academics/test_academics.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,6 @@ def setUp(self):
academic_supervisor=self.academic_supervisor,
)

def test_admin_can_view_any_institution(self):
self.client.force_authenticate(user=self.admin)
response = self.client.get(reverse("institutions-detail", args=[self.institution.pk]))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["name"], "University A")

def test_student_can_view_own_institution(self):
self.client.force_authenticate(user=self.student)
response = self.client.get(reverse("institutions-detail", args=[self.institution.pk]))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 4.2.30 on 2026-05-02 09:14

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('academics', '0004_alter_colleges_id_alter_departments_id_and_more'),
('accounts', '0006_remove_user_student_registry_id_user_intake_year_and_more'),
]

operations = [
migrations.AddField(
model_name='supervisorapplication',
name='college',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='academics.colleges'),
),
migrations.AlterField(
model_name='staffprofiles',
name='id',
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='supervisorapplication',
name='id',
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='user',
name='id',
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]
3 changes: 3 additions & 0 deletions logify-backend/apps/accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ class SupervisorApplication(models.Model):
user = models.OneToOneField(
User, on_delete=models.CASCADE, related_name="supervisor_application"
)
college = models.ForeignKey(
"academics.Colleges", on_delete=models.SET_NULL, null=True, blank=True
)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="pending")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
Expand Down
3 changes: 2 additions & 1 deletion logify-backend/apps/accounts/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def create(self, validated_data):
user.save()

# Create SupervisorApplication
SupervisorApplication.objects.create(user=user)
SupervisorApplication.objects.create(user=user, college=college)

# Academic supervisors get a staff profile linked to their department.
if role == User.ACADEMIC_SUPERVISOR and department_id:
Expand Down Expand Up @@ -297,6 +297,7 @@ class Meta:
"phone",
"is_active",
"staff_profile",
"college",
)

def get_full_name(self, obj):
Expand Down
30 changes: 27 additions & 3 deletions logify-backend/apps/accounts/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ def test_supervisor_signup_and_approval(self, api_client, setup_college_data):
last_name="Admin",
institution_id=str(institution.id),
)
StaffProfiles.objects.create(
user=admin, staff_number="ADM001", department=department, title="Admin"
)
api_client.force_authenticate(user=admin)
app = SupervisorApplication.objects.get(user=user)

Expand Down Expand Up @@ -198,6 +201,9 @@ def test_workplace_supervisor_signup_requires_valid_organization(
def test_admin_can_list_supervisor_applications(self, api_client, setup_college_data):
institution = setup_college_data["institution"]

college = setup_college_data["college_a"]
department = Departments.objects.create(college=college, name="IT")

pending_user = User.objects.create_user(
email="pending.supervisor@test.com",
password="securepassword123",
Expand All @@ -207,7 +213,7 @@ def test_admin_can_list_supervisor_applications(self, api_client, setup_college_
institution_id=str(institution.id),
is_active=False,
)
SupervisorApplication.objects.create(user=pending_user, status="pending")
SupervisorApplication.objects.create(user=pending_user, status="pending", college=college)

approved_user = User.objects.create_user(
email="approved.supervisor@test.com",
Expand All @@ -218,7 +224,7 @@ def test_admin_can_list_supervisor_applications(self, api_client, setup_college_
institution_id=str(institution.id),
is_active=True,
)
SupervisorApplication.objects.create(user=approved_user, status="approved")
SupervisorApplication.objects.create(user=approved_user, status="approved", college=college)

admin = User.objects.create_user(
email="admin.list@test.com",
Expand All @@ -228,6 +234,9 @@ def test_admin_can_list_supervisor_applications(self, api_client, setup_college_
last_name="Admin",
institution_id=str(institution.id),
)
StaffProfiles.objects.create(
user=admin, staff_number="ADM002", department=department, title="Admin"
)
api_client.force_authenticate(user=admin)

response = api_client.get("/api/v1/accounts/supervisor/applications/")
Expand All @@ -249,6 +258,9 @@ def test_admin_lists_only_same_institution_supervisor_applications(self, api_cli
institution_a = Institutions.objects.create(name="Institution A")
institution_b = Institutions.objects.create(name="Institution B")

college_a = Colleges.objects.create(institution=institution_a, name="College A")
dept_a = Departments.objects.create(college=college_a, name="Dept A")

scoped_supervisor = User.objects.create_user(
email="scoped.supervisor@test.com",
password="securepassword123",
Expand All @@ -258,7 +270,9 @@ def test_admin_lists_only_same_institution_supervisor_applications(self, api_cli
institution_id=str(institution_a.id),
is_active=False,
)
SupervisorApplication.objects.create(user=scoped_supervisor, status="pending")
SupervisorApplication.objects.create(
user=scoped_supervisor, status="pending", college=college_a
)

other_supervisor = User.objects.create_user(
email="other.institution.supervisor@test.com",
Expand All @@ -269,6 +283,7 @@ def test_admin_lists_only_same_institution_supervisor_applications(self, api_cli
institution_id=str(institution_b.id),
is_active=False,
)
# Note: we don't care about college for other_supervisor since they are in a different institution
SupervisorApplication.objects.create(user=other_supervisor, status="pending")

scoped_admin = User.objects.create_user(
Expand All @@ -279,6 +294,9 @@ def test_admin_lists_only_same_institution_supervisor_applications(self, api_cli
last_name="Admin",
institution_id=str(institution_a.id),
)
StaffProfiles.objects.create(
user=scoped_admin, staff_number="ADM-S", department=dept_a, title="Admin"
)
api_client.force_authenticate(user=scoped_admin)

response = api_client.get("/api/v1/accounts/supervisor/applications/")
Expand Down Expand Up @@ -310,6 +328,9 @@ def test_admin_cannot_approve_supervisor_from_other_institution(self, api_client
title="Supervisor",
)

college_root_a = Colleges.objects.create(institution=institution_a, name="College Root A")
dept_a = Departments.objects.create(college=college_root_a, name="Dept A")

admin = User.objects.create_user(
email="scoped.admin@test.com",
password="adminpassword",
Expand All @@ -318,6 +339,9 @@ def test_admin_cannot_approve_supervisor_from_other_institution(self, api_client
last_name="Admin",
institution_id=str(institution_a.id),
)
StaffProfiles.objects.create(
user=admin, staff_number="ADM-OA", department=dept_a, title="Admin"
)
api_client.force_authenticate(user=admin)

response = api_client.post(
Expand Down
18 changes: 13 additions & 5 deletions logify-backend/apps/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,18 @@ def post(self, request, application_id):

if not request.user.is_superuser and request.user.role == User.INTERNSHIP_ADMIN:
institution_id = get_user_institution_id(request.user)
if institution_id is None or not is_user_in_institution(
application.user, institution_id
admin_college_id = get_user_college_id(request.user)
if (
institution_id is None
or admin_college_id is None
or not is_user_in_institution(application.user, institution_id)
or (
application.college_id
and int(application.college_id) != int(admin_college_id)
)
):
raise PermissionDenied(
"You can only manage supervisor applications in your institution."
"You can only manage supervisor applications in your college scope."
)

action = request.data.get("action")
Expand Down Expand Up @@ -122,12 +129,13 @@ def get_queryset(self):
user = self.request.user
if not user.is_superuser:
institution_id = get_user_institution_id(user)
if institution_id is None:
admin_college_id = get_user_college_id(user)
if institution_id is None or admin_college_id is None:
return queryset.none()
queryset = queryset.filter(
Q(user__institution_id=str(institution_id))
| Q(user__staffprofiles__department__college__institution_id=institution_id)
).distinct()
).filter(college_id=admin_college_id)

status_filter = self.request.query_params.get("status") # type: ignore
if status_filter:
Expand Down
Loading