Skip to content

Commit 5f8ea8e

Browse files
authored
Merge pull request #15 from NYU-ITS/add_approver_user
Add approver user
2 parents 1f9929c + a09b05c commit 5f8ea8e

File tree

9 files changed

+309
-13
lines changed

9 files changed

+309
-13
lines changed

coldfront/core/user/management/__init__.py

Whitespace-only changes.

coldfront/core/user/management/commands/__init__.py

Whitespace-only changes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"astewart": ["Tandon School of Engineering", "NYU IT"],
3+
"arivera": ["NYU IT"]
4+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import json
2+
import os
3+
4+
from django.core.management.base import BaseCommand
5+
from coldfront.core.school.models import School
6+
from coldfront.core.user.models import UserProfile, ApproverProfile
7+
from django.contrib.auth.models import User, Permission
8+
9+
app_commands_dir = os.path.dirname(__file__)
10+
11+
12+
def load_approver_schools(json_data):
13+
"""
14+
Grant is_staff, approver_profile, "can_review_allocation_requests" permission associated with schools
15+
"""
16+
for approver_username, school_descriptions in json_data.items():
17+
try:
18+
# Check if the user exists
19+
user = User.objects.get(username=approver_username)
20+
# Make as a staff to let an approver view admin navigation bar
21+
user.is_staff = True
22+
user.save()
23+
except User.DoesNotExist:
24+
# print(f"User {approver_username} not found. Skipping.")
25+
continue # Skip to the next user
26+
27+
# Get UserProfile for the user
28+
user_profile = UserProfile.objects.filter(user=user).first()
29+
30+
# Grant 'can_review_allocation_requests' permission if not already granted
31+
perm_codename = "can_review_allocation_requests"
32+
perm = Permission.objects.filter(codename=perm_codename).first()
33+
34+
if perm and not user.has_perm(f"allocation.{perm_codename}"):
35+
user.user_permissions.add(perm)
36+
user.save()
37+
# print(f"Granted '{perm_codename}' permission to {approver_username}")
38+
39+
# Ensure user is an approver
40+
if not user_profile.is_approver():
41+
# print(f"Skipping {approver_username}: User does not have approver permission.")
42+
continue
43+
44+
# Create ApproverProfile if it does not exist
45+
approver_profile, created = ApproverProfile.objects.get_or_create(user_profile=user_profile)
46+
47+
# Ensure all schools exist before assigning
48+
school_objects = [School.objects.get_or_create(description=desc)[0] for desc in school_descriptions]
49+
50+
# Update schools for the approver
51+
approver_profile.schools.set(school_objects)
52+
approver_profile.save()
53+
54+
# print(f"Updated {approver_username} with schools: {school_descriptions}")
55+
56+
57+
58+
class Command(BaseCommand):
59+
help = 'Import school data'
60+
61+
def handle(self, *args, **options):
62+
print('Adding schools ...')
63+
json_file_path = os.path.join(app_commands_dir, 'data', 'approver_schools_data.json')
64+
with open(json_file_path, "r") as file:
65+
json_data = json.load(file)
66+
load_approver_schools(json_data)
67+
68+
print('Finished adding approvers')
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import json
2+
from django.test import TestCase
3+
from django.contrib.auth.models import User, Permission
4+
from coldfront.core.user.models import UserProfile, ApproverProfile
5+
from coldfront.core.school.models import School
6+
from coldfront.core.user.management.commands.load_approver_schools import load_approver_schools
7+
8+
9+
class LoadApproverSchoolsTest(TestCase):
10+
"""Tests for load_approver_schools function."""
11+
12+
def setUp(self):
13+
"""Set up test users, schools, and permissions."""
14+
# Create users
15+
self.approver1 = User.objects.create(username="approver1", is_staff=False)
16+
self.approver2 = User.objects.create(username="approver2", is_staff=False)
17+
self.non_existent_user = "nonuser" # This user does not exist
18+
19+
# Get UserProfiles
20+
self.approver1_profile = UserProfile.objects.get(user=self.approver1)
21+
self.approver2_profile = UserProfile.objects.get(user=self.approver2)
22+
23+
# Create Schools
24+
self.school1 = School.objects.create(description="Tandon School of Engineering")
25+
self.school2 = School.objects.create(description="NYU IT")
26+
27+
# Create permission
28+
self.review_permission = Permission.objects.get(codename="can_review_allocation_requests")
29+
30+
# Sample JSON Data
31+
self.json_data = {
32+
"approver1": ["Tandon School of Engineering", "NYU IT"],
33+
"approver2": ["NYU IT"],
34+
"nonuser": ["NYU IT"] # This user does not exist
35+
}
36+
37+
def test_users_are_assigned_as_staff(self):
38+
"""Test that users are correctly marked as staff after function execution."""
39+
load_approver_schools(self.json_data)
40+
41+
self.approver1.refresh_from_db()
42+
self.approver2.refresh_from_db()
43+
44+
self.assertTrue(self.approver1.is_staff)
45+
self.assertTrue(self.approver2.is_staff)
46+
47+
def test_users_are_granted_approver_permission(self):
48+
"""Test that users receive the 'can_review_allocation_requests' permission."""
49+
load_approver_schools(self.json_data)
50+
51+
self.assertTrue(self.approver1.has_perm("allocation.can_review_allocation_requests"))
52+
self.assertTrue(self.approver2.has_perm("allocation.can_review_allocation_requests"))
53+
54+
def test_approver_profiles_are_created(self):
55+
"""Test that an ApproverProfile is created for approvers."""
56+
load_approver_schools(self.json_data)
57+
58+
self.assertTrue(ApproverProfile.objects.filter(user_profile=self.approver1_profile).exists())
59+
self.assertTrue(ApproverProfile.objects.filter(user_profile=self.approver2_profile).exists())
60+
61+
def test_schools_are_assigned_correctly(self):
62+
"""Test that users are assigned the correct schools in their ApproverProfile."""
63+
load_approver_schools(self.json_data)
64+
65+
approver1_profile = ApproverProfile.objects.get(user_profile=self.approver1_profile)
66+
approver2_profile = ApproverProfile.objects.get(user_profile=self.approver2_profile)
67+
68+
self.assertEqual(set(approver1_profile.schools.values_list("description", flat=True)),
69+
{"Tandon School of Engineering", "NYU IT"})
70+
self.assertEqual(set(approver2_profile.schools.values_list("description", flat=True)), {"NYU IT"})
71+
72+
def test_nonexistent_user_is_skipped(self):
73+
"""Test that non-existent users are skipped without error."""
74+
load_approver_schools(self.json_data)
75+
76+
# Ensure no UserProfile or ApproverProfile is created for the non-existent user
77+
self.assertFalse(UserProfile.objects.filter(user__username="jdoe").exists())
78+
self.assertFalse(ApproverProfile.objects.filter(user_profile__user__username="jdoe").exists())
79+
80+
def test_function_does_not_duplicate_existing_profiles(self):
81+
"""Test that the function does not create duplicate ApproverProfiles when run multiple times."""
82+
load_approver_schools(self.json_data) # First execution
83+
load_approver_schools(self.json_data) # Second execution
84+
85+
# Ensure only one ApproverProfile per user
86+
self.assertEqual(ApproverProfile.objects.filter(user_profile=self.approver1_profile).count(), 1)
87+
self.assertEqual(ApproverProfile.objects.filter(user_profile=self.approver2_profile).count(), 1)
88+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Generated by Django 4.2.11 on 2025-02-16 06:44
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
dependencies = [
9+
("school", "0001_initial"),
10+
("user", "0001_initial"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="ApproverProfile",
16+
fields=[
17+
(
18+
"id",
19+
models.AutoField(
20+
auto_created=True,
21+
primary_key=True,
22+
serialize=False,
23+
verbose_name="ID",
24+
),
25+
),
26+
("schools", models.ManyToManyField(blank=True, to="school.school")),
27+
(
28+
"user_profile",
29+
models.OneToOneField(
30+
on_delete=django.db.models.deletion.CASCADE,
31+
related_name="approver_profile",
32+
to="user.userprofile",
33+
),
34+
),
35+
],
36+
),
37+
]

