This commit is contained in:
rachidatecs 2023-08-24 19:09:57 -04:00
parent 4b82f5e131
commit 91d1b9c1cc
No known key found for this signature in database
GPG key ID: 3CEBBFA7325E5525
10 changed files with 218 additions and 155 deletions

View file

@ -4,43 +4,12 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.http.response import HttpResponseRedirect from django.http.response import HttpResponseRedirect
from django.urls import reverse from django.urls import reverse
# from django.forms.widgets import CheckboxSelectMultiple
# from django import forms
from . import models from . import models
logger = logging.getLogger(__name__) 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): class AuditedAdmin(admin.ModelAdmin):
"""Custom admin to make auditing easier.""" """Custom admin to make auditing easier."""
@ -131,13 +100,34 @@ class MyUserAdmin(BaseUserAdmin):
inlines = [UserContactInline] inlines = [UserContactInline]
list_display = ("email", "first_name", "last_name", "is_staff", "is_superuser", "status") list_display = (
"email",
"first_name",
"last_name",
"is_staff",
"is_superuser",
"status",
)
fieldsets = ( fieldsets = (
(None, {'fields': ('username', 'password', 'status')}), # Add the 'status' field here (
('Personal Info', {'fields': ('first_name', 'last_name', 'email')}), None,
('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}), {"fields": ("username", "password", "status")},
('Important dates', {'fields': ('last_login', 'date_joined')}), ), # Add the 'status' field here
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
(
"Permissions",
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
)
},
),
("Important dates", {"fields": ("last_login", "date_joined")}),
) )
def get_list_display(self, request): def get_list_display(self, request):
@ -314,45 +304,66 @@ class DomainApplicationAdmin(ListHeaderAdmin):
# Get the original application from the database # Get the original application from the database
original_obj = models.DomainApplication.objects.get(pk=obj.pk) 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 != original_obj.status:
if obj.status == models.DomainApplication.STARTED: status_method_mapping = {
# No conditions models.DomainApplication.STARTED: None,
pass models.DomainApplication.SUBMITTED: obj.submit,
elif obj.status == models.DomainApplication.SUBMITTED: models.DomainApplication.IN_REVIEW: obj.in_review,
# This is an fsm in model which will throw an error if the models.DomainApplication.ACTION_NEEDED: obj.action_needed,
# transition condition is violated, so we roll back the models.DomainApplication.APPROVED: obj.approve,
# status to what it was before the admin user changed it and models.DomainApplication.WITHDRAWN: obj.withdraw,
# let the fsm method set it. Same comment applies to models.DomainApplication.REJECTED: obj.reject,
# transition method calls below. models.DomainApplication.INELIGIBLE: obj.reject_with_prejudice,
obj.status = original_obj.status }
obj.submit() selected_method = status_method_mapping.get(obj.status)
elif obj.status == models.DomainApplication.IN_REVIEW: if selected_method is None:
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") logger.warning("Unknown status selected in django admin")
else:
obj.status = original_obj.status
selected_method()
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
else: else:
messages.error(request, "This action is not permitted for applications with an ineligible creator.") messages.error(
request,
"This action is not permitted for applications "
+ "with an ineligible creator.",
)
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
"""Set the read-only state on form elements. """Set the read-only state on form elements.
We have 2 conditions that determine which fields are read-only: We have 2 conditions that determine which fields are read-only:
admin user permissions and the application creator's status, so admin user permissions and the application creator's status, so
@ -363,12 +374,14 @@ class DomainApplicationAdmin(ListHeaderAdmin):
# Check if the creator is ineligible # Check if the creator is ineligible
if obj and obj.creator.status == "ineligible": if obj and obj.creator.status == "ineligible":
# For fields like CharField, IntegerField, etc., the widget used is straightforward, # For fields like CharField, IntegerField, etc., the widget used is
# and the readonly_fields list can control their behavior # straightforward and the readonly_fields list can control their behavior
readonly_fields.extend([field.name for field in self.model._meta.fields]) readonly_fields.extend([field.name for field in self.model._meta.fields])
# Add the multi-select fields to readonly_fields: # Add the multi-select fields to readonly_fields:
# Complex fields like ManyToManyField require special handling # Complex fields like ManyToManyField require special handling
readonly_fields.extend(["current_websites", "other_contacts", "alternative_domains"]) readonly_fields.extend(
["current_websites", "other_contacts", "alternative_domains"]
)
if request.user.is_superuser: if request.user.is_superuser:
return readonly_fields return readonly_fields

View file

@ -35,7 +35,7 @@ class DomainApplication(TimeStampedModel):
(APPROVED, APPROVED), (APPROVED, APPROVED),
(WITHDRAWN, WITHDRAWN), (WITHDRAWN, WITHDRAWN),
(REJECTED, REJECTED), (REJECTED, REJECTED),
(INELIGIBLE, INELIGIBLE) (INELIGIBLE, INELIGIBLE),
] ]
class StateTerritoryChoices(models.TextChoices): class StateTerritoryChoices(models.TextChoices):
@ -556,7 +556,9 @@ class DomainApplication(TimeStampedModel):
) )
@transition( @transition(
field="status", source=[SUBMITTED, IN_REVIEW, REJECTED, INELIGIBLE], target=APPROVED field="status",
source=[SUBMITTED, IN_REVIEW, REJECTED, INELIGIBLE],
target=APPROVED,
) )
def approve(self): def approve(self):
"""Approve an application that has been submitted. """Approve an application that has been submitted.
@ -616,8 +618,6 @@ class DomainApplication(TimeStampedModel):
self.creator.block_user() self.creator.block_user()
# ## Form policies ### # ## Form policies ###
# #
# These methods control what questions need to be answered by applicants # These methods control what questions need to be answered by applicants

View file

@ -18,10 +18,8 @@ class User(AbstractUser):
""" """
# #### Constants for choice fields #### # #### Constants for choice fields ####
INELIGIBLE = 'ineligible' INELIGIBLE = "ineligible"
STATUS_CHOICES = ( STATUS_CHOICES = ((INELIGIBLE, INELIGIBLE),)
(INELIGIBLE, INELIGIBLE),
)
status = models.CharField( status = models.CharField(
max_length=10, max_length=10,

View file

@ -15,7 +15,9 @@ class TestDomainApplicationAdmin(TestCase):
def setUp(self): def setUp(self):
self.site = AdminSite() self.site = AdminSite()
self.factory = RequestFactory() self.factory = RequestFactory()
self.admin = DomainApplicationAdmin(model=DomainApplication, admin_site=self.site) self.admin = DomainApplicationAdmin(
model=DomainApplication, admin_site=self.site
)
self.superuser = create_superuser() self.superuser = create_superuser()
self.staffuser = create_user() self.staffuser = create_user()
@ -281,36 +283,82 @@ class TestDomainApplicationAdmin(TestCase):
self.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 # Test that approved domain exists and equals requested domain
self.assertEqual( self.assertEqual(application.creator.status, "ineligible")
application.creator.status, "ineligible"
)
def test_readonly_when_ineligible_creator(self): def test_readonly_when_ineligible_creator(self):
application = completed_application(status=DomainApplication.IN_REVIEW) application = completed_application(status=DomainApplication.IN_REVIEW)
application.creator.status = 'ineligible' application.creator.status = "ineligible"
application.creator.save() application.creator.save()
request = self.factory.get('/') request = self.factory.get("/")
request.user = self.superuser request.user = self.superuser
readonly_fields = self.admin.get_readonly_fields(request, application) 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'] 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) self.assertEqual(readonly_fields, expected_fields)
def test_readonly_fields_for_analyst(self): def test_readonly_fields_for_analyst(self):
request = self.factory.get('/') # Use the correct method and path request = self.factory.get("/") # Use the correct method and path
request.user = self.staffuser request.user = self.staffuser
readonly_fields = self.admin.get_readonly_fields(request) 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'] 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) self.assertEqual(readonly_fields, expected_fields)
def test_readonly_fields_for_superuser(self): def test_readonly_fields_for_superuser(self):
request = self.factory.get('/') # Use the correct method and path request = self.factory.get("/") # Use the correct method and path
request.user = self.superuser request.user = self.superuser
readonly_fields = self.admin.get_readonly_fields(request) readonly_fields = self.admin.get_readonly_fields(request)
@ -322,19 +370,23 @@ class TestDomainApplicationAdmin(TestCase):
def test_saving_when_ineligible_creator(self): def test_saving_when_ineligible_creator(self):
# Create an instance of the model # Create an instance of the model
application = completed_application(status=DomainApplication.IN_REVIEW) application = completed_application(status=DomainApplication.IN_REVIEW)
application.creator.status = 'ineligible' application.creator.status = "ineligible"
application.creator.save() application.creator.save()
# Create a request object with a superuser # Create a request object with a superuser
request = self.factory.get('/') request = self.factory.get("/")
request.user = self.superuser request.user = self.superuser
with patch('django.contrib.messages.error') as mock_error: with patch("django.contrib.messages.error") as mock_error:
# Simulate saving the model # Simulate saving the model
self.admin.save_model(request, application, None, False) self.admin.save_model(request, application, None, False)
# Assert that the error message was called with the correct argument # 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.") 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 # Assert that the status has not changed
self.assertEqual(application.status, DomainApplication.IN_REVIEW) self.assertEqual(application.status, DomainApplication.IN_REVIEW)

View file

@ -1444,9 +1444,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
home_page = self.app.get("/") home_page = self.app.get("/")
self.assertContains(home_page, "igorville.gov") self.assertContains(home_page, "igorville.gov")
with less_console_noise(): with less_console_noise():
response = self.client.get( response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
reverse("domain", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)

View file

@ -5,5 +5,5 @@ from .permission_views import (
DomainPermissionView, DomainPermissionView,
DomainApplicationPermissionView, DomainApplicationPermissionView,
DomainInvitationPermissionDeleteView, DomainInvitationPermissionDeleteView,
ApplicationWizardPermissionView ApplicationWizardPermissionView,
) )

View file

@ -10,7 +10,7 @@ from .mixins import (
DomainPermission, DomainPermission,
DomainApplicationPermission, DomainApplicationPermission,
DomainInvitationPermission, DomainInvitationPermission,
ApplicationWizardPermission ApplicationWizardPermission,
) )
@ -54,7 +54,9 @@ class DomainApplicationPermissionView(DomainApplicationPermission, DetailView, a
raise NotImplementedError raise NotImplementedError
class ApplicationWizardPermissionView(ApplicationWizardPermission, TemplateView, abc.ABC): class ApplicationWizardPermissionView(
ApplicationWizardPermission, TemplateView, abc.ABC
):
"""Abstract base view for the application form that enforces permissions """Abstract base view for the application form that enforces permissions