diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 8426b7e40..ccdb7be3e 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -4,11 +4,43 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.contenttypes.models import ContentType from django.http.response import HttpResponseRedirect from django.urls import reverse +# from django.forms.widgets import CheckboxSelectMultiple +# from django import forms from . import models logger = logging.getLogger(__name__) +# class TextDisplayWidget(forms.Widget): +# def render(self, name, value, attrs=None, renderer=None): +# if value: +# choices_dict = dict(self.choices) +# selected_values = set(value) +# display_values = [choices_dict[val] for val in selected_values] +# return ', '.join(display_values) +# return '' + + +# class DomainApplicationForm(forms.ModelForm): +# class Meta: +# model = models.DomainApplication +# fields = '__all__' + +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) + +# # Replace the current_websites field with text if a condition is met +# if self.instance and self.instance.creator.status == 'ineligible': +# self.fields['current_websites'].widget = TextDisplayWidget() +# self.fields['current_websites'].widget.choices = self.fields['current_websites'].choices + +# self.fields['other_contacts'].widget = TextDisplayWidget() +# self.fields['other_contacts'].widget.choices = self.fields['other_contacts'].choices + +# self.fields['alternative_domains'].widget = TextDisplayWidget() +# self.fields['alternative_domains'].widget.choices = self.fields['alternative_domains'].choices + + class AuditedAdmin(admin.ModelAdmin): """Custom admin to make auditing easier.""" @@ -181,6 +213,10 @@ class ContactAdmin(ListHeaderAdmin): class DomainApplicationAdmin(ListHeaderAdmin): """Customize the applications listing view.""" + + # Set multi-selects 'read-only' (hide selects and show data) + # based on user perms and application creator's status + #form = DomainApplicationForm # Columns list_display = [ @@ -255,7 +291,7 @@ class DomainApplicationAdmin(ListHeaderAdmin): ] # Read only that we'll leverage for CISA Analysts - readonly_fields = [ + analyst_readonly_fields = [ "creator", "type_of_work", "more_organization_information", @@ -273,52 +309,72 @@ class DomainApplicationAdmin(ListHeaderAdmin): # Trigger action when a fieldset is changed def save_model(self, request, obj, form, change): - if change: # Check if the application is being edited - # Get the original application from the database - original_obj = models.DomainApplication.objects.get(pk=obj.pk) + if obj and obj.creator.status != "ineligible": + if change: # Check if the application is being edited + # Get the original application from the database + original_obj = models.DomainApplication.objects.get(pk=obj.pk) - if obj.status != original_obj.status: - if obj.status == models.DomainApplication.STARTED: - # No conditions - pass - elif obj.status == models.DomainApplication.SUBMITTED: - # This is an fsm in model which will throw an error if the - # transition condition is violated, so we roll back the - # status to what it was before the admin user changed it and - # let the fsm method set it. Same comment applies to - # transition method calls below. - obj.status = original_obj.status - obj.submit() - elif obj.status == models.DomainApplication.IN_REVIEW: - obj.status = original_obj.status - obj.in_review() - elif obj.status == models.DomainApplication.ACTION_NEEDED: - obj.status = original_obj.status - obj.action_needed() - elif obj.status == models.DomainApplication.APPROVED: - obj.status = original_obj.status - obj.approve() - elif obj.status == models.DomainApplication.WITHDRAWN: - obj.status = original_obj.status - obj.withdraw() - elif obj.status == models.DomainApplication.REJECTED: - obj.status = original_obj.status - obj.reject() - elif obj.status == models.DomainApplication.INELIGIBLE: - obj.status = original_obj.status - obj.reject_with_prejudice() - else: - logger.warning("Unknown status selected in django admin") + if obj.status != original_obj.status: + if obj.status == models.DomainApplication.STARTED: + # No conditions + pass + elif obj.status == models.DomainApplication.SUBMITTED: + # This is an fsm in model which will throw an error if the + # transition condition is violated, so we roll back the + # status to what it was before the admin user changed it and + # let the fsm method set it. Same comment applies to + # transition method calls below. + obj.status = original_obj.status + obj.submit() + elif obj.status == models.DomainApplication.IN_REVIEW: + obj.status = original_obj.status + obj.in_review() + elif obj.status == models.DomainApplication.ACTION_NEEDED: + obj.status = original_obj.status + obj.action_needed() + elif obj.status == models.DomainApplication.APPROVED: + obj.status = original_obj.status + obj.approve() + elif obj.status == models.DomainApplication.WITHDRAWN: + obj.status = original_obj.status + obj.withdraw() + elif obj.status == models.DomainApplication.REJECTED: + obj.status = original_obj.status + obj.reject() + elif obj.status == models.DomainApplication.INELIGIBLE: + obj.status = original_obj.status + obj.reject_with_prejudice() + else: + logger.warning("Unknown status selected in django admin") - super().save_model(request, obj, form, change) + super().save_model(request, obj, form, change) + else: + messages.error(request, "This action is not permitted for applications with an ineligible creator.") def get_readonly_fields(self, request, obj=None): + + """Set the read-only state on form elements. + We have 2 conditions that determine which fields are read-only: + admin user permissions and the application creator's status, so + we'll use the baseline readonly_fields and extend it as needed. + """ + + readonly_fields = list(self.readonly_fields) + + # Check if the creator is ineligible + if obj and obj.creator.status == "ineligible": + # For fields like CharField, IntegerField, etc., the widget used is straightforward, + # and the readonly_fields list can control their behavior + readonly_fields.extend([field.name for field in self.model._meta.fields]) + # Add the multi-select fields to readonly_fields: + # Complex fields like ManyToManyField require special handling + readonly_fields.extend(["current_websites", "other_contacts", "alternative_domains"]) + if request.user.is_superuser: - # Superusers have full access, no fields are read-only - return [] + return readonly_fields else: - # Regular users can only view the specified fields - return self.readonly_fields + readonly_fields.extend([field for field in self.analyst_readonly_fields]) + return readonly_fields admin.site.register(models.User, MyUserAdmin) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index cc860e41c..6b6be1bd5 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -4,6 +4,7 @@ from registrar.admin import DomainApplicationAdmin, ListHeaderAdmin, MyUserAdmin from registrar.models import DomainApplication, DomainInformation, User from .common import completed_application, mock_user, create_superuser, create_user from django.contrib.auth import get_user_model +from unittest.mock import patch from django.conf import settings from unittest.mock import MagicMock @@ -14,6 +15,9 @@ class TestDomainApplicationAdmin(TestCase): def setUp(self): self.site = AdminSite() self.factory = RequestFactory() + self.admin = DomainApplicationAdmin(model=DomainApplication, admin_site=self.site) + self.superuser = create_superuser() + self.staffuser = create_user() @boto3_mocking.patching def test_save_model_sends_submitted_email(self): @@ -33,14 +37,11 @@ class TestDomainApplicationAdmin(TestCase): "/admin/registrar/domainapplication/{}/change/".format(application.pk) ) - # Create an instance of the model admin - model_admin = DomainApplicationAdmin(DomainApplication, self.site) - # Modify the application's property application.status = DomainApplication.SUBMITTED # Use the model admin's save_model method - model_admin.save_model(request, application, form=None, change=True) + self.admin.save_model(request, application, form=None, change=True) # Access the arguments passed to send_email call_args = mock_client_instance.send_email.call_args @@ -79,14 +80,11 @@ class TestDomainApplicationAdmin(TestCase): "/admin/registrar/domainapplication/{}/change/".format(application.pk) ) - # Create an instance of the model admin - model_admin = DomainApplicationAdmin(DomainApplication, self.site) - # Modify the application's property application.status = DomainApplication.IN_REVIEW # Use the model admin's save_model method - model_admin.save_model(request, application, form=None, change=True) + self.admin.save_model(request, application, form=None, change=True) # Access the arguments passed to send_email call_args = mock_client_instance.send_email.call_args @@ -125,14 +123,11 @@ class TestDomainApplicationAdmin(TestCase): "/admin/registrar/domainapplication/{}/change/".format(application.pk) ) - # Create an instance of the model admin - model_admin = DomainApplicationAdmin(DomainApplication, self.site) - # Modify the application's property application.status = DomainApplication.APPROVED # Use the model admin's save_model method - model_admin.save_model(request, application, form=None, change=True) + self.admin.save_model(request, application, form=None, change=True) # Access the arguments passed to send_email call_args = mock_client_instance.send_email.call_args @@ -166,14 +161,11 @@ class TestDomainApplicationAdmin(TestCase): "/admin/registrar/domainapplication/{}/change/".format(application.pk) ) - # Create an instance of the model admin - model_admin = DomainApplicationAdmin(DomainApplication, self.site) - # Modify the application's property application.status = DomainApplication.APPROVED # Use the model admin's save_model method - model_admin.save_model(request, application, form=None, change=True) + self.admin.save_model(request, application, form=None, change=True) # Test that approved domain exists and equals requested domain self.assertEqual( @@ -198,14 +190,11 @@ class TestDomainApplicationAdmin(TestCase): "/admin/registrar/domainapplication/{}/change/".format(application.pk) ) - # Create an instance of the model admin - model_admin = DomainApplicationAdmin(DomainApplication, self.site) - # Modify the application's property application.status = DomainApplication.ACTION_NEEDED # Use the model admin's save_model method - model_admin.save_model(request, application, form=None, change=True) + self.admin.save_model(request, application, form=None, change=True) # Access the arguments passed to send_email call_args = mock_client_instance.send_email.call_args @@ -247,14 +236,11 @@ class TestDomainApplicationAdmin(TestCase): "/admin/registrar/domainapplication/{}/change/".format(application.pk) ) - # Create an instance of the model admin - model_admin = DomainApplicationAdmin(DomainApplication, self.site) - # Modify the application's property application.status = DomainApplication.REJECTED # Use the model admin's save_model method - model_admin.save_model(request, application, form=None, change=True) + self.admin.save_model(request, application, form=None, change=True) # Access the arguments passed to send_email call_args = mock_client_instance.send_email.call_args @@ -288,19 +274,70 @@ class TestDomainApplicationAdmin(TestCase): "/admin/registrar/domainapplication/{}/change/".format(application.pk) ) - # Create an instance of the model admin - model_admin = DomainApplicationAdmin(DomainApplication, self.site) - # Modify the application's property application.status = DomainApplication.INELIGIBLE # Use the model admin's save_model method - model_admin.save_model(request, application, form=None, change=True) + self.admin.save_model(request, application, form=None, change=True) # Test that approved domain exists and equals requested domain self.assertEqual( application.creator.status, "ineligible" ) + + def test_readonly_when_ineligible_creator(self): + application = completed_application(status=DomainApplication.IN_REVIEW) + application.creator.status = 'ineligible' + application.creator.save() + + request = self.factory.get('/') + request.user = self.superuser + + readonly_fields = self.admin.get_readonly_fields(request, application) + + expected_fields = ['id', 'created_at', 'updated_at', 'status', 'creator', 'investigator', 'organization_type', 'federally_recognized_tribe', 'state_recognized_tribe', 'tribe_name', 'federal_agency', 'federal_type', 'is_election_board', 'organization_name', 'address_line1', 'address_line2', 'city', 'state_territory', 'zipcode', 'urbanization', 'type_of_work', 'more_organization_information', 'authorizing_official', 'approved_domain', 'requested_domain', 'submitter', 'purpose', 'no_other_contacts_rationale', 'anything_else', 'is_policy_acknowledged', 'current_websites', 'other_contacts', 'alternative_domains'] + + self.assertEqual(readonly_fields, expected_fields) + + def test_readonly_fields_for_analyst(self): + request = self.factory.get('/') # Use the correct method and path + request.user = self.staffuser + + readonly_fields = self.admin.get_readonly_fields(request) + + expected_fields = ['creator', 'type_of_work', 'more_organization_information', 'address_line1', 'address_line2', 'zipcode', 'requested_domain', 'alternative_domains', 'purpose', 'submitter', 'no_other_contacts_rationale', 'anything_else', 'is_policy_acknowledged'] + + self.assertEqual(readonly_fields, expected_fields) + + def test_readonly_fields_for_superuser(self): + request = self.factory.get('/') # Use the correct method and path + request.user = self.superuser + + readonly_fields = self.admin.get_readonly_fields(request) + + expected_fields = [] + + self.assertEqual(readonly_fields, expected_fields) + + def test_saving_when_ineligible_creator(self): + # Create an instance of the model + application = completed_application(status=DomainApplication.IN_REVIEW) + application.creator.status = 'ineligible' + application.creator.save() + + # Create a request object with a superuser + request = self.factory.get('/') + request.user = self.superuser + + with patch('django.contrib.messages.error') as mock_error: + # Simulate saving the model + self.admin.save_model(request, application, None, False) + + # Assert that the error message was called with the correct argument + mock_error.assert_called_once_with(request, "This action is not permitted for applications with an ineligible creator.") + + # Assert that the status has not changed + self.assertEqual(application.status, DomainApplication.IN_REVIEW) def tearDown(self): DomainInformation.objects.all().delete() @@ -381,8 +418,7 @@ class ListHeaderAdminTest(TestCase): DomainInformation.objects.all().delete() DomainApplication.objects.all().delete() User.objects.all().delete() - self.superuser.delete() - + class MyUserAdminTest(TestCase): def setUp(self):