coldfront/core/user/models.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.contrib.auth.models import User
22
from django.db import models
3+
from coldfront.core.school.models import School
34

45

56
class UserProfile(models.Model):
@@ -11,4 +12,33 @@ class UserProfile(models.Model):
1112
"""
1213

1314
user = models.OneToOneField(User, on_delete=models.CASCADE)
14-
is_pi = models.BooleanField(default=False)
15+
is_pi = models.BooleanField(default=False)
16+
17+
def is_approver(self):
18+
"""Checks if the user has the 'can_review_allocation_requests' permission."""
19+
return self.user.has_perm('allocation.can_review_allocation_requests')
20+
21+
@property
22+
def schools(self):
23+
"""Get schools from ApproverProfile if the user is an approver."""
24+
default_schools = School.objects.none()
25+
if not hasattr(self, 'approver_profile'):
26+
return default_schools
27+
return self.approver_profile.schools.all()
28+
29+
@schools.setter
30+
def schools(self, values):
31+
"""Set schools in ApproverProfile if the user is an approver."""
32+
if self.is_approver():
33+
approver_profile, created = ApproverProfile.objects.get_or_create(user_profile=self)
34+
approver_profile.schools.set(School.objects.filter(description__in=values))
35+
else:
36+
raise ValueError("User is not an approver, cannot set schools.")
37+
38+
39+
class ApproverProfile(models.Model):
40+
"""Stores additional information for approvers."""
41+
user_profile = models.OneToOneField(UserProfile, on_delete=models.CASCADE, related_name="approver_profile")
42+
schools = models.ManyToManyField(School, blank=True)
43+
44+

coldfront/core/user/tests.py

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,31 @@
11
from coldfront.core.test_helpers.factories import UserFactory
2+
from coldfront.core.user.models import UserProfile, ApproverProfile
3+
from coldfront.core.school.models import School
24
from django.test import TestCase
3-
4-
5-
from coldfront.core.user.models import UserProfile
5+
from django.contrib.auth.models import Permission
66

77
class TestUserProfile(TestCase):
88
class Data:
99
"""Collection of test data, separated for readability"""
1010

1111
def __init__(self):
12-
user = UserFactory(username='submitter')
12+
self.user = UserFactory(username='approver_user')
13+
self.non_approver_user = UserFactory(username='regular_user')
14+
15+
self.user_profile, _ = UserProfile.objects.get_or_create(user=self.user)
16+
self.non_approver_profile, _ = UserProfile.objects.get_or_create(user=self.non_approver_user)
17+
18+
self.permission = Permission.objects.get(codename="can_review_allocation_requests")
19+
self.user.user_permissions.add(self.permission)
20+
21+
self.school1 = School.objects.create(description="Tandon School of Engineering")
22+
self.school2 = School.objects.create(description="NYU IT")
23+
self.school3 = School.objects.create(description="Arts & Science")
1324

1425
self.initial_fields = {
15-
'user': user,
16-
'is_pi': True,
17-
'id': user.id
26+
'user': self.user,
27+
'is_pi': False,
28+
'id': self.user.id
1829
}
1930

2031
self.unsaved_object = UserProfile(**self.initial_fields)
@@ -23,10 +34,12 @@ def setUp(self):
2334
self.data = self.Data()
2435

2536
def test_fields_generic(self):
37+
"""Test that UserProfile fields are correctly saved and retrieved."""
2638
profile_obj = self.data.unsaved_object
2739
profile_obj.save()
2840

29-
self.assertEqual(1, len(UserProfile.objects.all()))
41+
# Ensure only one UserProfile exists for this user
42+
self.assertEqual(1, UserProfile.objects.filter(user=self.data.user).count())
3043

3144
retrieved_profile = UserProfile.objects.get(pk=profile_obj.pk)
3245

@@ -38,14 +51,63 @@ def test_fields_generic(self):
3851
self.assertEqual(profile_obj, retrieved_profile)
3952

4053
def test_user_on_delete(self):
54+
"""Test that deleting a User also deletes the related UserProfile (CASCADE)."""
4155
profile_obj = self.data.unsaved_object
4256
profile_obj.save()
4357

44-
self.assertEqual(1, len(UserProfile.objects.all()))
58+
# Ensure only the specific user's UserProfile is considered
59+
self.assertEqual(1, UserProfile.objects.filter(user=self.data.user).count())
4560

4661
profile_obj.user.delete()
4762

4863
# expecting CASCADE
4964
with self.assertRaises(UserProfile.DoesNotExist):
5065
UserProfile.objects.get(pk=profile_obj.pk)
51-
self.assertEqual(0, len(UserProfile.objects.all()))
66+
67+
# Only this user's UserProfile should be deleted
68+
self.assertEqual(0, UserProfile.objects.filter(user=self.data.user).count())
69+
70+
def test_is_approver(self):
71+
"""Test if a user is correctly identified as an approver."""
72+
self.assertTrue(self.data.user_profile.is_approver())
73+
self.assertFalse(self.data.non_approver_profile.is_approver())
74+
75+
def test_approver_profile_creation(self):
76+
"""Test that ApproverProfile is created automatically when an approver sets schools."""
77+
self.assertFalse(ApproverProfile.objects.filter(user_profile=self.data.user_profile).exists())
78+
79+
# Assign schools to the approver
80+
self.data.user_profile.schools = ["Tandon School of Engineering", "NYU IT"]
81+
82+
# Ensure ApproverProfile is created
83+
self.assertTrue(ApproverProfile.objects.filter(user_profile=self.data.user_profile).exists())
84+
85+
# Convert both lists to sets to ignore order
86+
expected_schools = {"Tandon School of Engineering", "NYU IT"}
87+
actual_schools = {school.description for school in self.data.user_profile.schools}
88+
89+
self.assertEqual(expected_schools, actual_schools)
90+
91+
def test_non_approver_cannot_set_schools(self):
92+
"""Test that a non-approver cannot set schools and raises ValueError."""
93+
with self.assertRaises(ValueError):
94+
self.data.non_approver_profile.schools = ["Tandon School of Engineering"]
95+
96+
def test_schools_property_getter(self):
97+
"""Test getting the list of schools for an approver (order-independent)."""
98+
approver_profile = ApproverProfile.objects.create(user_profile=self.data.user_profile)
99+
approver_profile.schools.set([self.data.school1, self.data.school2])
100+
101+
# Convert both lists to sets to ignore order
102+
expected_schools = {"Tandon School of Engineering", "NYU IT"}
103+
actual_schools = {school.description for school in self.data.user_profile.schools}
104+
105+
self.assertEqual(expected_schools, actual_schools)
106+
107+
def test_schools_property_setter(self):
108+
"""Test setting schools through the schools property."""
109+
self.data.user_profile.schools = ["Arts & Science"]
110+
approver_profile = ApproverProfile.objects.get(user_profile=self.data.user_profile)
111+
112+
self.assertEqual(list(approver_profile.schools.values_list('description', flat=True)),
113+
["Arts & Science"])

coldfront/core/utils/management/commands/load_test_data.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@
2121
from coldfront.core.resource.models import (Resource, ResourceAttribute,
2222
ResourceAttributeType,
2323
ResourceType)
24+
from coldfront.core.user.management.commands.load_approver_schools import load_approver_schools
2425

2526
base_dir = settings.BASE_DIR
2627

2728
# first, last
2829
Users = ['Carl Gray', # PI#1
2930
'Stephanie Foster', # PI#2
3031
'Charles Simmons', # Director
31-
'Andrea Stewart',
32-
'Alice Rivera',
32+
'Andrea Stewart', # Approver#1
33+
'Alice Rivera', # Approver#2
3334
'Frank Hernandez',
3435
'Justin James',
3536
'Randy Perry',
@@ -158,6 +159,12 @@ def handle(self, *args, **options):
158159
email=email.strip()
159160
)
160161

162+
json_data = {
163+
"astewart": ["Tandon School of Engineering", "NYU IT"],
164+
"arivera": ["NYU IT"]
165+
}
166+
load_approver_schools(json_data)
167+
161168
admin_user, _ = User.objects.get_or_create(username='admin')
162169
admin_user.is_superuser = True
163170
admin_user.is_staff = True

0 commit comments

Comments
 (0)