Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"astewart": ["Tandon School of Engineering", "NYU IT"],
"arivera": ["NYU IT"]
}
65 changes: 65 additions & 0 deletions coldfront/core/user/management/commands/load_approver_schools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import json
import os

from django.core.management.base import BaseCommand
from coldfront.core.school.models import School
from coldfront.core.user.models import UserProfile, ApproverProfile
from django.contrib.auth.models import User, Permission

app_commands_dir = os.path.dirname(__file__)


def load_approver_schools(json_data):
for approver_username, school_descriptions in json_data.items():
try:
# Check if the user exists
user = User.objects.get(username=approver_username)
except User.DoesNotExist:
print(f"User {approver_username} not found. Skipping.")
continue # Skip to the next user

# Check if a UserProfile exists for the user
user_profile = UserProfile.objects.filter(user=user).first()
if not user_profile:
print(f"Skipping {approver_username}: UserProfile does not exist.")
continue

# Grant 'can_review_allocation_requests' permission if not already granted
perm_codename = "can_review_allocation_requests"
perm = Permission.objects.filter(codename=perm_codename).first()

if perm and not user.has_perm(f"allocation.{perm_codename}"):
user.user_permissions.add(perm)
user.save()
print(f"Granted '{perm_codename}' permission to {approver_username}")

# Ensure user is an approver
if not user_profile.is_approver():
print(f"Skipping {approver_username}: User does not have approver permission.")
continue

# Create ApproverProfile if it does not exist
approver_profile, created = ApproverProfile.objects.get_or_create(user_profile=user_profile)

# Ensure all schools exist before assigning
school_objects = [School.objects.get_or_create(description=desc)[0] for desc in school_descriptions]

# Update schools for the approver
approver_profile.schools.set(school_objects)
approver_profile.save()

print(f"Updated {approver_username} with schools: {school_descriptions}")



class Command(BaseCommand):
help = 'Import school data'

def handle(self, *args, **options):
print('Adding schools ...')
json_file_path = os.path.join(app_commands_dir, 'data', 'approver_schools_data.json')
with open(json_file_path, "r") as file:
json_data = json.load(file)
load_approver_schools(json_data)

print('Finished adding approvers')
37 changes: 37 additions & 0 deletions coldfront/core/user/migrations/0002_approverprofile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated by Django 4.2.11 on 2025-02-16 06:44

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


class Migration(migrations.Migration):
dependencies = [
("school", "0001_initial"),
("user", "0001_initial"),
]

operations = [
migrations.CreateModel(
name="ApproverProfile",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("schools", models.ManyToManyField(blank=True, to="school.school")),
(
"user_profile",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="approver_profile",
to="user.userprofile",
),
),
],
),
]
31 changes: 30 additions & 1 deletion coldfront/core/user/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.contrib.auth.models import User
from django.db import models
from coldfront.core.school.models import School


class UserProfile(models.Model):
Expand All @@ -11,4 +12,32 @@ class UserProfile(models.Model):
"""

user = models.OneToOneField(User, on_delete=models.CASCADE)
is_pi = models.BooleanField(default=False)
is_pi = models.BooleanField(default=False)

def is_approver(self):
"""Checks if the user has the 'can_review_allocation_requests' permission."""
return self.user.has_perm('allocation.can_review_allocation_requests')

@property
def schools(self):
"""Get schools from ApproverProfile if the user is an approver."""
if hasattr(self, 'approver_profile'):
return self.approver_profile.schools.all()
return School.objects.none()

@schools.setter
def schools(self, values):
"""Set schools in ApproverProfile if the user is an approver."""
if self.is_approver():
approver_profile, created = ApproverProfile.objects.get_or_create(user_profile=self)
approver_profile.schools.set(School.objects.filter(description__in=values))
else:
raise ValueError("User is not an approver, cannot set schools.")


class ApproverProfile(models.Model):
"""Stores additional information for approvers."""
user_profile = models.OneToOneField(UserProfile, on_delete=models.CASCADE, related_name="approver_profile")
schools = models.ManyToManyField(School, blank=True)


82 changes: 72 additions & 10 deletions coldfront/core/user/tests.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
from coldfront.core.test_helpers.factories import UserFactory
from coldfront.core.user.models import UserProfile, ApproverProfile
from coldfront.core.school.models import School
from django.test import TestCase


from coldfront.core.user.models import UserProfile
from django.contrib.auth.models import Permission

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

def __init__(self):
user = UserFactory(username='submitter')
self.user = UserFactory(username='approver_user')
self.non_approver_user = UserFactory(username='regular_user')

self.user_profile, _ = UserProfile.objects.get_or_create(user=self.user)
self.non_approver_profile, _ = UserProfile.objects.get_or_create(user=self.non_approver_user)

self.permission = Permission.objects.get(codename="can_review_allocation_requests")
self.user.user_permissions.add(self.permission)

self.school1 = School.objects.create(description="Tandon School of Engineering")
self.school2 = School.objects.create(description="NYU IT")
self.school3 = School.objects.create(description="Arts & Science")

self.initial_fields = {
'user': user,
'is_pi': True,
'id': user.id
'user': self.user,
'is_pi': False,
'id': self.user.id
}

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

def test_fields_generic(self):
"""Test that UserProfile fields are correctly saved and retrieved."""
profile_obj = self.data.unsaved_object
profile_obj.save()

self.assertEqual(1, len(UserProfile.objects.all()))
# Ensure only one UserProfile exists for this user
self.assertEqual(1, UserProfile.objects.filter(user=self.data.user).count())

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

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

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

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

profile_obj.user.delete()

# expecting CASCADE
with self.assertRaises(UserProfile.DoesNotExist):
UserProfile.objects.get(pk=profile_obj.pk)
self.assertEqual(0, len(UserProfile.objects.all()))

# Only this user's UserProfile should be deleted
self.assertEqual(0, UserProfile.objects.filter(user=self.data.user).count())

def test_is_approver(self):
"""Test if a user is correctly identified as an approver."""
self.assertTrue(self.data.user_profile.is_approver())
self.assertFalse(self.data.non_approver_profile.is_approver())

def test_approver_profile_creation(self):
"""Test that ApproverProfile is created automatically when an approver sets schools."""
self.assertFalse(ApproverProfile.objects.filter(user_profile=self.data.user_profile).exists())

# Assign schools to the approver
self.data.user_profile.schools = ["Tandon School of Engineering", "NYU IT"]

# Ensure ApproverProfile is created
self.assertTrue(ApproverProfile.objects.filter(user_profile=self.data.user_profile).exists())

# Convert both lists to sets to ignore order
expected_schools = {"Tandon School of Engineering", "NYU IT"}
actual_schools = {school.description for school in self.data.user_profile.schools}

self.assertEqual(expected_schools, actual_schools)

def test_non_approver_cannot_set_schools(self):
"""Test that a non-approver cannot set schools and raises ValueError."""
with self.assertRaises(ValueError):
self.data.non_approver_profile.schools = ["Tandon School of Engineering"]

def test_schools_property_getter(self):
"""Test getting the list of schools for an approver (order-independent)."""
approver_profile = ApproverProfile.objects.create(user_profile=self.data.user_profile)
approver_profile.schools.set([self.data.school1, self.data.school2])

# Convert both lists to sets to ignore order
expected_schools = {"Tandon School of Engineering", "NYU IT"}
actual_schools = {school.description for school in self.data.user_profile.schools}

self.assertEqual(expected_schools, actual_schools)

def test_schools_property_setter(self):
"""Test setting schools through the schools property."""
self.data.user_profile.schools = ["Arts & Science"]
approver_profile = ApproverProfile.objects.get(user_profile=self.data.user_profile)

self.assertEqual(list(approver_profile.schools.values_list('description', flat=True)),
["Arts & Science"])
11 changes: 9 additions & 2 deletions coldfront/core/utils/management/commands/load_test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@
from coldfront.core.resource.models import (Resource, ResourceAttribute,
ResourceAttributeType,
ResourceType)
from coldfront.core.user.management.commands.load_approver_schools import load_approver_schools

base_dir = settings.BASE_DIR

# first, last
Users = ['Carl Gray', # PI#1
'Stephanie Foster', # PI#2
'Charles Simmons', # Director
'Andrea Stewart',
'Alice Rivera',
'Andrea Stewart', # Approver#1
'Alice Rivera', # Approver#2
'Frank Hernandez',
'Justin James',
'Randy Perry',
Expand Down Expand Up @@ -151,6 +152,12 @@ def handle(self, *args, **options):
email=email.strip()
)

json_data = {
"astewart": ["Tandon School of Engineering", "NYU IT"],
"arivera": ["NYU IT"]
}
load_approver_schools(json_data)

admin_user, _ = User.objects.get_or_create(username='admin')
admin_user.is_superuser = True
admin_user.is_staff = True
Expand Down
3 changes: 3 additions & 0 deletions coldfront/plugins/mokey_oidc/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
ALLOWED_GROUPS = import_from_settings('MOKEY_OIDC_ALLOWED_GROUPS', [])
DENY_GROUPS = import_from_settings('MOKEY_OIDC_DENY_GROUPS', [])

"""
See if school is available from claims. Otherwise, we'll find another way to assign school to user
"""
class OIDCMokeyAuthenticationBackend(OIDCAuthenticationBackend):

def _sync_groups(self, user, groups):
Expand Down
Loading