mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-25 03:58:39 +02:00
merge from main
This commit is contained in:
commit
b03eea131b
40 changed files with 850 additions and 716 deletions
8
.github/workflows/deploy-stable.yaml
vendored
8
.github/workflows/deploy-stable.yaml
vendored
|
@ -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"
|
8
.github/workflows/deploy-staging.yaml
vendored
8
.github/workflows/deploy-staging.yaml
vendored
|
@ -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"
|
||||
|
|
|
@ -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
1137
src/Pipfile.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -784,7 +784,6 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
|
||||
|
||||
class DomainApplicationAdmin(ListHeaderAdmin):
|
||||
|
||||
"""Custom domain applications admin class."""
|
||||
|
||||
form = DomainApplicationAdminForm
|
||||
|
|
|
@ -2,7 +2,6 @@ from django.apps import AppConfig
|
|||
|
||||
|
||||
class RegistrarConfig(AppConfig):
|
||||
|
||||
"""Configure signal handling for our registrar Django application."""
|
||||
|
||||
name = "registrar"
|
||||
|
|
|
@ -16,6 +16,7 @@ $ docker-compose exec app python manage.py shell
|
|||
```
|
||||
|
||||
"""
|
||||
|
||||
import environs
|
||||
from base64 import b64decode
|
||||
from cfenv import AppEnv # type: ignore
|
||||
|
|
|
@ -201,7 +201,6 @@ class DomainApplicationFixture:
|
|||
|
||||
|
||||
class DomainFixture(DomainApplicationFixture):
|
||||
|
||||
"""Create one domain and permissions on it for each user."""
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Loads files from /tmp into our sandboxes"""
|
||||
|
||||
import glob
|
||||
import logging
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Generates current-full.csv and current-federal.csv then uploads them to the desired URL."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Generates current-full.csv and current-federal.csv then uploads them to the desired URL."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Loops through each valid DomainInformation object and updates its agency value"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import logging
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
""""""
|
||||
|
||||
import csv
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
|
|
@ -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"),
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ better caching responses.
|
|||
|
||||
|
||||
class NoCacheMiddleware:
|
||||
|
||||
"""Middleware to add a single header to every response."""
|
||||
|
||||
def __init__(self, get_response):
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
{% endblock %}
|
||||
<h1>Manage your domains</h2>
|
||||
|
||||
|
||||
<p class="margin-top-4">
|
||||
<a href="{% url 'application:' %}" class="usa-button"
|
||||
>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Custom field helpers for our inputs."""
|
||||
|
||||
import re
|
||||
|
||||
from django import template
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1236,7 +1236,6 @@ class TestDomainSecurityEmail(TestDomainOverview):
|
|||
|
||||
|
||||
class TestDomainDNSSEC(TestDomainOverview):
|
||||
|
||||
"""MockEPPLib is already inherited."""
|
||||
|
||||
def test_dnssec_page_refreshes_enable_button(self):
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -10,7 +10,6 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class EmailSendingError(RuntimeError):
|
||||
|
||||
"""Local error for handling all failures when sending email."""
|
||||
|
||||
pass
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue