mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-24 03:30:50 +02:00
Merge branch 'main' into za/1484-domain-manager-delete
This commit is contained in:
commit
12c8cd7c19
26 changed files with 230 additions and 66 deletions
1
.github/workflows/migrate.yaml
vendored
1
.github/workflows/migrate.yaml
vendored
|
@ -26,7 +26,6 @@ on:
|
||||||
- rb
|
- rb
|
||||||
- ko
|
- ko
|
||||||
- ab
|
- ab
|
||||||
- bl
|
|
||||||
- rjm
|
- rjm
|
||||||
- dk
|
- dk
|
||||||
|
|
||||||
|
|
1
.github/workflows/reset-db.yaml
vendored
1
.github/workflows/reset-db.yaml
vendored
|
@ -26,7 +26,6 @@ on:
|
||||||
- rb
|
- rb
|
||||||
- ko
|
- ko
|
||||||
- ab
|
- ab
|
||||||
- bl
|
|
||||||
- rjm
|
- rjm
|
||||||
- dk
|
- dk
|
||||||
|
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
---
|
|
||||||
applications:
|
|
||||||
- name: getgov-bl
|
|
||||||
buildpacks:
|
|
||||||
- python_buildpack
|
|
||||||
path: ../../src
|
|
||||||
instances: 1
|
|
||||||
memory: 512M
|
|
||||||
stack: cflinuxfs4
|
|
||||||
timeout: 180
|
|
||||||
command: ./run.sh
|
|
||||||
health-check-type: http
|
|
||||||
health-check-http-endpoint: /health
|
|
||||||
health-check-invocation-timeout: 40
|
|
||||||
env:
|
|
||||||
# Send stdout and stderr straight to the terminal without buffering
|
|
||||||
PYTHONUNBUFFERED: yup
|
|
||||||
# Tell Django where to find its configuration
|
|
||||||
DJANGO_SETTINGS_MODULE: registrar.config.settings
|
|
||||||
# Tell Django where it is being hosted
|
|
||||||
DJANGO_BASE_URL: https://getgov-bl.app.cloud.gov
|
|
||||||
# Tell Django how much stuff to log
|
|
||||||
DJANGO_LOG_LEVEL: INFO
|
|
||||||
# default public site location
|
|
||||||
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
|
||||||
# Flag to disable/enable features in prod environments
|
|
||||||
IS_PRODUCTION: False
|
|
||||||
routes:
|
|
||||||
- route: getgov-bl.app.cloud.gov
|
|
||||||
services:
|
|
||||||
- getgov-credentials
|
|
||||||
- getgov-bl-database
|
|
|
@ -33,6 +33,10 @@ class AuthenticationFailed(OIDCException):
|
||||||
friendly_message = "This login attempt didn't work."
|
friendly_message = "This login attempt didn't work."
|
||||||
|
|
||||||
|
|
||||||
|
class NoStateDefined(OIDCException):
|
||||||
|
friendly_message = "The session state is None."
|
||||||
|
|
||||||
|
|
||||||
class InternalError(OIDCException):
|
class InternalError(OIDCException):
|
||||||
status = status.INTERNAL_SERVER_ERROR
|
status = status.INTERNAL_SERVER_ERROR
|
||||||
friendly_message = "The system broke while trying to log you in."
|
friendly_message = "The system broke while trying to log you in."
|
||||||
|
|
|
@ -183,6 +183,8 @@ class Client(oic.Client):
|
||||||
if authn_response["state"] != session.get("state", None):
|
if authn_response["state"] != session.get("state", None):
|
||||||
# this most likely means the user's Django session vanished
|
# this most likely means the user's Django session vanished
|
||||||
logger.error("Received state not the same as expected for %s" % state)
|
logger.error("Received state not the same as expected for %s" % state)
|
||||||
|
if session.get("state", None) is None:
|
||||||
|
raise o_e.NoStateDefined()
|
||||||
raise o_e.AuthenticationFailed(locator=state)
|
raise o_e.AuthenticationFailed(locator=state)
|
||||||
|
|
||||||
if self.behaviour.get("response_type") == "code":
|
if self.behaviour.get("response_type") == "code":
|
||||||
|
@ -272,6 +274,11 @@ class Client(oic.Client):
|
||||||
|
|
||||||
super(Client, self).store_response(resp, info)
|
super(Client, self).store_response(resp, info)
|
||||||
|
|
||||||
|
def get_default_acr_value(self):
|
||||||
|
"""returns the acr_value from settings
|
||||||
|
this helper function is called from djangooidc views"""
|
||||||
|
return self.behaviour.get("acr_value")
|
||||||
|
|
||||||
def get_step_up_acr_value(self):
|
def get_step_up_acr_value(self):
|
||||||
"""returns the step_up_acr_value from settings
|
"""returns the step_up_acr_value from settings
|
||||||
this helper function is called from djangooidc views"""
|
this helper function is called from djangooidc views"""
|
||||||
|
|
|
@ -3,6 +3,8 @@ from unittest.mock import MagicMock, patch
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.test import Client, TestCase, RequestFactory
|
from django.test import Client, TestCase, RequestFactory
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from djangooidc.exceptions import NoStateDefined
|
||||||
from ..views import login_callback
|
from ..views import login_callback
|
||||||
|
|
||||||
from .common import less_console_noise
|
from .common import less_console_noise
|
||||||
|
@ -17,6 +19,9 @@ class ViewsTest(TestCase):
|
||||||
def say_hi(*args):
|
def say_hi(*args):
|
||||||
return HttpResponse("Hi")
|
return HttpResponse("Hi")
|
||||||
|
|
||||||
|
def create_acr(*args):
|
||||||
|
return "any string"
|
||||||
|
|
||||||
def user_info(*args):
|
def user_info(*args):
|
||||||
return {
|
return {
|
||||||
"sub": "TEST",
|
"sub": "TEST",
|
||||||
|
@ -34,6 +39,7 @@ class ViewsTest(TestCase):
|
||||||
callback_url = reverse("openid_login_callback")
|
callback_url = reverse("openid_login_callback")
|
||||||
# mock
|
# mock
|
||||||
mock_client.create_authn_request.side_effect = self.say_hi
|
mock_client.create_authn_request.side_effect = self.say_hi
|
||||||
|
mock_client.get_default_acr_value.side_effect = self.create_acr
|
||||||
# test
|
# test
|
||||||
response = self.client.get(reverse("login"), {"next": callback_url})
|
response = self.client.get(reverse("login"), {"next": callback_url})
|
||||||
# assert
|
# assert
|
||||||
|
@ -53,6 +59,19 @@ class ViewsTest(TestCase):
|
||||||
self.assertTemplateUsed(response, "500.html")
|
self.assertTemplateUsed(response, "500.html")
|
||||||
self.assertIn("Server error", response.content.decode("utf-8"))
|
self.assertIn("Server error", response.content.decode("utf-8"))
|
||||||
|
|
||||||
|
def test_callback_with_no_session_state(self, mock_client):
|
||||||
|
"""If the local session is None (ie the server restarted while user was logged out),
|
||||||
|
we do not throw an exception. Rather, we attempt to login again."""
|
||||||
|
# mock
|
||||||
|
mock_client.get_default_acr_value.side_effect = self.create_acr
|
||||||
|
mock_client.callback.side_effect = NoStateDefined()
|
||||||
|
# test
|
||||||
|
with less_console_noise():
|
||||||
|
response = self.client.get(reverse("openid_login_callback"))
|
||||||
|
# assert
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/")
|
||||||
|
|
||||||
def test_login_callback_reads_next(self, mock_client):
|
def test_login_callback_reads_next(self, mock_client):
|
||||||
# setup
|
# setup
|
||||||
session = self.client.session
|
session = self.client.session
|
||||||
|
|
|
@ -55,6 +55,10 @@ def error_page(request, error):
|
||||||
|
|
||||||
def openid(request):
|
def openid(request):
|
||||||
"""Redirect the user to an authentication provider (OP)."""
|
"""Redirect the user to an authentication provider (OP)."""
|
||||||
|
|
||||||
|
# If the session reset because of a server restart, attempt to login again
|
||||||
|
request.session["acr_value"] = CLIENT.get_default_acr_value()
|
||||||
|
|
||||||
request.session["next"] = request.GET.get("next", "/")
|
request.session["next"] = request.GET.get("next", "/")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -78,9 +82,13 @@ def login_callback(request):
|
||||||
if user:
|
if user:
|
||||||
login(request, user)
|
login(request, user)
|
||||||
logger.info("Successfully logged in user %s" % user)
|
logger.info("Successfully logged in user %s" % user)
|
||||||
|
# Double login bug (1507)?
|
||||||
return redirect(request.session.get("next", "/"))
|
return redirect(request.session.get("next", "/"))
|
||||||
else:
|
else:
|
||||||
raise o_e.BannedUser()
|
raise o_e.BannedUser()
|
||||||
|
except o_e.NoStateDefined as nsd_err:
|
||||||
|
logger.warning(f"No State Defined: {nsd_err}")
|
||||||
|
return redirect(request.session.get("next", "/"))
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
return error_page(request, err)
|
return error_page(request, err)
|
||||||
|
|
||||||
|
|
|
@ -610,7 +610,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
||||||
),
|
),
|
||||||
("Anything else?", {"fields": ["anything_else"]}),
|
("Anything else?", {"fields": ["anything_else"]}),
|
||||||
(
|
(
|
||||||
"Requirements for operating .gov domains",
|
"Requirements for operating a .gov domain",
|
||||||
{"fields": ["is_policy_acknowledged"]},
|
{"fields": ["is_policy_acknowledged"]},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -779,7 +779,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
),
|
),
|
||||||
("Anything else?", {"fields": ["anything_else"]}),
|
("Anything else?", {"fields": ["anything_else"]}),
|
||||||
(
|
(
|
||||||
"Requirements for operating .gov domains",
|
"Requirements for operating a .gov domain",
|
||||||
{"fields": ["is_policy_acknowledged"]},
|
{"fields": ["is_policy_acknowledged"]},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -1239,6 +1239,29 @@ class DraftDomainAdmin(ListHeaderAdmin):
|
||||||
search_help_text = "Search by draft domain name."
|
search_help_text = "Search by draft domain name."
|
||||||
|
|
||||||
|
|
||||||
|
class VeryImportantPersonAdmin(ListHeaderAdmin):
|
||||||
|
list_display = ("email", "requestor", "truncated_notes", "created_at")
|
||||||
|
search_fields = ["email"]
|
||||||
|
search_help_text = "Search by email."
|
||||||
|
list_filter = [
|
||||||
|
"requestor",
|
||||||
|
]
|
||||||
|
readonly_fields = [
|
||||||
|
"requestor",
|
||||||
|
]
|
||||||
|
|
||||||
|
def truncated_notes(self, obj):
|
||||||
|
# Truncate the 'notes' field to 50 characters
|
||||||
|
return str(obj.notes)[:50]
|
||||||
|
|
||||||
|
truncated_notes.short_description = "Notes (Truncated)" # type: ignore
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
# Set the user field to the current admin user
|
||||||
|
obj.requestor = request.user if request.user.is_authenticated else None
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
|
||||||
admin.site.unregister(LogEntry) # Unregister the default registration
|
admin.site.unregister(LogEntry) # Unregister the default registration
|
||||||
admin.site.register(LogEntry, CustomLogEntryAdmin)
|
admin.site.register(LogEntry, CustomLogEntryAdmin)
|
||||||
admin.site.register(models.User, MyUserAdmin)
|
admin.site.register(models.User, MyUserAdmin)
|
||||||
|
@ -1259,3 +1282,4 @@ admin.site.register(models.Website, WebsiteAdmin)
|
||||||
admin.site.register(models.PublicContact, AuditedAdmin)
|
admin.site.register(models.PublicContact, AuditedAdmin)
|
||||||
admin.site.register(models.DomainApplication, DomainApplicationAdmin)
|
admin.site.register(models.DomainApplication, DomainApplicationAdmin)
|
||||||
admin.site.register(models.TransitionDomain, TransitionDomainAdmin)
|
admin.site.register(models.TransitionDomain, TransitionDomainAdmin)
|
||||||
|
admin.site.register(models.VeryImportantPerson, VeryImportantPersonAdmin)
|
||||||
|
|
|
@ -660,7 +660,6 @@ ALLOWED_HOSTS = [
|
||||||
"getgov-rb.app.cloud.gov",
|
"getgov-rb.app.cloud.gov",
|
||||||
"getgov-ko.app.cloud.gov",
|
"getgov-ko.app.cloud.gov",
|
||||||
"getgov-ab.app.cloud.gov",
|
"getgov-ab.app.cloud.gov",
|
||||||
"getgov-bl.app.cloud.gov",
|
|
||||||
"getgov-rjm.app.cloud.gov",
|
"getgov-rjm.app.cloud.gov",
|
||||||
"getgov-dk.app.cloud.gov",
|
"getgov-dk.app.cloud.gov",
|
||||||
"manage.get.gov",
|
"manage.get.gov",
|
||||||
|
|
|
@ -838,8 +838,8 @@ class AnythingElseForm(RegistrarForm):
|
||||||
|
|
||||||
class RequirementsForm(RegistrarForm):
|
class RequirementsForm(RegistrarForm):
|
||||||
is_policy_acknowledged = forms.BooleanField(
|
is_policy_acknowledged = forms.BooleanField(
|
||||||
label="I read and agree to the requirements for operating .gov domains.",
|
label="I read and agree to the requirements for operating a .gov domain.",
|
||||||
error_messages={
|
error_messages={
|
||||||
"required": ("Check the box if you read and agree to the requirements for operating .gov domains.")
|
"required": ("Check the box if you read and agree to the requirements for operating a .gov domain.")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
37
src/registrar/migrations/0063_veryimportantperson.py
Normal file
37
src/registrar/migrations/0063_veryimportantperson.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# Generated by Django 4.2.7 on 2024-01-23 23:51
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0062_alter_host_name"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="VeryImportantPerson",
|
||||||
|
fields=[
|
||||||
|
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
("email", models.EmailField(db_index=True, help_text="Email", max_length=254)),
|
||||||
|
("notes", models.TextField(help_text="Notes")),
|
||||||
|
(
|
||||||
|
"requestor",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="verifiedby_user",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 4.2.7 on 2024-01-23 22:01
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0063_veryimportantperson"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainapplication",
|
||||||
|
name="address_line1",
|
||||||
|
field=models.TextField(blank=True, help_text="Street address", null=True, verbose_name="Address line 1"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainapplication",
|
||||||
|
name="address_line2",
|
||||||
|
field=models.TextField(
|
||||||
|
blank=True, help_text="Street address line 2 (optional)", null=True, verbose_name="Address line 2"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -13,6 +13,7 @@ from .user import User
|
||||||
from .user_group import UserGroup
|
from .user_group import UserGroup
|
||||||
from .website import Website
|
from .website import Website
|
||||||
from .transition_domain import TransitionDomain
|
from .transition_domain import TransitionDomain
|
||||||
|
from .very_important_person import VeryImportantPerson
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Contact",
|
"Contact",
|
||||||
|
@ -29,6 +30,7 @@ __all__ = [
|
||||||
"UserGroup",
|
"UserGroup",
|
||||||
"Website",
|
"Website",
|
||||||
"TransitionDomain",
|
"TransitionDomain",
|
||||||
|
"VeryImportantPerson",
|
||||||
]
|
]
|
||||||
|
|
||||||
auditlog.register(Contact)
|
auditlog.register(Contact)
|
||||||
|
@ -45,3 +47,4 @@ auditlog.register(User, m2m_fields=["user_permissions", "groups"])
|
||||||
auditlog.register(UserGroup, m2m_fields=["permissions"])
|
auditlog.register(UserGroup, m2m_fields=["permissions"])
|
||||||
auditlog.register(Website)
|
auditlog.register(Website)
|
||||||
auditlog.register(TransitionDomain)
|
auditlog.register(TransitionDomain)
|
||||||
|
auditlog.register(VeryImportantPerson)
|
||||||
|
|
|
@ -431,11 +431,13 @@ class DomainApplication(TimeStampedModel):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Street address",
|
help_text="Street address",
|
||||||
|
verbose_name="Address line 1",
|
||||||
)
|
)
|
||||||
address_line2 = models.TextField(
|
address_line2 = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Street address line 2 (optional)",
|
help_text="Street address line 2 (optional)",
|
||||||
|
verbose_name="Address line 2",
|
||||||
)
|
)
|
||||||
city = models.TextField(
|
city = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
|
|
|
@ -7,6 +7,7 @@ from registrar.models.user_domain_role import UserDomainRole
|
||||||
|
|
||||||
from .domain_invitation import DomainInvitation
|
from .domain_invitation import DomainInvitation
|
||||||
from .transition_domain import TransitionDomain
|
from .transition_domain import TransitionDomain
|
||||||
|
from .very_important_person import VeryImportantPerson
|
||||||
from .domain import Domain
|
from .domain import Domain
|
||||||
|
|
||||||
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
||||||
|
@ -89,6 +90,10 @@ class User(AbstractUser):
|
||||||
if TransitionDomain.objects.filter(username=email).exists():
|
if TransitionDomain.objects.filter(username=email).exists():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# New users flagged by Staff to bypass ial2
|
||||||
|
if VeryImportantPerson.objects.filter(email=email).exists():
|
||||||
|
return False
|
||||||
|
|
||||||
# A new incoming user who is being invited to be a domain manager (that is,
|
# A new incoming user who is being invited to be a domain manager (that is,
|
||||||
# their email address is in DomainInvitation for an invitation that is not yet "retrieved").
|
# their email address is in DomainInvitation for an invitation that is not yet "retrieved").
|
||||||
invited = DomainInvitation.DomainInvitationStatus.INVITED
|
invited = DomainInvitation.DomainInvitationStatus.INVITED
|
||||||
|
|
32
src/registrar/models/very_important_person.py
Normal file
32
src/registrar/models/very_important_person.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
|
||||||
|
|
||||||
|
class VeryImportantPerson(TimeStampedModel):
|
||||||
|
|
||||||
|
"""emails that get added to this table will bypass ial2 on login."""
|
||||||
|
|
||||||
|
email = models.EmailField(
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
help_text="Email",
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
requestor = models.ForeignKey(
|
||||||
|
"registrar.User",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="verifiedby_user",
|
||||||
|
)
|
||||||
|
|
||||||
|
notes = models.TextField(
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
help_text="Notes",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.email
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load static form_helpers url_helpers %}
|
{% load static form_helpers url_helpers %}
|
||||||
|
|
||||||
{% block title %}Apply for a .gov domain | {{form_titles|get_item:steps.current}} | {% endblock %}
|
{% block title %}Request a .gov domain | {{form_titles|get_item:steps.current}} | {% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="grid-container">
|
<div class="grid-container">
|
||||||
<div class="grid-row grid-gap">
|
<div class="grid-row grid-gap">
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
{% load field_helpers %}
|
{% load field_helpers %}
|
||||||
|
|
||||||
{% block form_instructions %}
|
{% block form_instructions %}
|
||||||
<p>Please read this page. Check the box at the bottom to show that you agree to the requirements for operating .gov domains.</p>
|
<p>Please read this page. Check the box at the bottom to show that you agree to the requirements for operating a .gov domain.</p>
|
||||||
<p>The .gov domain space exists to support a broad diversity of government missions. Generally, we don’t review or audit how government organizations use their registered domains. However, misuse of a .gov domain can reflect upon the integrity of the entire .gov space. There are categories of misuse that are statutorily prohibited or abusive in nature.</p>
|
<p>The .gov domain space exists to support a broad diversity of government missions. Generally, we don’t review or audit how government organizations use their registered domains. However, misuse of a .gov domain can reflect upon the integrity of the entire .gov space. There are categories of misuse that are statutorily prohibited or abusive in nature.</p>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
{% if widget.attrs.required %}
|
{% if widget.attrs.required %}
|
||||||
<!--Don't add asterisk to one-field forms -->
|
<!--Don't add asterisk to one-field forms -->
|
||||||
{% if field.label == "Is your organization an election office?" or field.label == "What .gov domain do you want?" or field.label == "I read and agree to the requirements for operating .gov domains." or field.label == "Please explain why there are no other employees from your organization we can contact to help us assess your eligibility for a .gov domain." %}
|
{% if field.label == "Is your organization an election office?" or field.label == "What .gov domain do you want?" or field.label == "I read and agree to the requirements for operating a .gov domain." or field.label == "Please explain why there are no other employees from your organization we can contact to help us assess your eligibility for a .gov domain." %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
<abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||||
Hi.
|
Hi.
|
||||||
|
|
||||||
{{ requester_email }} has added you as a manager on {{ domain.name }}.
|
{{ requestor_email }} has added you as a manager on {{ domain.name }}.
|
||||||
|
|
||||||
You can manage this domain on the .gov registrar <https://manage.get.gov>.
|
You can manage this domain on the .gov registrar <https://manage.get.gov>.
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,11 @@ from registrar.admin import (
|
||||||
ContactAdmin,
|
ContactAdmin,
|
||||||
DomainInformationAdmin,
|
DomainInformationAdmin,
|
||||||
UserDomainRoleAdmin,
|
UserDomainRoleAdmin,
|
||||||
|
VeryImportantPersonAdmin,
|
||||||
)
|
)
|
||||||
from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website
|
from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
|
from registrar.models.very_important_person import VeryImportantPerson
|
||||||
from .common import (
|
from .common import (
|
||||||
MockSESClient,
|
MockSESClient,
|
||||||
AuditedAdminMockData,
|
AuditedAdminMockData,
|
||||||
|
@ -1737,3 +1739,28 @@ class ContactAdminTest(TestCase):
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
|
class VeryImportantPersonAdminTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.superuser = create_superuser()
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
def test_save_model_sets_user_field(self):
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
|
||||||
|
# Create an instance of the admin class
|
||||||
|
admin_instance = VeryImportantPersonAdmin(model=VeryImportantPerson, admin_site=None)
|
||||||
|
|
||||||
|
# Create a VeryImportantPerson instance
|
||||||
|
vip_instance = VeryImportantPerson(email="test@example.com", notes="Test Notes")
|
||||||
|
|
||||||
|
# Create a request object
|
||||||
|
request = self.factory.post("/admin/yourapp/veryimportantperson/add/")
|
||||||
|
request.user = self.superuser
|
||||||
|
|
||||||
|
# Call the save_model method
|
||||||
|
admin_instance.save_model(request, vip_instance, None, None)
|
||||||
|
|
||||||
|
# Check that the user field is set to the request.user
|
||||||
|
self.assertEqual(vip_instance.requestor, self.superuser)
|
||||||
|
|
|
@ -338,7 +338,7 @@ class TestFormValidation(MockEppLib):
|
||||||
form = RequirementsForm(data={})
|
form = RequirementsForm(data={})
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
form.errors["is_policy_acknowledged"],
|
form.errors["is_policy_acknowledged"],
|
||||||
["Check the box if you read and agree to the requirements for operating .gov domains."],
|
["Check the box if you read and agree to the requirements for operating a .gov domain."],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_requirements_form_unchecked(self):
|
def test_requirements_form_unchecked(self):
|
||||||
|
@ -346,7 +346,7 @@ class TestFormValidation(MockEppLib):
|
||||||
form = RequirementsForm(data={"is_policy_acknowledged": False})
|
form = RequirementsForm(data={"is_policy_acknowledged": False})
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
form.errors["is_policy_acknowledged"],
|
form.errors["is_policy_acknowledged"],
|
||||||
["Check the box if you read and agree to the requirements for operating .gov domains."],
|
["Check the box if you read and agree to the requirements for operating a .gov domain."],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_tribal_government_unrecognized(self):
|
def test_tribal_government_unrecognized(self):
|
||||||
|
|
|
@ -15,7 +15,8 @@ from registrar.models import (
|
||||||
)
|
)
|
||||||
|
|
||||||
import boto3_mocking
|
import boto3_mocking
|
||||||
from registrar.models.transition_domain import TransitionDomain # type: ignore
|
from registrar.models.transition_domain import TransitionDomain
|
||||||
|
from registrar.models.very_important_person import VeryImportantPerson # type: ignore
|
||||||
from .common import MockSESClient, less_console_noise, completed_application
|
from .common import MockSESClient, less_console_noise, completed_application
|
||||||
from django_fsm import TransitionNotAllowed
|
from django_fsm import TransitionNotAllowed
|
||||||
|
|
||||||
|
@ -652,6 +653,12 @@ class TestUser(TestCase):
|
||||||
TransitionDomain.objects.get_or_create(username=self.user.email, domain_name=self.domain_name)
|
TransitionDomain.objects.get_or_create(username=self.user.email, domain_name=self.domain_name)
|
||||||
self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username))
|
self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username))
|
||||||
|
|
||||||
|
def test_identity_verification_with_very_important_person(self):
|
||||||
|
"""A Very Important Person should return False
|
||||||
|
when tested with class method needs_identity_verification"""
|
||||||
|
VeryImportantPerson.objects.get_or_create(email=self.user.email)
|
||||||
|
self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username))
|
||||||
|
|
||||||
def test_identity_verification_with_invited_user(self):
|
def test_identity_verification_with_invited_user(self):
|
||||||
"""An invited user should return False when tested with class
|
"""An invited user should return False when tested with class
|
||||||
method needs_identity_verification"""
|
method needs_identity_verification"""
|
||||||
|
|
|
@ -2908,7 +2908,7 @@ class TestDomainManagers(TestDomainOverview):
|
||||||
)
|
)
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_domain_invitation_email_has_email_as_requester_non_existent(self):
|
def test_domain_invitation_email_has_email_as_requestor_non_existent(self):
|
||||||
"""Inviting a non existent user sends them an email, with email as the name."""
|
"""Inviting a non existent user sends them an email, with email as the name."""
|
||||||
# make sure there is no user with this email
|
# make sure there is no user with this email
|
||||||
email_address = "mayor@igorville.gov"
|
email_address = "mayor@igorville.gov"
|
||||||
|
@ -2941,13 +2941,13 @@ class TestDomainManagers(TestDomainOverview):
|
||||||
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||||
self.assertIn("info@example.com", email_content)
|
self.assertIn("info@example.com", email_content)
|
||||||
|
|
||||||
# Check that the requesters first/last name do not exist
|
# Check that the requestors first/last name do not exist
|
||||||
self.assertNotIn("First", email_content)
|
self.assertNotIn("First", email_content)
|
||||||
self.assertNotIn("Last", email_content)
|
self.assertNotIn("Last", email_content)
|
||||||
self.assertNotIn("First Last", email_content)
|
self.assertNotIn("First Last", email_content)
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_domain_invitation_email_has_email_as_requester(self):
|
def test_domain_invitation_email_has_email_as_requestor(self):
|
||||||
"""Inviting a user sends them an email, with email as the name."""
|
"""Inviting a user sends them an email, with email as the name."""
|
||||||
# Create a fake user object
|
# Create a fake user object
|
||||||
email_address = "mayor@igorville.gov"
|
email_address = "mayor@igorville.gov"
|
||||||
|
@ -2980,13 +2980,13 @@ class TestDomainManagers(TestDomainOverview):
|
||||||
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||||
self.assertIn("info@example.com", email_content)
|
self.assertIn("info@example.com", email_content)
|
||||||
|
|
||||||
# Check that the requesters first/last name do not exist
|
# Check that the requestors first/last name do not exist
|
||||||
self.assertNotIn("First", email_content)
|
self.assertNotIn("First", email_content)
|
||||||
self.assertNotIn("Last", email_content)
|
self.assertNotIn("Last", email_content)
|
||||||
self.assertNotIn("First Last", email_content)
|
self.assertNotIn("First Last", email_content)
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_domain_invitation_email_has_email_as_requester_staff(self):
|
def test_domain_invitation_email_has_email_as_requestor_staff(self):
|
||||||
"""Inviting a user sends them an email, with email as the name."""
|
"""Inviting a user sends them an email, with email as the name."""
|
||||||
# Create a fake user object
|
# Create a fake user object
|
||||||
email_address = "mayor@igorville.gov"
|
email_address = "mayor@igorville.gov"
|
||||||
|
@ -3023,7 +3023,7 @@ class TestDomainManagers(TestDomainOverview):
|
||||||
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||||
self.assertIn("help@get.gov", email_content)
|
self.assertIn("help@get.gov", email_content)
|
||||||
|
|
||||||
# Check that the requesters first/last name do not exist
|
# Check that the requestors first/last name do not exist
|
||||||
self.assertNotIn("First", email_content)
|
self.assertNotIn("First", email_content)
|
||||||
self.assertNotIn("Last", email_content)
|
self.assertNotIn("Last", email_content)
|
||||||
self.assertNotIn("First Last", email_content)
|
self.assertNotIn("First Last", email_content)
|
||||||
|
|
|
@ -92,7 +92,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
||||||
Step.YOUR_CONTACT: _("Your contact information"),
|
Step.YOUR_CONTACT: _("Your contact information"),
|
||||||
Step.OTHER_CONTACTS: _("Other employees from your organization"),
|
Step.OTHER_CONTACTS: _("Other employees from your organization"),
|
||||||
Step.ANYTHING_ELSE: _("Anything else?"),
|
Step.ANYTHING_ELSE: _("Anything else?"),
|
||||||
Step.REQUIREMENTS: _("Requirements for operating .gov domains"),
|
Step.REQUIREMENTS: _("Requirements for operating a .gov domain"),
|
||||||
Step.REVIEW: _("Review and submit your domain request"),
|
Step.REVIEW: _("Review and submit your domain request"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -711,7 +711,7 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
"""Get an absolute URL for this domain."""
|
"""Get an absolute URL for this domain."""
|
||||||
return self.request.build_absolute_uri(reverse("domain", kwargs={"pk": self.object.id}))
|
return self.request.build_absolute_uri(reverse("domain", kwargs={"pk": self.object.id}))
|
||||||
|
|
||||||
def _send_domain_invitation_email(self, email: str, requester: User, add_success=True):
|
def _send_domain_invitation_email(self, email: str, requestor: User, add_success=True):
|
||||||
"""Performs the sending of the domain invitation email,
|
"""Performs the sending of the domain invitation email,
|
||||||
does not make a domain information object
|
does not make a domain information object
|
||||||
email: string- email to send to
|
email: string- email to send to
|
||||||
|
@ -719,16 +719,16 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
adding a success message to the view if the email sending succeeds"""
|
adding a success message to the view if the email sending succeeds"""
|
||||||
|
|
||||||
# Set a default email address to send to for staff
|
# Set a default email address to send to for staff
|
||||||
requester_email = "help@get.gov"
|
requestor_email = "help@get.gov"
|
||||||
|
|
||||||
# Check if the email requester has a valid email address
|
# Check if the email requestor has a valid email address
|
||||||
if not requester.is_staff and requester.email is not None and requester.email.strip() != "":
|
if not requestor.is_staff and requestor.email is not None and requestor.email.strip() != "":
|
||||||
requester_email = requester.email
|
requestor_email = requestor.email
|
||||||
elif not requester.is_staff:
|
elif not requestor.is_staff:
|
||||||
messages.error(self.request, "Can't send invitation email. No email is associated with your account.")
|
messages.error(self.request, "Can't send invitation email. No email is associated with your account.")
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Can't send email to '{email}' on domain '{self.object}'."
|
f"Can't send email to '{email}' on domain '{self.object}'."
|
||||||
f"No email exists for the requester '{requester.username}'.",
|
f"No email exists for the requestor '{requestor.username}'.",
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
@ -741,7 +741,7 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
context={
|
context={
|
||||||
"domain_url": self._domain_abs_url(),
|
"domain_url": self._domain_abs_url(),
|
||||||
"domain": self.object,
|
"domain": self.object,
|
||||||
"requester_email": requester_email,
|
"requestor_email": requestor_email,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except EmailSendingError:
|
except EmailSendingError:
|
||||||
|
@ -756,7 +756,7 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
if add_success:
|
if add_success:
|
||||||
messages.success(self.request, f"{email} has been invited to this domain.")
|
messages.success(self.request, f"{email} has been invited to this domain.")
|
||||||
|
|
||||||
def _make_invitation(self, email_address: str, requester: User):
|
def _make_invitation(self, email_address: str, requestor: User):
|
||||||
"""Make a Domain invitation for this email and redirect with a message."""
|
"""Make a Domain invitation for this email and redirect with a message."""
|
||||||
invitation, created = DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
|
invitation, created = DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
|
||||||
if not created:
|
if not created:
|
||||||
|
@ -766,22 +766,22 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
f"{email_address} has already been invited to this domain.",
|
f"{email_address} has already been invited to this domain.",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self._send_domain_invitation_email(email=email_address, requester=requester)
|
self._send_domain_invitation_email(email=email_address, requestor=requestor)
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
"""Add the specified user on this domain."""
|
"""Add the specified user on this domain."""
|
||||||
requested_email = form.cleaned_data["email"]
|
requested_email = form.cleaned_data["email"]
|
||||||
requester = self.request.user
|
requestor = self.request.user
|
||||||
# look up a user with that email
|
# look up a user with that email
|
||||||
try:
|
try:
|
||||||
requested_user = User.objects.get(email=requested_email)
|
requested_user = User.objects.get(email=requested_email)
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
# no matching user, go make an invitation
|
# no matching user, go make an invitation
|
||||||
return self._make_invitation(requested_email, requester)
|
return self._make_invitation(requested_email, requestor)
|
||||||
else:
|
else:
|
||||||
# if user already exists then just send an email
|
# if user already exists then just send an email
|
||||||
self._send_domain_invitation_email(requested_email, requester, add_success=False)
|
self._send_domain_invitation_email(requested_email, requestor, add_success=False)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
UserDomainRole.objects.create(
|
UserDomainRole.objects.create(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue