merge from main

This commit is contained in:
Rachid Mrad 2024-02-21 18:01:42 -05:00
commit b03eea131b
No known key found for this signature in database
40 changed files with 850 additions and 716 deletions

View file

@ -37,3 +37,11 @@ jobs:
cf_org: cisa-dotgov
cf_space: stable
cf_manifest: "ops/manifests/manifest-stable.yaml"
- name: Run Django migrations
uses: cloud-gov/cg-cli-tools@main
with:
cf_username: ${{ secrets.CF_STABLE_USERNAME }}
cf_password: ${{ secrets.CF_STABLE_PASSWORD }}
cf_org: cisa-dotgov
cf_space: stable
cf_command: "run-task getgov-stable --command 'python manage.py migrate' --name migrate"

View file

@ -37,3 +37,11 @@ jobs:
cf_org: cisa-dotgov
cf_space: staging
cf_manifest: "ops/manifests/manifest-staging.yaml"
- name: Run Django migrations
uses: cloud-gov/cg-cli-tools@main
with:
cf_username: ${{ secrets.CF_STAGING_USERNAME }}
cf_password: ${{ secrets.CF_STAGING_PASSWORD }}
cf_org: cisa-dotgov
cf_space: staging
cf_command: "run-task getgov-staging --command 'python manage.py migrate' --name migrate"

View file

@ -4,7 +4,7 @@ verify_ssl = true
name = "pypi"
[packages]
django = "*"
django = "4.2.10"
cfenv = "*"
django-cors-headers = "*"
pycryptodomex = "*"

1137
src/Pipfile.lock generated

File diff suppressed because it is too large Load diff

View file

@ -19,7 +19,6 @@ API_BASE_PATH = "/api/v1/available/?domain="
class AvailableViewTest(MockEppLib):
"""Test that the view function works as expected."""
def setUp(self):
@ -123,7 +122,6 @@ class AvailableViewTest(MockEppLib):
class AvailableAPITest(MockEppLib):
"""Test that the API can be called as expected."""
def setUp(self):

View file

@ -1,4 +1,5 @@
"""Internal API views"""
from django.apps import apps
from django.views.decorators.http import require_http_methods
from django.http import HttpResponse

View file

@ -327,6 +327,27 @@ class ViewsTest(TestCase):
self.assertEqual(response.status_code, 302)
self.assertEqual(actual, expected)
def test_logout_redirect_url_with_no_session_state(self, mock_client):
"""Test that logout redirects to the configured post_logout_redirect_uris."""
with less_console_noise():
# MOCK
mock_client.callback.side_effect = self.user_info
mock_client.registration_response = {"post_logout_redirect_uris": ["http://example.com/back"]}
mock_client.provider_info = {"end_session_endpoint": "http://example.com/log_me_out"}
mock_client.client_id = "TEST"
# TEST
with less_console_noise():
response = self.client.get(reverse("logout"))
# ASSERTIONS
# Assert redirect code and url are accurate
expected = (
"http://example.com/log_me_out?client_id=TEST"
"&post_logout_redirect_uri=http%3A%2F%2Fexample.com%2Fback"
)
actual = response.url
self.assertEqual(response.status_code, 302)
self.assertEqual(actual, expected)
@patch("djangooidc.views.auth_logout")
def test_logout_always_logs_out(self, mock_logout, _):
"""Without additional mocking, logout will always fail.

View file

@ -145,8 +145,12 @@ def logout(request, next_page=None):
user = request.user
request_args = {
"client_id": CLIENT.client_id,
"state": request.session["state"],
}
# if state is not in request session, still redirect to the identity
# provider's logout url, but don't include the state in the url; this
# will successfully log out of the identity provider
if "state" in request.session:
request_args["state"] = request.session["state"]
if (
"post_logout_redirect_uris" in CLIENT.registration_response.keys()
and len(CLIENT.registration_response["post_logout_redirect_uris"]) > 0

View file

@ -784,7 +784,6 @@ class DomainInformationAdmin(ListHeaderAdmin):
class DomainApplicationAdmin(ListHeaderAdmin):
"""Custom domain applications admin class."""
form = DomainApplicationAdminForm

View file

@ -2,7 +2,6 @@ from django.apps import AppConfig
class RegistrarConfig(AppConfig):
"""Configure signal handling for our registrar Django application."""
name = "registrar"

View file

@ -16,6 +16,7 @@ $ docker-compose exec app python manage.py shell
```
"""
import environs
from base64 import b64decode
from cfenv import AppEnv # type: ignore

View file

@ -201,7 +201,6 @@ class DomainApplicationFixture:
class DomainFixture(DomainApplicationFixture):
"""Create one domain and permissions on it for each user."""
@classmethod

View file

@ -1,4 +1,5 @@
"""Loads files from /tmp into our sandboxes"""
import glob
import logging

View file

@ -1,4 +1,5 @@
"""Generates current-full.csv and current-federal.csv then uploads them to the desired URL."""
import logging
import os

View file

@ -1,4 +1,5 @@
"""Generates current-full.csv and current-federal.csv then uploads them to the desired URL."""
import logging
import os

View file

@ -1,4 +1,5 @@
"""Loops through each valid DomainInformation object and updates its agency value"""
import argparse
import csv
import logging

View file

@ -5,6 +5,7 @@ Regarding our dataclasses:
Not intended to be used as models but rather as an alternative to storing as a dictionary.
By keeping it as a dataclass instead of a dictionary, we can maintain data consistency.
""" # noqa
from dataclasses import dataclass, field
from datetime import date
from enum import Enum

View file

@ -1,4 +1,5 @@
""""""
import csv
from dataclasses import dataclass
from datetime import datetime

View file

@ -0,0 +1,45 @@
# Generated by Django 4.2.7 on 2024-02-14 21:45
from django.db import migrations, models
import phonenumber_field.modelfields
class Migration(migrations.Migration):
dependencies = [
("registrar", "0068_domainapplication_notes_domaininformation_notes"),
]
operations = [
migrations.AlterField(
model_name="contact",
name="email",
field=models.EmailField(blank=True, db_index=True, max_length=254, null=True),
),
migrations.AlterField(
model_name="contact",
name="first_name",
field=models.TextField(blank=True, db_index=True, null=True, verbose_name="first name / given name"),
),
migrations.AlterField(
model_name="contact",
name="last_name",
field=models.TextField(blank=True, db_index=True, null=True, verbose_name="last name / family name"),
),
migrations.AlterField(
model_name="contact",
name="middle_name",
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name="contact",
name="phone",
field=phonenumber_field.modelfields.PhoneNumberField(
blank=True, db_index=True, max_length=128, null=True, region=None
),
),
migrations.AlterField(
model_name="contact",
name="title",
field=models.TextField(blank=True, null=True, verbose_name="title or role in your organization"),
),
]

View file

@ -6,7 +6,6 @@ from .utility.time_stamped_model import TimeStampedModel
class Contact(TimeStampedModel):
"""Contact information follows a similar pattern for each contact."""
user = models.OneToOneField(
@ -19,38 +18,32 @@ class Contact(TimeStampedModel):
first_name = models.TextField(
null=True,
blank=True,
help_text="First name",
verbose_name="first name / given name",
db_index=True,
)
middle_name = models.TextField(
null=True,
blank=True,
help_text="Middle name (optional)",
)
last_name = models.TextField(
null=True,
blank=True,
help_text="Last name",
verbose_name="last name / family name",
db_index=True,
)
title = models.TextField(
null=True,
blank=True,
help_text="Title",
verbose_name="title or role in your organization",
)
email = models.EmailField(
null=True,
blank=True,
help_text="Email",
db_index=True,
)
phone = PhoneNumberField(
null=True,
blank=True,
help_text="Phone",
db_index=True,
)

View file

@ -17,7 +17,6 @@ logger = logging.getLogger(__name__)
class DomainApplication(TimeStampedModel):
"""A registrant's application for a new domain."""
# Constants for choice fields
@ -97,7 +96,6 @@ class DomainApplication(TimeStampedModel):
ARMED_FORCES_AP = "AP", "Armed Forces Pacific (AP)"
class OrganizationChoices(models.TextChoices):
"""
Primary organization choices:
For use in django admin
@ -114,7 +112,6 @@ class DomainApplication(TimeStampedModel):
SCHOOL_DISTRICT = "school_district", "School district"
class OrganizationChoicesVerbose(models.TextChoices):
"""
Secondary organization choices
For use in the application form and on the templates

View file

@ -14,7 +14,6 @@ logger = logging.getLogger(__name__)
class DomainInformation(TimeStampedModel):
"""A registrant's domain information for that domain, exported from
DomainApplication. We use these field from DomainApplication with few exceptions
which are 'removed' via pop at the bottom of this file. Most of design for domain
@ -256,6 +255,14 @@ class DomainInformation(TimeStampedModel):
else:
da_many_to_many_dict[field] = getattr(domain_application, field).all()
# This will not happen in normal code flow, but having some redundancy doesn't hurt.
# da_dict should not have "id" under any circumstances.
# If it does have it, then this indicates that common_fields is overzealous in the data
# that it is returning. Try looking in DomainHelper.get_common_fields.
if "id" in da_dict:
logger.warning("create_from_da() -> Found attribute 'id' when trying to create")
da_dict.pop("id", None)
# Create a placeholder DomainInformation object
domain_info = DomainInformation(**da_dict)

View file

@ -4,11 +4,9 @@ from .utility.time_stamped_model import TimeStampedModel
class UserDomainRole(TimeStampedModel):
"""This is a linking table that connects a user with a role on a domain."""
class Roles(models.TextChoices):
"""The possible roles are listed here.
Implementation of the named roles for allowing particular operations happens

View file

@ -180,8 +180,8 @@ class DomainHelper:
"""
# Get a list of the existing fields on model_1 and model_2
model_1_fields = set(field.name for field in model_1._meta.get_fields() if field != "id")
model_2_fields = set(field.name for field in model_2._meta.get_fields() if field != "id")
model_1_fields = set(field.name for field in model_1._meta.get_fields() if field.name != "id")
model_2_fields = set(field.name for field in model_2._meta.get_fields() if field.name != "id")
# Get the fields that exist on both DomainApplication and DomainInformation
common_fields = model_1_fields & model_2_fields

View file

@ -4,7 +4,6 @@ from .utility.time_stamped_model import TimeStampedModel
class VerifiedByStaff(TimeStampedModel):
"""emails that get added to this table will bypass ial2 on login."""
email = models.EmailField(

View file

@ -4,7 +4,6 @@ from .utility.time_stamped_model import TimeStampedModel
class Website(TimeStampedModel):
"""Keep domain names in their own table so that applications can refer to
many of them."""

View file

@ -6,7 +6,6 @@ better caching responses.
class NoCacheMiddleware:
"""Middleware to add a single header to every response."""
def __init__(self, get_response):

View file

@ -15,6 +15,7 @@
{% endblock %}
<h1>Manage your domains</h2>
<p class="margin-top-4">
<a href="{% url 'application:' %}" class="usa-button"
>

View file

@ -1,4 +1,5 @@
"""Custom field helpers for our inputs."""
import re
from django import template

View file

@ -630,7 +630,6 @@ class TestPermissions(TestCase):
class TestDomainInformation(TestCase):
"""Test the DomainInformation model, when approved or otherwise"""
def setUp(self):
@ -683,7 +682,6 @@ class TestDomainInformation(TestCase):
class TestInvitations(TestCase):
"""Test the retrieval of invitations."""
def setUp(self):

View file

@ -3,6 +3,7 @@ Feature being tested: Registry Integration
This file tests the various ways in which the registrar interacts with the registry.
"""
from django.test import TestCase
from django.db.utils import IntegrityError
from unittest.mock import MagicMock, patch, call

View file

@ -7,13 +7,14 @@ from registrar.models.domain import Domain
from registrar.models.public_contact import PublicContact
from registrar.models.user import User
from django.contrib.auth import get_user_model
from registrar.models.user_domain_role import UserDomainRole
from registrar.tests.common import MockEppLib
from registrar.utility.csv_export import (
write_header,
write_body,
write_csv,
get_default_start_date,
get_default_end_date,
)
from django.core.management import call_command
from unittest.mock import MagicMock, call, mock_open, patch
from api.views import get_current_federal, get_current_full
@ -336,11 +337,30 @@ class ExportDataTest(MockEppLib):
federal_agency="Armed Forces Retirement Home",
)
meoward_user = get_user_model().objects.create(
username="meoward_username", first_name="first_meoward", last_name="last_meoward", email="meoward@rocks.com"
)
# Test for more than 1 domain manager
_, created = UserDomainRole.objects.get_or_create(
user=meoward_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
)
_, created = UserDomainRole.objects.get_or_create(
user=self.user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
)
# Test for just 1 domain manager
_, created = UserDomainRole.objects.get_or_create(
user=meoward_user, domain=self.domain_2, role=UserDomainRole.Roles.MANAGER
)
def tearDown(self):
PublicContact.objects.all().delete()
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
User.objects.all().delete()
UserDomainRole.objects.all().delete()
super().tearDown()
def test_export_domains_to_writer_security_emails(self):
@ -383,8 +403,10 @@ class ExportDataTest(MockEppLib):
}
self.maxDiff = None
# Call the export functions
write_header(writer, columns)
write_body(writer, columns, sort_fields, filter_condition)
write_csv(
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
)
# Reset the CSV file's position to the beginning
csv_file.seek(0)
# Read the content into a variable
@ -405,7 +427,7 @@ class ExportDataTest(MockEppLib):
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.assertEqual(csv_content, expected_content)
def test_write_body(self):
def test_write_csv(self):
"""Test that write_body returns the
existing domain, test that sort by domain name works,
test that filter works"""
@ -440,8 +462,9 @@ class ExportDataTest(MockEppLib):
],
}
# Call the export functions
write_header(writer, columns)
write_body(writer, columns, sort_fields, filter_condition)
write_csv(
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
)
# Reset the CSV file's position to the beginning
csv_file.seek(0)
# Read the content into a variable
@ -489,8 +512,9 @@ class ExportDataTest(MockEppLib):
],
}
# Call the export functions
write_header(writer, columns)
write_body(writer, columns, sort_fields, filter_condition)
write_csv(
writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
)
# Reset the CSV file's position to the beginning
csv_file.seek(0)
# Read the content into a variable
@ -567,20 +591,22 @@ class ExportDataTest(MockEppLib):
}
# Call the export functions
write_header(writer, columns)
write_body(
write_csv(
writer,
columns,
sort_fields,
filter_condition,
get_domain_managers=False,
should_write_header=True,
)
write_body(
write_csv(
writer,
columns,
sort_fields_for_deleted_domains,
filter_conditions_for_deleted_domains,
get_domain_managers=False,
should_write_header=False,
)
# Reset the CSV file's position to the beginning
csv_file.seek(0)
@ -606,6 +632,64 @@ class ExportDataTest(MockEppLib):
self.assertEqual(csv_content, expected_content)
def test_export_domains_to_writer_domain_managers(self):
"""Test that export_domains_to_writer returns the
expected domain managers"""
with less_console_noise():
# Create a CSV file in memory
csv_file = StringIO()
writer = csv.writer(csv_file)
# Define columns, sort fields, and filter condition
columns = [
"Domain name",
"Status",
"Expiration date",
"Domain type",
"Agency",
"Organization name",
"City",
"State",
"AO",
"AO email",
"Security contact email",
]
sort_fields = ["domain__name"]
filter_condition = {
"domain__state__in": [
Domain.State.READY,
Domain.State.DNS_NEEDED,
Domain.State.ON_HOLD,
],
}
self.maxDiff = None
# Call the export functions
write_csv(
writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True
)
# Reset the CSV file's position to the beginning
csv_file.seek(0)
# Read the content into a variable
csv_content = csv_file.read()
# We expect READY domains,
# sorted alphabetially by domain name
expected_content = (
"Domain name,Status,Expiration date,Domain type,Agency,"
"Organization name,City,State,AO,AO email,"
"Security contact email,Domain manager email 1,Domain manager email 2,\n"
"adomain10.gov,Ready,,Federal,Armed Forces Retirement Home,,,, , ,\n"
"adomain2.gov,Dns needed,,Interstate,,,,, , , ,meoward@rocks.com\n"
"cdomain1.gov,Ready,,Federal - Executive,World War I Centennial Commission,,,"
", , , ,meoward@rocks.com,info@example.com\n"
"ddomain3.gov,On hold,,Federal,Armed Forces Retirement Home,,,, , , ,,\n"
)
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.assertEqual(csv_content, expected_content)
class HelperFunctions(TestCase):
"""This asserts that 1=1. Its limited usefulness lies in making sure the helper methods stay healthy."""

View file

@ -30,7 +30,6 @@ logger = logging.getLogger(__name__)
class DomainApplicationTests(TestWithUser, WebTest):
"""Webtests for domain application to test filling and submitting."""
# Doesn't work with CSRF checking

View file

@ -1236,7 +1236,6 @@ class TestDomainSecurityEmail(TestDomainOverview):
class TestDomainDNSSEC(TestDomainOverview):
"""MockEPPLib is already inherited."""
def test_dnssec_page_refreshes_enable_button(self):

View file

@ -17,8 +17,9 @@ logger = logging.getLogger(__name__)
def write_header(writer, columns):
"""
Receives params from the parent methods and outputs a CSV with a header row.
Works with write_header as longas the same writer object is passed.
Works with write_header as long as the same writer object is passed.
"""
writer.writerow(columns)
@ -43,7 +44,7 @@ def get_domain_infos(filter_condition, sort_fields):
return domain_infos_cleaned
def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None):
def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None, get_domain_managers=False):
"""Given a set of columns, generate a new row from cleaned column data"""
# Domain should never be none when parsing this information
@ -77,6 +78,8 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None
# create a dictionary of fields which can be included in output
FIELDS = {
"Domain name": domain.name,
"Status": domain.get_state_display(),
"Expiration date": domain.expiration_date,
"Domain type": domain_type,
"Agency": domain_info.federal_agency,
"Organization name": domain_info.organization_name,
@ -85,39 +88,27 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None
"AO": domain_info.ao, # type: ignore
"AO email": domain_info.authorizing_official.email if domain_info.authorizing_official else " ",
"Security contact email": security_email,
"Status": domain.get_state_display(),
"Expiration date": domain.expiration_date,
"Created at": domain.created_at,
"First ready": domain.first_ready,
"Deleted": domain.deleted,
}
# user_emails = [user.email for user in domain.permissions]
if get_domain_managers:
# Get each domain managers email and add to list
dm_emails = [dm.user.email for dm in domain.permissions.all()]
# Dynamically add user emails to the FIELDS dictionary
# for i, user_email in enumerate(user_emails, start=1):
# FIELDS[f"User{i} email"] = user_email
# Set up the "matching header" + row field data
for i, dm_email in enumerate(dm_emails, start=1):
FIELDS[f"Domain manager email {i}"] = dm_email
row = [FIELDS.get(column, "") for column in columns]
return row
def write_body(
writer,
columns,
sort_fields,
filter_condition,
):
def _get_security_emails(sec_contact_ids):
"""
Receives params from the parent methods and outputs a CSV with fltered and sorted domains.
Works with write_header as longas the same writer object is passed.
Retrieve security contact emails for the given security contact IDs.
"""
# Get the domainInfos
all_domain_infos = get_domain_infos(filter_condition, sort_fields)
# Store all security emails to avoid epp calls or excessive filters
sec_contact_ids = all_domain_infos.values_list("domain__security_contact_registry_id", flat=True)
security_emails_dict = {}
public_contacts = (
PublicContact.objects.only("email", "domain__name")
@ -133,24 +124,55 @@ def write_body(
else:
logger.warning("csv_export -> Domain was none for PublicContact")
# all_user_nums = 0
# for domain_info in all_domain_infos:
# user_num = len(domain_info.domain.permissions)
# all_user_nums.append(user_num)
return security_emails_dict
# if user_num > highest_user_nums:
# highest_user_nums = user_num
# Build the header here passing to it highest_user_nums
def update_columns_with_domain_managers(columns, max_dm_count):
"""
Update the columns list to include "Domain manager email {#}" headers
based on the maximum domain manager count.
"""
for i in range(1, max_dm_count + 1):
columns.append(f"Domain manager email {i}")
def write_csv(
writer,
columns,
sort_fields,
filter_condition,
get_domain_managers=False,
should_write_header=True,
):
"""
Receives params from the parent methods and outputs a CSV with fltered and sorted domains.
Works with write_header as longas the same writer object is passed.
get_domain_managers: Conditional bc we only use domain manager info for export_data_full_to_csv
should_write_header: Conditional bc export_data_growth_to_csv calls write_body twice
"""
all_domain_infos = get_domain_infos(filter_condition, sort_fields)
# Store all security emails to avoid epp calls or excessive filters
sec_contact_ids = all_domain_infos.values_list("domain__security_contact_registry_id", flat=True)
security_emails_dict = _get_security_emails(sec_contact_ids)
# Reduce the memory overhead when performing the write operation
paginator = Paginator(all_domain_infos, 1000)
if get_domain_managers and len(all_domain_infos) > 0:
# We want to get the max amont of domain managers an
# account has to set the column header dynamically
max_dm_count = max(len(domain_info.domain.permissions.all()) for domain_info in all_domain_infos)
update_columns_with_domain_managers(columns, max_dm_count)
for page_num in paginator.page_range:
page = paginator.page(page_num)
rows = []
for domain_info in page.object_list:
try:
row = parse_row(columns, domain_info, security_emails_dict)
row = parse_row(columns, domain_info, security_emails_dict, get_domain_managers)
rows.append(row)
except ValueError:
# This should not happen. If it does, just skip this row.
@ -158,6 +180,9 @@ def write_body(
logger.error("csv_export -> Error when parsing row, domain was None")
continue
if should_write_header:
write_header(writer, columns)
writer.writerows(rows)
@ -168,6 +193,8 @@ def export_data_type_to_csv(csv_file):
# define columns to include in export
columns = [
"Domain name",
"Status",
"Expiration date",
"Domain type",
"Agency",
"Organization name",
@ -176,9 +203,9 @@ def export_data_type_to_csv(csv_file):
"AO",
"AO email",
"Security contact email",
"Status",
"Expiration date",
# For domain manager we are pass it in as a parameter below in write_body
]
# Coalesce is used to replace federal_type of None with ZZZZZ
sort_fields = [
"organization_type",
@ -193,8 +220,7 @@ def export_data_type_to_csv(csv_file):
Domain.State.ON_HOLD,
],
}
write_header(writer, columns)
write_body(writer, columns, sort_fields, filter_condition)
write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True)
def export_data_full_to_csv(csv_file):
@ -225,8 +251,7 @@ def export_data_full_to_csv(csv_file):
Domain.State.ON_HOLD,
],
}
write_header(writer, columns)
write_body(writer, columns, sort_fields, filter_condition)
write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True)
def export_data_federal_to_csv(csv_file):
@ -258,8 +283,7 @@ def export_data_federal_to_csv(csv_file):
Domain.State.ON_HOLD,
],
}
write_header(writer, columns)
write_body(writer, columns, sort_fields, filter_condition)
write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True)
def get_default_start_date():
@ -326,6 +350,12 @@ def export_data_growth_to_csv(csv_file, start_date, end_date):
"domain__deleted__gte": start_date_formatted,
}
write_header(writer, columns)
write_body(writer, columns, sort_fields, filter_condition)
write_body(writer, columns, sort_fields_for_deleted_domains, filter_condition_for_deleted_domains)
write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True)
write_csv(
writer,
columns,
sort_fields_for_deleted_domains,
filter_condition_for_deleted_domains,
get_domain_managers=False,
should_write_header=False,
)

View file

@ -10,7 +10,6 @@ logger = logging.getLogger(__name__)
class EmailSendingError(RuntimeError):
"""Local error for handling all failures when sending email."""
pass

View file

@ -135,7 +135,6 @@ class DomainFormBaseView(DomainBaseView, FormMixin):
class DomainView(DomainBaseView):
"""Domain detail overview page."""
template_name = "domain_detail.html"
@ -787,14 +786,17 @@ class DomainAddUserView(DomainFormBaseView):
return redirect(self.get_success_url())
class DomainInvitationDeleteView(DomainInvitationPermissionDeleteView, SuccessMessageMixin):
# The order of the superclasses matters here. BaseDeleteView has a bug where the
# "form_valid" function does not call super, so it cannot use SuccessMessageMixin.
# The workaround is to use SuccessMessageMixin first.
class DomainInvitationDeleteView(SuccessMessageMixin, DomainInvitationPermissionDeleteView):
object: DomainInvitation # workaround for type mismatch in DeleteView
def get_success_url(self):
return reverse("domain-users", kwargs={"pk": self.object.domain.id})
def get_success_message(self, cleaned_data):
return f"Successfully canceled invitation for {self.object.email}."
return f"Canceled invitation to {self.object.email}."
class DomainDeleteUserView(UserDomainRolePermissionDeleteView):

View file

@ -146,7 +146,6 @@ class OrderableFieldsMixin:
class PermissionsLoginMixin(PermissionRequiredMixin):
"""Mixin that redirects to login page if not logged in, otherwise 403."""
def handle_no_permission(self):
@ -155,7 +154,6 @@ class PermissionsLoginMixin(PermissionRequiredMixin):
class DomainPermission(PermissionsLoginMixin):
"""Permission mixin that redirects to domain if user has access,
otherwise 403"""
@ -264,7 +262,6 @@ class DomainPermission(PermissionsLoginMixin):
class DomainApplicationPermission(PermissionsLoginMixin):
"""Permission mixin that redirects to domain application if user
has access, otherwise 403"""
@ -287,7 +284,6 @@ class DomainApplicationPermission(PermissionsLoginMixin):
class UserDeleteDomainRolePermission(PermissionsLoginMixin):
"""Permission mixin for UserDomainRole if user
has access, otherwise 403"""
@ -324,7 +320,6 @@ class UserDeleteDomainRolePermission(PermissionsLoginMixin):
class DomainApplicationPermissionWithdraw(PermissionsLoginMixin):
"""Permission mixin that redirects to withdraw action on domain application
if user has access, otherwise 403"""
@ -347,7 +342,6 @@ class DomainApplicationPermissionWithdraw(PermissionsLoginMixin):
class ApplicationWizardPermission(PermissionsLoginMixin):
"""Permission mixin that redirects to start or edit domain application if
user has access, otherwise 403"""
@ -365,7 +359,6 @@ class ApplicationWizardPermission(PermissionsLoginMixin):
class DomainInvitationPermission(PermissionsLoginMixin):
"""Permission mixin that redirects to domain invitation if user has
access, otherwise 403"

View file

@ -20,7 +20,6 @@ logger = logging.getLogger(__name__)
class DomainPermissionView(DomainPermission, DetailView, abc.ABC):
"""Abstract base view for domains that enforces permissions.
This abstract view cannot be instantiated. Actual views must specify
@ -58,7 +57,6 @@ class DomainPermissionView(DomainPermission, DetailView, abc.ABC):
class DomainApplicationPermissionView(DomainApplicationPermission, DetailView, abc.ABC):
"""Abstract base view for domain applications that enforces permissions
This abstract view cannot be instantiated. Actual views must specify
@ -78,7 +76,6 @@ class DomainApplicationPermissionView(DomainApplicationPermission, DetailView, a
class DomainApplicationPermissionWithdrawView(DomainApplicationPermissionWithdraw, DetailView, abc.ABC):
"""Abstract base view for domain application withdraw function
This abstract view cannot be instantiated. Actual views must specify
@ -98,7 +95,6 @@ class DomainApplicationPermissionWithdrawView(DomainApplicationPermissionWithdra
class ApplicationWizardPermissionView(ApplicationWizardPermission, TemplateView, abc.ABC):
"""Abstract base view for the application form that enforces permissions
This abstract view cannot be instantiated. Actual views must specify
@ -113,7 +109,6 @@ class ApplicationWizardPermissionView(ApplicationWizardPermission, TemplateView,
class DomainInvitationPermissionDeleteView(DomainInvitationPermission, DeleteView, abc.ABC):
"""Abstract view for deleting a domain invitation.
This one is fairly specialized, but this is the only thing that we do
@ -127,7 +122,6 @@ class DomainInvitationPermissionDeleteView(DomainInvitationPermission, DeleteVie
class DomainApplicationPermissionDeleteView(DomainApplicationPermission, DeleteView, abc.ABC):
"""Abstract view for deleting a DomainApplication."""
model = DomainApplication
@ -135,7 +129,6 @@ class DomainApplicationPermissionDeleteView(DomainApplicationPermission, DeleteV
class UserDomainRolePermissionDeleteView(UserDeleteDomainRolePermission, DeleteView, abc.ABC):
"""Abstract base view for deleting a UserDomainRole.
This abstract view cannot be instantiated. Actual views must specify

View file

@ -1,18 +1,18 @@
-i https://pypi.python.org/simple
annotated-types==0.6.0; python_version >= '3.8'
asgiref==3.7.2; python_version >= '3.7'
boto3==1.33.7; python_version >= '3.7'
botocore==1.33.7; python_version >= '3.7'
boto3==1.34.37; python_version >= '3.8'
botocore==1.34.37; python_version >= '3.8'
cachetools==5.3.2; python_version >= '3.7'
certifi==2023.11.17; python_version >= '3.6'
certifi==2024.2.2; python_version >= '3.6'
cfenv==0.5.3
cffi==1.16.0; python_version >= '3.8'
cffi==1.16.0; platform_python_implementation != 'PyPy'
charset-normalizer==3.3.2; python_full_version >= '3.7.0'
cryptography==41.0.7; python_version >= '3.7'
cryptography==42.0.2; python_version >= '3.7'
defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
dj-database-url==2.1.0
dj-email-url==1.0.6
django==4.2.7; python_version >= '3.8'
django==4.2.10; python_version >= '3.8'
django-allow-cidr==0.7.1
django-auditlog==2.3.0; python_version >= '3.7'
django-cache-url==3.4.5
@ -20,42 +20,42 @@ django-cors-headers==4.3.1; python_version >= '3.8'
django-csp==3.7
django-fsm==2.8.1
django-login-required-middleware==0.9.0
django-phonenumber-field[phonenumberslite]==7.2.0; python_version >= '3.8'
django-phonenumber-field[phonenumberslite]==7.3.0; python_version >= '3.8'
django-widget-tweaks==1.5.0; python_version >= '3.8'
environs[django]==9.5.0; python_version >= '3.6'
faker==20.1.0; python_version >= '3.8'
environs[django]==10.3.0; python_version >= '3.8'
faker==23.1.0; python_version >= '3.8'
fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c
furl==2.1.3
future==0.18.3; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
gevent==23.9.1; python_version >= '3.8'
geventconnpool@ git+https://github.com/rasky/geventconnpool.git@1bbb93a714a331a069adf27265fe582d9ba7ecd4
greenlet==3.0.1; python_version >= '3.7'
greenlet==3.0.3; python_version >= '3.7'
gunicorn==21.2.0; python_version >= '3.5'
idna==3.6; python_version >= '3.5'
jmespath==1.0.1; python_version >= '3.7'
lxml==4.9.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
mako==1.3.0; python_version >= '3.8'
markupsafe==2.1.3; python_version >= '3.7'
marshmallow==3.20.1; python_version >= '3.8'
lxml==5.1.0; python_version >= '3.6'
mako==1.3.2; python_version >= '3.8'
markupsafe==2.1.5; python_version >= '3.7'
marshmallow==3.20.2; python_version >= '3.8'
oic==1.6.1; python_version ~= '3.7'
orderedmultidict==1.0.1
packaging==23.2; python_version >= '3.7'
phonenumberslite==8.13.26
phonenumberslite==8.13.29
psycopg2-binary==2.9.9; python_version >= '3.7'
pycparser==2.21
pycryptodomex==3.19.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
pydantic==2.5.2; python_version >= '3.7'
pydantic-core==2.14.5; python_version >= '3.7'
pycryptodomex==3.20.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
pydantic==2.6.1; python_version >= '3.8'
pydantic-core==2.16.2; python_version >= '3.8'
pydantic-settings==2.1.0; python_version >= '3.8'
pyjwkest==1.4.2
python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
python-dotenv==1.0.0; python_version >= '3.8'
python-dotenv==1.0.1; python_version >= '3.8'
requests==2.31.0; python_version >= '3.7'
s3transfer==0.8.2; python_version >= '3.7'
setuptools==69.0.2; python_version >= '3.8'
s3transfer==0.10.0; python_version >= '3.8'
setuptools==69.0.3; python_version >= '3.8'
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
sqlparse==0.4.4; python_version >= '3.5'
typing-extensions==4.8.0; python_version >= '3.8'
typing-extensions==4.9.0; python_version >= '3.8'
urllib3==2.0.7; python_version >= '3.7'
whitenoise==6.6.0; python_version >= '3.8'
zope.event==5.0; python_version >= '3.7'