mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-19 02:49:21 +02:00
Merge branch 'main' into dk/1073-admin-multi-selects
This commit is contained in:
commit
60da0fcd5a
23 changed files with 274 additions and 48 deletions
5
.github/workflows/deploy-sandbox.yaml
vendored
5
.github/workflows/deploy-sandbox.yaml
vendored
|
@ -47,9 +47,8 @@ jobs:
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: docker compose run app python manage.py collectstatic --no-input
|
run: docker compose run app python manage.py collectstatic --no-input
|
||||||
- name: Deploy to cloud.gov sandbox
|
- name: Deploy to cloud.gov sandbox
|
||||||
uses: 18f/cg-deploy-action@main
|
uses: cloud-gov/cg-cli-tools@main
|
||||||
env:
|
env:
|
||||||
DEPLOY_NOW: thanks
|
|
||||||
ENVIRONMENT: ${{ needs.variables.outputs.environment }}
|
ENVIRONMENT: ${{ needs.variables.outputs.environment }}
|
||||||
CF_USERNAME: CF_${{ needs.variables.outputs.environment }}_USERNAME
|
CF_USERNAME: CF_${{ needs.variables.outputs.environment }}_USERNAME
|
||||||
CF_PASSWORD: CF_${{ needs.variables.outputs.environment }}_PASSWORD
|
CF_PASSWORD: CF_${{ needs.variables.outputs.environment }}_PASSWORD
|
||||||
|
@ -58,7 +57,7 @@ jobs:
|
||||||
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
||||||
cf_org: cisa-dotgov
|
cf_org: cisa-dotgov
|
||||||
cf_space: ${{ env.ENVIRONMENT }}
|
cf_space: ${{ env.ENVIRONMENT }}
|
||||||
push_arguments: "-f ops/manifests/manifest-${{ env.ENVIRONMENT }}.yaml"
|
cf_manifest: ops/manifests/manifest-${{ env.ENVIRONMENT }}.yaml
|
||||||
comment:
|
comment:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [variables, deploy]
|
needs: [variables, deploy]
|
||||||
|
|
6
.github/workflows/deploy-stable.yaml
vendored
6
.github/workflows/deploy-stable.yaml
vendored
|
@ -30,12 +30,10 @@ jobs:
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: docker compose run app python manage.py collectstatic --no-input
|
run: docker compose run app python manage.py collectstatic --no-input
|
||||||
- name: Deploy to cloud.gov sandbox
|
- name: Deploy to cloud.gov sandbox
|
||||||
uses: 18f/cg-deploy-action@main
|
uses: cloud-gov/cg-cli-tools@main
|
||||||
env:
|
|
||||||
DEPLOY_NOW: thanks
|
|
||||||
with:
|
with:
|
||||||
cf_username: ${{ secrets.CF_STABLE_USERNAME }}
|
cf_username: ${{ secrets.CF_STABLE_USERNAME }}
|
||||||
cf_password: ${{ secrets.CF_STABLE_PASSWORD }}
|
cf_password: ${{ secrets.CF_STABLE_PASSWORD }}
|
||||||
cf_org: cisa-dotgov
|
cf_org: cisa-dotgov
|
||||||
cf_space: stable
|
cf_space: stable
|
||||||
push_arguments: "-f ops/manifests/manifest-stable.yaml"
|
cf_manifest: "ops/manifests/manifest-stable.yaml"
|
||||||
|
|
6
.github/workflows/deploy-staging.yaml
vendored
6
.github/workflows/deploy-staging.yaml
vendored
|
@ -30,12 +30,10 @@ jobs:
|
||||||
working-directory: ./src
|
working-directory: ./src
|
||||||
run: docker compose run app python manage.py collectstatic --no-input
|
run: docker compose run app python manage.py collectstatic --no-input
|
||||||
- name: Deploy to cloud.gov sandbox
|
- name: Deploy to cloud.gov sandbox
|
||||||
uses: 18f/cg-deploy-action@main
|
uses: cloud-gov/cg-cli-tools@main
|
||||||
env:
|
|
||||||
DEPLOY_NOW: thanks
|
|
||||||
with:
|
with:
|
||||||
cf_username: ${{ secrets.CF_STAGING_USERNAME }}
|
cf_username: ${{ secrets.CF_STAGING_USERNAME }}
|
||||||
cf_password: ${{ secrets.CF_STAGING_PASSWORD }}
|
cf_password: ${{ secrets.CF_STAGING_PASSWORD }}
|
||||||
cf_org: cisa-dotgov
|
cf_org: cisa-dotgov
|
||||||
cf_space: staging
|
cf_space: staging
|
||||||
push_arguments: "-f ops/manifests/manifest-staging.yaml"
|
cf_manifest: "ops/manifests/manifest-staging.yaml"
|
||||||
|
|
4
.github/workflows/migrate.yaml
vendored
4
.github/workflows/migrate.yaml
vendored
|
@ -38,10 +38,10 @@ jobs:
|
||||||
CF_PASSWORD: CF_${{ github.event.inputs.environment }}_PASSWORD
|
CF_PASSWORD: CF_${{ github.event.inputs.environment }}_PASSWORD
|
||||||
steps:
|
steps:
|
||||||
- name: Run Django migrations for ${{ github.event.inputs.environment }}
|
- name: Run Django migrations for ${{ github.event.inputs.environment }}
|
||||||
uses: 18f/cg-deploy-action@main
|
uses: cloud-gov/cg-cli-tools@main
|
||||||
with:
|
with:
|
||||||
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
||||||
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
||||||
cf_org: cisa-dotgov
|
cf_org: cisa-dotgov
|
||||||
cf_space: ${{ github.event.inputs.environment }}
|
cf_space: ${{ github.event.inputs.environment }}
|
||||||
full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py migrate' --name migrate"
|
cf_command: "run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py migrate' --name migrate"
|
||||||
|
|
12
.github/workflows/reset-db.yaml
vendored
12
.github/workflows/reset-db.yaml
vendored
|
@ -38,28 +38,28 @@ jobs:
|
||||||
CF_PASSWORD: CF_${{ github.event.inputs.environment }}_PASSWORD
|
CF_PASSWORD: CF_${{ github.event.inputs.environment }}_PASSWORD
|
||||||
steps:
|
steps:
|
||||||
- name: Delete existing data for ${{ github.event.inputs.environment }}
|
- name: Delete existing data for ${{ github.event.inputs.environment }}
|
||||||
uses: 18f/cg-deploy-action@main
|
uses: cloud-gov/cg-cli-tools@main
|
||||||
with:
|
with:
|
||||||
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
||||||
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
||||||
cf_org: cisa-dotgov
|
cf_org: cisa-dotgov
|
||||||
cf_space: ${{ github.event.inputs.environment }}
|
cf_space: ${{ github.event.inputs.environment }}
|
||||||
full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py flush --no-input' --name flush"
|
cf_command: "run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py flush --no-input' --name flush"
|
||||||
|
|
||||||
- name: Run Django migrations for ${{ github.event.inputs.environment }}
|
- name: Run Django migrations for ${{ github.event.inputs.environment }}
|
||||||
uses: 18f/cg-deploy-action@main
|
uses: cloud-gov/cg-cli-tools@main
|
||||||
with:
|
with:
|
||||||
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
||||||
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
||||||
cf_org: cisa-dotgov
|
cf_org: cisa-dotgov
|
||||||
cf_space: ${{ github.event.inputs.environment }}
|
cf_space: ${{ github.event.inputs.environment }}
|
||||||
full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py migrate' --name migrate"
|
cf_command: "run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py migrate' --name migrate"
|
||||||
|
|
||||||
- name: Load fake data for ${{ github.event.inputs.environment }}
|
- name: Load fake data for ${{ github.event.inputs.environment }}
|
||||||
uses: 18f/cg-deploy-action@main
|
uses: cloud-gov/cg-cli-tools@main
|
||||||
with:
|
with:
|
||||||
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
cf_username: ${{ secrets[env.CF_USERNAME] }}
|
||||||
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
cf_password: ${{ secrets[env.CF_PASSWORD] }}
|
||||||
cf_org: cisa-dotgov
|
cf_org: cisa-dotgov
|
||||||
cf_space: ${{ github.event.inputs.environment }}
|
cf_space: ${{ github.event.inputs.environment }}
|
||||||
full_command: "cf run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py load' --name loaddata"
|
cf_command: "run-task getgov-${{ github.event.inputs.environment }} --command 'python manage.py load' --name loaddata"
|
||||||
|
|
|
@ -8,6 +8,7 @@ from django.test import RequestFactory
|
||||||
from ..views import available, check_domain_available
|
from ..views import available, check_domain_available
|
||||||
from .common import less_console_noise
|
from .common import less_console_noise
|
||||||
from registrar.tests.common import MockEppLib
|
from registrar.tests.common import MockEppLib
|
||||||
|
from registrar.utility.errors import GenericError, GenericErrorCodes
|
||||||
from unittest.mock import call
|
from unittest.mock import call
|
||||||
|
|
||||||
from epplibwrapper import (
|
from epplibwrapper import (
|
||||||
|
@ -100,16 +101,25 @@ class AvailableViewTest(MockEppLib):
|
||||||
response = available(request, domain="igorville")
|
response = available(request, domain="igorville")
|
||||||
self.assertTrue(json.loads(response.content)["available"])
|
self.assertTrue(json.loads(response.content)["available"])
|
||||||
|
|
||||||
def test_error_handling(self):
|
def test_bad_string_handling(self):
|
||||||
"""Calling with bad strings raises an error."""
|
"""Calling with bad strings returns unavailable."""
|
||||||
bad_string = "blah!;"
|
bad_string = "blah!;"
|
||||||
request = self.factory.get(API_BASE_PATH + bad_string)
|
request = self.factory.get(API_BASE_PATH + bad_string)
|
||||||
request.user = self.user
|
request.user = self.user
|
||||||
response = available(request, domain=bad_string)
|
response = available(request, domain=bad_string)
|
||||||
self.assertFalse(json.loads(response.content)["available"])
|
self.assertFalse(json.loads(response.content)["available"])
|
||||||
# domain set to raise error returns false for availability
|
|
||||||
error_domain_available = available(request, "errordomain.gov")
|
def test_error_handling(self):
|
||||||
self.assertFalse(json.loads(error_domain_available.content)["available"])
|
"""Error thrown while calling availabilityAPI returns error."""
|
||||||
|
request = self.factory.get(API_BASE_PATH + "errordomain.gov")
|
||||||
|
request.user = self.user
|
||||||
|
# domain set to raise error returns false for availability and error message
|
||||||
|
error_domain_response = available(request, domain="errordomain.gov")
|
||||||
|
self.assertFalse(json.loads(error_domain_response.content)["available"])
|
||||||
|
self.assertEqual(
|
||||||
|
GenericError.get_error_message(GenericErrorCodes.CANNOT_CONTACT_REGISTRY),
|
||||||
|
json.loads(error_domain_response.content)["message"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AvailableAPITest(MockEppLib):
|
class AvailableAPITest(MockEppLib):
|
||||||
|
|
|
@ -5,6 +5,7 @@ from django.http import JsonResponse
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from registrar.templatetags.url_helpers import public_site_url
|
from registrar.templatetags.url_helpers import public_site_url
|
||||||
|
from registrar.utility.errors import GenericError, GenericErrorCodes
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
@ -30,7 +31,7 @@ DOMAIN_API_MESSAGES = {
|
||||||
),
|
),
|
||||||
"invalid": "Enter a domain using only letters, numbers, or hyphens (though we don't recommend using hyphens).",
|
"invalid": "Enter a domain using only letters, numbers, or hyphens (though we don't recommend using hyphens).",
|
||||||
"success": "That domain is available!",
|
"success": "That domain is available!",
|
||||||
"error": "Error finding domain availability.",
|
"error": GenericError.get_error_message(GenericErrorCodes.CANNOT_CONTACT_REGISTRY),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,17 +64,14 @@ def check_domain_available(domain):
|
||||||
|
|
||||||
The given domain is lowercased to match against the domains list. If the
|
The given domain is lowercased to match against the domains list. If the
|
||||||
given domain doesn't end with .gov, ".gov" is added when looking for
|
given domain doesn't end with .gov, ".gov" is added when looking for
|
||||||
a match.
|
a match. If check fails, throws a RegistryError.
|
||||||
"""
|
"""
|
||||||
Domain = apps.get_model("registrar.Domain")
|
Domain = apps.get_model("registrar.Domain")
|
||||||
try:
|
|
||||||
if domain.endswith(".gov"):
|
if domain.endswith(".gov"):
|
||||||
return Domain.available(domain)
|
return Domain.available(domain)
|
||||||
else:
|
else:
|
||||||
# domain search string doesn't end with .gov, add it on here
|
# domain search string doesn't end with .gov, add it on here
|
||||||
return Domain.available(domain + ".gov")
|
return Domain.available(domain + ".gov")
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
|
|
|
@ -333,6 +333,14 @@ class WebsiteAdmin(ListHeaderAdmin):
|
||||||
class UserDomainRoleAdmin(ListHeaderAdmin):
|
class UserDomainRoleAdmin(ListHeaderAdmin):
|
||||||
"""Custom user domain role admin class."""
|
"""Custom user domain role admin class."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Contains meta information about this class"""
|
||||||
|
|
||||||
|
model = models.UserDomainRole
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
_meta = Meta()
|
||||||
|
|
||||||
# Columns
|
# Columns
|
||||||
list_display = [
|
list_display = [
|
||||||
"user",
|
"user",
|
||||||
|
@ -344,10 +352,11 @@ class UserDomainRoleAdmin(ListHeaderAdmin):
|
||||||
search_fields = [
|
search_fields = [
|
||||||
"user__first_name",
|
"user__first_name",
|
||||||
"user__last_name",
|
"user__last_name",
|
||||||
|
"user__email",
|
||||||
"domain__name",
|
"domain__name",
|
||||||
"role",
|
"role",
|
||||||
]
|
]
|
||||||
search_help_text = "Search by user, domain, or role."
|
search_help_text = "Search by firstname, lastname, email, domain, or role."
|
||||||
|
|
||||||
autocomplete_fields = ["user", "domain"]
|
autocomplete_fields = ["user", "domain"]
|
||||||
|
|
||||||
|
|
|
@ -399,6 +399,8 @@ class AlternativeDomainForm(RegistrarForm):
|
||||||
raise forms.ValidationError(DOMAIN_API_MESSAGES["extra_dots"], code="extra_dots")
|
raise forms.ValidationError(DOMAIN_API_MESSAGES["extra_dots"], code="extra_dots")
|
||||||
except errors.DomainUnavailableError:
|
except errors.DomainUnavailableError:
|
||||||
raise forms.ValidationError(DOMAIN_API_MESSAGES["unavailable"], code="unavailable")
|
raise forms.ValidationError(DOMAIN_API_MESSAGES["unavailable"], code="unavailable")
|
||||||
|
except errors.RegistrySystemError:
|
||||||
|
raise forms.ValidationError(DOMAIN_API_MESSAGES["error"], code="error")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise forms.ValidationError(DOMAIN_API_MESSAGES["invalid"], code="invalid")
|
raise forms.ValidationError(DOMAIN_API_MESSAGES["invalid"], code="invalid")
|
||||||
return validated
|
return validated
|
||||||
|
@ -484,6 +486,8 @@ class DotGovDomainForm(RegistrarForm):
|
||||||
raise forms.ValidationError(DOMAIN_API_MESSAGES["extra_dots"], code="extra_dots")
|
raise forms.ValidationError(DOMAIN_API_MESSAGES["extra_dots"], code="extra_dots")
|
||||||
except errors.DomainUnavailableError:
|
except errors.DomainUnavailableError:
|
||||||
raise forms.ValidationError(DOMAIN_API_MESSAGES["unavailable"], code="unavailable")
|
raise forms.ValidationError(DOMAIN_API_MESSAGES["unavailable"], code="unavailable")
|
||||||
|
except errors.RegistrySystemError:
|
||||||
|
raise forms.ValidationError(DOMAIN_API_MESSAGES["error"], code="error")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise forms.ValidationError(DOMAIN_API_MESSAGES["invalid"], code="invalid")
|
raise forms.ValidationError(DOMAIN_API_MESSAGES["invalid"], code="invalid")
|
||||||
return validated
|
return validated
|
||||||
|
|
|
@ -176,6 +176,7 @@ class Command(BaseCommand):
|
||||||
"clienthold": TransitionDomain.StatusChoices.ON_HOLD,
|
"clienthold": TransitionDomain.StatusChoices.ON_HOLD,
|
||||||
"created": TransitionDomain.StatusChoices.READY,
|
"created": TransitionDomain.StatusChoices.READY,
|
||||||
"ok": TransitionDomain.StatusChoices.READY,
|
"ok": TransitionDomain.StatusChoices.READY,
|
||||||
|
"unknown": TransitionDomain.StatusChoices.UNKNOWN,
|
||||||
}
|
}
|
||||||
mapped_status = status_maps.get(status_to_map)
|
mapped_status = status_maps.get(status_to_map)
|
||||||
return mapped_status
|
return mapped_status
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 4.2.7 on 2023-12-01 17:45
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0047_transitiondomain_address_line_transitiondomain_city_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="transitiondomain",
|
||||||
|
name="status",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[("ready", "Ready"), ("on hold", "On Hold"), ("unknown", "Unknown")],
|
||||||
|
default="ready",
|
||||||
|
help_text="domain status during the transfer",
|
||||||
|
max_length=255,
|
||||||
|
verbose_name="Status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -5,6 +5,7 @@ from .utility.time_stamped_model import TimeStampedModel
|
||||||
class StatusChoices(models.TextChoices):
|
class StatusChoices(models.TextChoices):
|
||||||
READY = "ready", "Ready"
|
READY = "ready", "Ready"
|
||||||
ON_HOLD = "on hold", "On Hold"
|
ON_HOLD = "on hold", "On Hold"
|
||||||
|
UNKNOWN = "unknown", "Unknown"
|
||||||
|
|
||||||
|
|
||||||
class TransitionDomain(TimeStampedModel):
|
class TransitionDomain(TimeStampedModel):
|
||||||
|
|
|
@ -2,6 +2,7 @@ import re
|
||||||
|
|
||||||
from api.views import check_domain_available
|
from api.views import check_domain_available
|
||||||
from registrar.utility import errors
|
from registrar.utility import errors
|
||||||
|
from epplibwrapper.errors import RegistryError
|
||||||
|
|
||||||
|
|
||||||
class DomainHelper:
|
class DomainHelper:
|
||||||
|
@ -29,10 +30,7 @@ class DomainHelper:
|
||||||
if not isinstance(domain, str):
|
if not isinstance(domain, str):
|
||||||
raise ValueError("Domain name must be a string")
|
raise ValueError("Domain name must be a string")
|
||||||
domain = domain.lower().strip()
|
domain = domain.lower().strip()
|
||||||
if domain == "":
|
if domain == "" and not blank_ok:
|
||||||
if blank_ok:
|
|
||||||
return domain
|
|
||||||
else:
|
|
||||||
raise errors.BlankValueError()
|
raise errors.BlankValueError()
|
||||||
if domain.endswith(".gov"):
|
if domain.endswith(".gov"):
|
||||||
domain = domain[:-4]
|
domain = domain[:-4]
|
||||||
|
@ -40,8 +38,11 @@ class DomainHelper:
|
||||||
raise errors.ExtraDotsError()
|
raise errors.ExtraDotsError()
|
||||||
if not DomainHelper.string_could_be_domain(domain + ".gov"):
|
if not DomainHelper.string_could_be_domain(domain + ".gov"):
|
||||||
raise ValueError()
|
raise ValueError()
|
||||||
|
try:
|
||||||
if not check_domain_available(domain):
|
if not check_domain_available(domain):
|
||||||
raise errors.DomainUnavailableError()
|
raise errors.DomainUnavailableError()
|
||||||
|
except RegistryError as err:
|
||||||
|
raise errors.RegistrySystemError() from err
|
||||||
return domain
|
return domain
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -22,6 +22,14 @@
|
||||||
{% include "includes/form_messages.html" %}
|
{% include "includes/form_messages.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% if pending_requests_message %}
|
||||||
|
<div class="usa-alert usa-alert--info margin-bottom-3">
|
||||||
|
<div class="usa-alert__body">
|
||||||
|
{{ pending_requests_message }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% block form_errors %}
|
{% block form_errors %}
|
||||||
{% comment %}
|
{% comment %}
|
||||||
to make sense of this loop, consider that
|
to make sense of this loop, consider that
|
||||||
|
@ -66,6 +74,13 @@
|
||||||
value="next"
|
value="next"
|
||||||
class="usa-button"
|
class="usa-button"
|
||||||
>Save and continue</button>
|
>Save and continue</button>
|
||||||
|
{% elif pending_requests_exist %}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
name="submit_button"
|
||||||
|
value="save_and_return"
|
||||||
|
class="usa-button usa-button--outline"
|
||||||
|
>Save and return to manage your domains</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
@ -40,9 +40,9 @@
|
||||||
>
|
>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div id="enable-dnssec">
|
<div id="enable-dnssec">
|
||||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
<div class="usa-alert usa-alert--info">
|
||||||
<div class="usa-alert__body">
|
<div class="usa-alert__body">
|
||||||
It is strongly recommended that you only enable DNSSEC if you know how to set it up properly at your hosting service. If you make a mistake, it could cause your domain name to stop working.
|
<p class="margin-y-0">It is strongly recommended that you only enable DNSSEC if you know how to set it up properly at your hosting service. If you make a mistake, it could cause your domain name to stop working.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="{% url 'domain-dns-dnssec-dsdata' pk=domain.id %}" class="usa-button">Enable DNSSEC</a>
|
<a href="{% url 'domain-dns-dnssec-dsdata' pk=domain.id %}" class="usa-button">Enable DNSSEC</a>
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
<p>Add a name server record by entering the address (e.g., ns1.nameserver.com) in the name server fields below. You must add at least two name servers (13 max).</p>
|
<p>Add a name server record by entering the address (e.g., ns1.nameserver.com) in the name server fields below. You must add at least two name servers (13 max).</p>
|
||||||
|
|
||||||
<div class="usa-alert usa-alert--slim usa-alert--info">
|
<div class="usa-alert usa-alert--info">
|
||||||
<div class="usa-alert__body">
|
<div class="usa-alert__body">
|
||||||
<p class="margin-top-0">Add an IP address only when your name server's address includes your domain name (e.g., if your domain name is “example.gov” and your name server is “ns1.example.gov,” then an IP address is required). Multiple IP addresses must be separated with commas.</p>
|
<p class="margin-top-0">Add an IP address only when your name server's address includes your domain name (e.g., if your domain name is “example.gov” and your name server is “ns1.example.gov,” then an IP address is required). Multiple IP addresses must be separated with commas.</p>
|
||||||
<p class="margin-bottom-0">This step is uncommon unless you self-host your DNS or use custom addresses for your nameserver.</p>
|
<p class="margin-bottom-0">This step is uncommon unless you self-host your DNS or use custom addresses for your nameserver.</p>
|
||||||
|
|
|
@ -2,4 +2,4 @@ Anomaly.gov|muahaha|
|
||||||
TestDomain.gov|ok|
|
TestDomain.gov|ok|
|
||||||
FakeWebsite1.gov|serverHold|
|
FakeWebsite1.gov|serverHold|
|
||||||
FakeWebsite2.gov|Hold|
|
FakeWebsite2.gov|Hold|
|
||||||
FakeWebsite3.gov|ok|
|
FakeWebsite3.gov|unknown|
|
||||||
|
|
|
@ -13,6 +13,7 @@ from registrar.admin import (
|
||||||
MyUserAdmin,
|
MyUserAdmin,
|
||||||
AuditedAdmin,
|
AuditedAdmin,
|
||||||
ContactAdmin,
|
ContactAdmin,
|
||||||
|
UserDomainRoleAdmin,
|
||||||
)
|
)
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
Domain,
|
Domain,
|
||||||
|
@ -21,6 +22,7 @@ from registrar.models import (
|
||||||
User,
|
User,
|
||||||
DomainInvitation,
|
DomainInvitation,
|
||||||
)
|
)
|
||||||
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
from .common import (
|
from .common import (
|
||||||
completed_application,
|
completed_application,
|
||||||
generic_domain_object,
|
generic_domain_object,
|
||||||
|
@ -886,6 +888,86 @@ class DomainInvitationAdminTest(TestCase):
|
||||||
self.assertContains(response, retrieved_html, count=1)
|
self.assertContains(response, retrieved_html, count=1)
|
||||||
|
|
||||||
|
|
||||||
|
class UserDomainRoleAdminTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
"""Setup environment for a mock admin user"""
|
||||||
|
self.site = AdminSite()
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.admin = ListHeaderAdmin(model=UserDomainRoleAdmin, admin_site=None)
|
||||||
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
self.superuser = create_superuser()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Delete all Users, Domains, and UserDomainRoles"""
|
||||||
|
User.objects.all().delete()
|
||||||
|
Domain.objects.all().delete()
|
||||||
|
UserDomainRole.objects.all().delete()
|
||||||
|
|
||||||
|
def test_email_not_in_search(self):
|
||||||
|
"""Tests the search bar in Django Admin for UserDomainRoleAdmin.
|
||||||
|
Should return no results for an invalid email."""
|
||||||
|
# Have to get creative to get past linter
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
fake_user = User.objects.create(
|
||||||
|
username="dummyuser", first_name="Stewart", last_name="Jones", email="AntarcticPolarBears@example.com"
|
||||||
|
)
|
||||||
|
fake_domain = Domain.objects.create(name="test123")
|
||||||
|
UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager")
|
||||||
|
# Make the request using the Client class
|
||||||
|
# which handles CSRF
|
||||||
|
# Follow=True handles the redirect
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/userdomainrole/",
|
||||||
|
{
|
||||||
|
"q": "testmail@igorville.com",
|
||||||
|
},
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert that the query is added to the extra_context
|
||||||
|
self.assertIn("search_query", response.context)
|
||||||
|
# Assert the content of filters and search_query
|
||||||
|
search_query = response.context["search_query"]
|
||||||
|
self.assertEqual(search_query, "testmail@igorville.com")
|
||||||
|
|
||||||
|
# We only need to check for the end of the HTML string
|
||||||
|
self.assertNotContains(response, "Stewart Jones AntarcticPolarBears@example.com</a></th>")
|
||||||
|
|
||||||
|
def test_email_in_search(self):
|
||||||
|
"""Tests the search bar in Django Admin for UserDomainRoleAdmin.
|
||||||
|
Should return results for an valid email."""
|
||||||
|
# Have to get creative to get past linter
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
fake_user = User.objects.create(
|
||||||
|
username="dummyuser", first_name="Joe", last_name="Jones", email="AntarcticPolarBears@example.com"
|
||||||
|
)
|
||||||
|
fake_domain = Domain.objects.create(name="fake")
|
||||||
|
UserDomainRole.objects.create(user=fake_user, domain=fake_domain, role="manager")
|
||||||
|
# Make the request using the Client class
|
||||||
|
# which handles CSRF
|
||||||
|
# Follow=True handles the redirect
|
||||||
|
response = self.client.get(
|
||||||
|
"/admin/registrar/userdomainrole/",
|
||||||
|
{
|
||||||
|
"q": "AntarcticPolarBears@example.com",
|
||||||
|
},
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert that the query is added to the extra_context
|
||||||
|
self.assertIn("search_query", response.context)
|
||||||
|
|
||||||
|
search_query = response.context["search_query"]
|
||||||
|
self.assertEqual(search_query, "AntarcticPolarBears@example.com")
|
||||||
|
|
||||||
|
# We only need to check for the end of the HTML string
|
||||||
|
self.assertContains(response, "Joe Jones AntarcticPolarBears@example.com</a></th>", count=1)
|
||||||
|
|
||||||
|
|
||||||
class ListHeaderAdminTest(TestCase):
|
class ListHeaderAdminTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.site = AdminSite()
|
self.site = AdminSite()
|
||||||
|
|
|
@ -193,6 +193,17 @@ class TestOrganizationMigration(TestCase):
|
||||||
|
|
||||||
self.assertEqual(transition, expected_transition_domain)
|
self.assertEqual(transition, expected_transition_domain)
|
||||||
|
|
||||||
|
def test_transition_domain_status_unknown(self):
|
||||||
|
"""
|
||||||
|
Test that a domain in unknown status can be loaded
|
||||||
|
""" # noqa - E501 (harder to read)
|
||||||
|
# == First, parse all existing data == #
|
||||||
|
self.run_load_domains()
|
||||||
|
self.run_transfer_domains()
|
||||||
|
|
||||||
|
domain_object = Domain.objects.get(name="fakewebsite3.gov")
|
||||||
|
self.assertEqual(domain_object.state, Domain.State.UNKNOWN)
|
||||||
|
|
||||||
def test_load_organization_data_domain_information(self):
|
def test_load_organization_data_domain_information(self):
|
||||||
"""
|
"""
|
||||||
This test verifies the functionality of the load_organization_data method.
|
This test verifies the functionality of the load_organization_data method.
|
||||||
|
|
|
@ -144,6 +144,18 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
result = page.form.submit()
|
result = page.form.submit()
|
||||||
self.assertIn("What kind of U.S.-based government organization do you represent?", result)
|
self.assertIn("What kind of U.S.-based government organization do you represent?", result)
|
||||||
|
|
||||||
|
def test_application_multiple_applications_exist(self):
|
||||||
|
"""Test that an info message appears when user has multiple applications already"""
|
||||||
|
# create and submit an application
|
||||||
|
application = completed_application(user=self.user)
|
||||||
|
application.submit()
|
||||||
|
application.save()
|
||||||
|
|
||||||
|
# now, attempt to create another one
|
||||||
|
with less_console_noise():
|
||||||
|
page = self.app.get("/register/").follow()
|
||||||
|
self.assertContains(page, "You cannot submit this request yet")
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_application_form_submission(self):
|
def test_application_form_submission(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -13,6 +13,10 @@ class DomainUnavailableError(ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RegistrySystemError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ActionNotAllowed(Exception):
|
class ActionNotAllowed(Exception):
|
||||||
"""User accessed an action that is not
|
"""User accessed an action that is not
|
||||||
allowed by the current state"""
|
allowed by the current state"""
|
||||||
|
@ -42,7 +46,7 @@ class GenericError(Exception):
|
||||||
GenericErrorCodes.CANNOT_CONTACT_REGISTRY: """
|
GenericErrorCodes.CANNOT_CONTACT_REGISTRY: """
|
||||||
We’re experiencing a system connection error. Please wait a few minutes
|
We’re experiencing a system connection error. Please wait a few minutes
|
||||||
and try again. If you continue to receive this error after a few tries,
|
and try again. If you continue to receive this error after a few tries,
|
||||||
contact help@get.gov
|
contact help@get.gov.
|
||||||
""",
|
""",
|
||||||
GenericErrorCodes.GENERIC_ERROR: ("Value entered was wrong."),
|
GenericErrorCodes.GENERIC_ERROR: ("Value entered was wrong."),
|
||||||
}
|
}
|
||||||
|
@ -56,6 +60,10 @@ contact help@get.gov
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.message}"
|
return f"{self.message}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_error_message(self, code=None):
|
||||||
|
return self._error_mapping.get(code)
|
||||||
|
|
||||||
|
|
||||||
class NameserverErrorCodes(IntEnum):
|
class NameserverErrorCodes(IntEnum):
|
||||||
"""Used in the NameserverError class for
|
"""Used in the NameserverError class for
|
||||||
|
|
|
@ -3,6 +3,7 @@ import logging
|
||||||
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.urls import resolve, reverse
|
from django.urls import resolve, reverse
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
@ -218,6 +219,23 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
||||||
self.steps.current = current_url
|
self.steps.current = current_url
|
||||||
context = self.get_context_data()
|
context = self.get_context_data()
|
||||||
context["forms"] = self.get_forms()
|
context["forms"] = self.get_forms()
|
||||||
|
|
||||||
|
# if pending requests exist and user does not have approved domains,
|
||||||
|
# present message that domain application cannot be submitted
|
||||||
|
pending_requests = self.pending_requests()
|
||||||
|
if len(pending_requests) > 0:
|
||||||
|
message_header = "You cannot submit this request yet"
|
||||||
|
message_content = (
|
||||||
|
f"<h4 class='usa-alert__heading'>{message_header}</h4> "
|
||||||
|
"<p class='margin-bottom-0'>New domain requests cannot be submitted until we have finished "
|
||||||
|
f"reviewing your pending request: <strong>{pending_requests[0].requested_domain}</strong>. "
|
||||||
|
"You can continue to fill out this request and save it as a draft to be submitted later. "
|
||||||
|
f"<a class='usa-link' href='{reverse('home')}'>View your pending requests.</a></p>"
|
||||||
|
)
|
||||||
|
context["pending_requests_message"] = mark_safe(message_content) # nosec
|
||||||
|
|
||||||
|
context["pending_requests_exist"] = len(pending_requests) > 0
|
||||||
|
|
||||||
return render(request, self.template_name, context)
|
return render(request, self.template_name, context)
|
||||||
|
|
||||||
def get_all_forms(self, **kwargs) -> list:
|
def get_all_forms(self, **kwargs) -> list:
|
||||||
|
@ -266,6 +284,37 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
||||||
|
|
||||||
return instantiated
|
return instantiated
|
||||||
|
|
||||||
|
def pending_requests(self):
|
||||||
|
"""return an array of pending requests if user has pending requests
|
||||||
|
and no approved requests"""
|
||||||
|
if self.approved_applications_exist() or self.approved_domains_exist():
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
return self.pending_applications()
|
||||||
|
|
||||||
|
def approved_applications_exist(self):
|
||||||
|
"""Checks if user is creator of applications with APPROVED status"""
|
||||||
|
approved_application_count = DomainApplication.objects.filter(
|
||||||
|
creator=self.request.user, status=DomainApplication.APPROVED
|
||||||
|
).count()
|
||||||
|
return approved_application_count > 0
|
||||||
|
|
||||||
|
def approved_domains_exist(self):
|
||||||
|
"""Checks if user has permissions on approved domains
|
||||||
|
|
||||||
|
This additional check is necessary to account for domains which were migrated
|
||||||
|
and do not have an application"""
|
||||||
|
return self.request.user.permissions.count() > 0
|
||||||
|
|
||||||
|
def pending_applications(self):
|
||||||
|
"""Returns a List of user's applications with one of the following states:
|
||||||
|
SUBMITTED, IN_REVIEW, ACTION_NEEDED"""
|
||||||
|
# if the current application has ACTION_NEEDED status, this check should not be performed
|
||||||
|
if self.application.status == DomainApplication.ACTION_NEEDED:
|
||||||
|
return []
|
||||||
|
check_statuses = [DomainApplication.SUBMITTED, DomainApplication.IN_REVIEW, DomainApplication.ACTION_NEEDED]
|
||||||
|
return DomainApplication.objects.filter(creator=self.request.user, status__in=check_statuses)
|
||||||
|
|
||||||
def get_context_data(self):
|
def get_context_data(self):
|
||||||
"""Define context for access on all wizard pages."""
|
"""Define context for access on all wizard pages."""
|
||||||
return {
|
return {
|
||||||
|
@ -328,6 +377,10 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
||||||
if button == "save":
|
if button == "save":
|
||||||
messages.success(request, "Your progress has been saved!")
|
messages.success(request, "Your progress has been saved!")
|
||||||
return self.goto(self.steps.current)
|
return self.goto(self.steps.current)
|
||||||
|
# if user opted to save progress and return,
|
||||||
|
# return them to the home page
|
||||||
|
if button == "save_and_return":
|
||||||
|
return HttpResponseRedirect(reverse("home"))
|
||||||
# otherwise, proceed as normal
|
# otherwise, proceed as normal
|
||||||
return self.goto_next_step()
|
return self.goto_next_step()
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,9 @@
|
||||||
10038 OUTOFSCOPE http://app:8080/withdrawconfirmed
|
10038 OUTOFSCOPE http://app:8080/withdrawconfirmed
|
||||||
10038 OUTOFSCOPE http://app:8080/dns
|
10038 OUTOFSCOPE http://app:8080/dns
|
||||||
10038 OUTOFSCOPE http://app:8080/dnssec
|
10038 OUTOFSCOPE http://app:8080/dnssec
|
||||||
|
10038 OUTOFSCOPE http://app:8080/dns/nameservers
|
||||||
10038 OUTOFSCOPE http://app:8080/dns/dnssec
|
10038 OUTOFSCOPE http://app:8080/dns/dnssec
|
||||||
|
10038 OUTOFSCOPE http://app:8080/dns/dnssec/dsdata
|
||||||
# This URL always returns 404, so include it as well.
|
# This URL always returns 404, so include it as well.
|
||||||
10038 OUTOFSCOPE http://app:8080/todo
|
10038 OUTOFSCOPE http://app:8080/todo
|
||||||
# OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers
|
# OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue