mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-18 18:39:21 +02:00
merge main
This commit is contained in:
commit
5e84432fd7
59 changed files with 2516 additions and 2599 deletions
12
.github/ISSUE_TEMPLATE/developer-onboarding.md
vendored
12
.github/ISSUE_TEMPLATE/developer-onboarding.md
vendored
|
@ -16,6 +16,8 @@ assignees: abroddrick
|
||||||
|
|
||||||
There are several tools we use locally that you will need to have.
|
There are several tools we use locally that you will need to have.
|
||||||
- [ ] [Install the cf CLI v7](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html#pkg-mac) for the ability to deploy
|
- [ ] [Install the cf CLI v7](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html#pkg-mac) for the ability to deploy
|
||||||
|
- If you are using Windows, installation information can be found [here](https://github.com/cloudfoundry/cli/wiki/V8-CLI-Installation-Guide#installers-and-compressed-binaries)
|
||||||
|
- Alternatively, for Windows, [consider using chocolately](https://community.chocolatey.org/packages/cloudfoundry-cli/7.2.0)
|
||||||
- [ ] Make sure you have `gpg` >2.1.7. Run `gpg --version` to check. If not, [install gnupg](https://formulae.brew.sh/formula/gnupg)
|
- [ ] Make sure you have `gpg` >2.1.7. Run `gpg --version` to check. If not, [install gnupg](https://formulae.brew.sh/formula/gnupg)
|
||||||
- [ ] Install the [Github CLI](https://cli.github.com/)
|
- [ ] Install the [Github CLI](https://cli.github.com/)
|
||||||
|
|
||||||
|
@ -70,6 +72,7 @@ when setting up your key in Github.
|
||||||
|
|
||||||
Now test commit signing is working by checking out a branch (`yourname/test-commit-signing`) and making some small change to a file. Commit the change (it should prompt you for your GPG credential) and push it to Github. Look on Github at your branch and ensure the commit is `verified`.
|
Now test commit signing is working by checking out a branch (`yourname/test-commit-signing`) and making some small change to a file. Commit the change (it should prompt you for your GPG credential) and push it to Github. Look on Github at your branch and ensure the commit is `verified`.
|
||||||
|
|
||||||
|
### MacOS
|
||||||
**Note:** if you are on a mac and not able to successfully create a signed commit, getting the following error:
|
**Note:** if you are on a mac and not able to successfully create a signed commit, getting the following error:
|
||||||
```zsh
|
```zsh
|
||||||
error: gpg failed to sign the data
|
error: gpg failed to sign the data
|
||||||
|
@ -90,6 +93,15 @@ or
|
||||||
source ~/.zshrc
|
source ~/.zshrc
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
If GPG doesn't work out of the box with git for you:
|
||||||
|
- You can [download the GPG binary directly](https://gnupg.org/download/).
|
||||||
|
- It may be helpful to use [gpg4win](https://www.gpg4win.org/get-gpg4win.html).
|
||||||
|
|
||||||
|
From there, you should be able to access gpg through the terminal.
|
||||||
|
|
||||||
|
Additionally, consider a gpg key manager like Kleopatra if you run into issues with environment variables or with the gpg service not running on startup.
|
||||||
|
|
||||||
## Setting up developer sandbox
|
## Setting up developer sandbox
|
||||||
|
|
||||||
We have three types of environments: stable, staging, and sandbox. Stable (production)and staging (pre-prod) get deployed via tagged release, and developer sandboxes are given to get.gov developers to mess around in a production-like environment without disrupting stable or staging. Each sandbox is namespaced and will automatically be deployed too when the appropriate branch syntax is used for that space in an open pull request. There are several things you need to setup to make the sandbox work for a developer.
|
We have three types of environments: stable, staging, and sandbox. Stable (production)and staging (pre-prod) get deployed via tagged release, and developer sandboxes are given to get.gov developers to mess around in a production-like environment without disrupting stable or staging. Each sandbox is namespaced and will automatically be deployed too when the appropriate branch syntax is used for that space in an open pull request. There are several things you need to setup to make the sandbox work for a developer.
|
||||||
|
|
4
.github/ISSUE_TEMPLATE/issue-default.yml
vendored
4
.github/ISSUE_TEMPLATE/issue-default.yml
vendored
|
@ -31,8 +31,8 @@ body:
|
||||||
attributes:
|
attributes:
|
||||||
label: Links to other issues
|
label: Links to other issues
|
||||||
description: |
|
description: |
|
||||||
"Add issue #numbers this relates to and how (e.g., 🚧 [construction] Blocks, ⛔️ [no_entry] Is blocked by, 🔄 [arrows_counterclockwise] Relates to)."
|
"With a `-` to start the line, add issue #numbers this relates to and how (e.g., 🚧 [construction] Blocks, ⛔️ [no_entry] Is blocked by, 🔄 [arrows_counterclockwise] Relates to)."
|
||||||
placeholder: 🔄 Relates to...
|
placeholder: "- 🔄 Relates to..."
|
||||||
- type: markdown
|
- type: markdown
|
||||||
id: note
|
id: note
|
||||||
attributes:
|
attributes:
|
||||||
|
|
1
.github/workflows/deploy-sandbox.yaml
vendored
1
.github/workflows/deploy-sandbox.yaml
vendored
|
@ -28,6 +28,7 @@ jobs:
|
||||||
|| startsWith(github.head_ref, 'hotgov/')
|
|| startsWith(github.head_ref, 'hotgov/')
|
||||||
|| startsWith(github.head_ref, 'litterbox/')
|
|| startsWith(github.head_ref, 'litterbox/')
|
||||||
|| startsWith(github.head_ref, 'ag/')
|
|| startsWith(github.head_ref, 'ag/')
|
||||||
|
|| startsWith(github.head_ref, 'ms/')
|
||||||
outputs:
|
outputs:
|
||||||
environment: ${{ steps.var.outputs.environment}}
|
environment: ${{ steps.var.outputs.environment}}
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
|
|
1
.github/workflows/migrate.yaml
vendored
1
.github/workflows/migrate.yaml
vendored
|
@ -16,6 +16,7 @@ on:
|
||||||
- stable
|
- stable
|
||||||
- staging
|
- staging
|
||||||
- development
|
- development
|
||||||
|
- ms
|
||||||
- ag
|
- ag
|
||||||
- litterbox
|
- litterbox
|
||||||
- hotgov
|
- hotgov
|
||||||
|
|
1
.github/workflows/reset-db.yaml
vendored
1
.github/workflows/reset-db.yaml
vendored
|
@ -16,6 +16,7 @@ on:
|
||||||
options:
|
options:
|
||||||
- staging
|
- staging
|
||||||
- development
|
- development
|
||||||
|
- ms
|
||||||
- ag
|
- ag
|
||||||
- litterbox
|
- litterbox
|
||||||
- hotgov
|
- hotgov
|
||||||
|
|
|
@ -353,49 +353,6 @@ cf env getgov-{app name}
|
||||||
|
|
||||||
Then, copy the variables under the section labled `s3`.
|
Then, copy the variables under the section labled `s3`.
|
||||||
|
|
||||||
## Signals
|
|
||||||
The application uses [Django signals](https://docs.djangoproject.com/en/5.0/topics/signals/). In particular, it uses a subset of prebuilt signals called [model signals](https://docs.djangoproject.com/en/5.0/ref/signals/#module-django.db.models.signals).
|
|
||||||
|
|
||||||
Per Django, signals "[...allow certain senders to notify a set of receivers that some action has taken place.](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch)"
|
|
||||||
|
|
||||||
In other words, signals are a mechanism that allows different parts of an application to communicate with each other by sending and receiving notifications when events occur. When an event occurs (such as creating, updating, or deleting a record), signals can automatically trigger specific actions in response. This allows different parts of an application to stay synchronized without tightly coupling the component.
|
|
||||||
|
|
||||||
### Rules of use
|
|
||||||
When using signals, try to adhere to these guidelines:
|
|
||||||
1. Don't use signals when you can use another method, such as an override of `save()` or `__init__`.
|
|
||||||
2. Document its usage in this readme (or another centralized location), as well as briefly on the underlying class it is associated with. For instance, since the `handle_profile` directly affects the class `Contact`, the class description notes this and links to [signals.py](../../src/registrar/signals.py).
|
|
||||||
3. Where possible, avoid chaining signals together (i.e. a signal that calls a signal). If this has to be done, clearly document the flow.
|
|
||||||
4. Minimize logic complexity within the signal as much as possible.
|
|
||||||
|
|
||||||
### When should you use signals?
|
|
||||||
Generally, you would use signals when you want an event to be synchronized across multiple areas of code at once (such as with two models or more models at once) in a way that would otherwise be difficult to achieve by overriding functions.
|
|
||||||
|
|
||||||
However, in most scenarios, if you can get away with avoiding signals - you should. The reasoning for this is that [signals give the appearance of loose coupling, but they can quickly lead to code that is hard to understand, adjust and debug](https://docs.djangoproject.com/en/5.0/topics/signals/#module-django.dispatch).
|
|
||||||
|
|
||||||
Consider using signals when:
|
|
||||||
1. Synchronizing events across multiple models or areas of code.
|
|
||||||
2. Performing logic before or after saving a model to the database (when otherwise difficult through `save()`).
|
|
||||||
3. Encountering an import loop when overriding functions such as `save()`.
|
|
||||||
4. You are otherwise unable to achieve the intended behavior by overrides or other means.
|
|
||||||
5. (Rare) Offloading tasks when multi-threading.
|
|
||||||
|
|
||||||
For the vast majority of use cases, the [pre_save](https://docs.djangoproject.com/en/5.0/ref/signals/#pre-save) and [post_save](https://docs.djangoproject.com/en/5.0/ref/signals/#post-save) signals are sufficient in terms of model-to-model management.
|
|
||||||
|
|
||||||
### Where should you use them?
|
|
||||||
This project compiles signals in a unified location to maintain readability. If you are adding a signal or otherwise utilizing one, you should always define them in [signals.py](../../src/registrar/signals.py). Except under rare circumstances, this should be adhered to for the reasons mentioned above.
|
|
||||||
|
|
||||||
### How are we currently using signals?
|
|
||||||
At the time of writing, we currently only use signals for the Contact and User objects when synchronizing data returned from Login.gov. This is because the `Contact` object holds information that the user specified in our system, whereas the `User` object holds information that was specified in Login.gov.
|
|
||||||
|
|
||||||
To keep our signal usage coherent and well-documented, add to this document when a new function is added for ease of reference and use.
|
|
||||||
|
|
||||||
#### handle_profile
|
|
||||||
This function is triggered by the post_save event on the User model, designed to manage the synchronization between User and Contact entities. It operates under the following conditions:
|
|
||||||
|
|
||||||
1. For New Users: Upon the creation of a new user, it checks for an existing `Contact` by email. If no matching contact is found, it creates a new Contact using the user's details from Login.gov. If a matching contact is found, it associates this contact with the user. In cases where multiple contacts with the same email exist, it logs a warning and associates the first contact found.
|
|
||||||
|
|
||||||
2. For Existing Users: For users logging in subsequent times, the function ensures that any updates from Login.gov are applied to the associated User record. However, it does not alter any existing Contact records.
|
|
||||||
|
|
||||||
## Disable email sending (toggling the disable_email_sending flag)
|
## Disable email sending (toggling the disable_email_sending flag)
|
||||||
1. On the app, navigate to `\admin`.
|
1. On the app, navigate to `\admin`.
|
||||||
2. Under models, click `Waffle flags`.
|
2. Under models, click `Waffle flags`.
|
||||||
|
|
32
ops/manifests/manifest-ms.yaml
Normal file
32
ops/manifests/manifest-ms.yaml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
applications:
|
||||||
|
- name: getgov-ms
|
||||||
|
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-ms.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-ms.app.cloud.gov
|
||||||
|
services:
|
||||||
|
- getgov-credentials
|
||||||
|
- getgov-ms-database
|
|
@ -9,6 +9,7 @@ from django.db.models.functions import Concat, Coalesce
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django_fsm import get_available_FIELD_transitions, FSMField
|
from django_fsm import get_available_FIELD_transitions, FSMField
|
||||||
|
from waffle.decorators import flag_is_active
|
||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
|
@ -18,7 +19,7 @@ from epplibwrapper.errors import ErrorCode, RegistryError
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
from waffle.admin import FlagAdmin
|
from waffle.admin import FlagAdmin
|
||||||
from waffle.models import Sample, Switch
|
from waffle.models import Sample, Switch
|
||||||
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website
|
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial
|
||||||
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
||||||
from registrar.views.utility.mixins import OrderableFieldsMixin
|
from registrar.views.utility.mixins import OrderableFieldsMixin
|
||||||
from django.contrib.admin.views.main import ORDER_VAR
|
from django.contrib.admin.views.main import ORDER_VAR
|
||||||
|
@ -166,6 +167,9 @@ class DomainRequestAdminForm(forms.ModelForm):
|
||||||
"alternative_domains": NoAutocompleteFilteredSelectMultiple("alternative_domains", False),
|
"alternative_domains": NoAutocompleteFilteredSelectMultiple("alternative_domains", False),
|
||||||
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
|
"other_contacts": NoAutocompleteFilteredSelectMultiple("other_contacts", False),
|
||||||
}
|
}
|
||||||
|
labels = {
|
||||||
|
"action_needed_reason_email": "Auto-generated email",
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -444,8 +448,9 @@ class AdminSortFields:
|
||||||
sort_mapping = {
|
sort_mapping = {
|
||||||
# == Contact == #
|
# == Contact == #
|
||||||
"other_contacts": (Contact, _name_sort),
|
"other_contacts": (Contact, _name_sort),
|
||||||
"senior_official": (Contact, _name_sort),
|
|
||||||
"submitter": (Contact, _name_sort),
|
"submitter": (Contact, _name_sort),
|
||||||
|
# == Senior Official == #
|
||||||
|
"senior_official": (SeniorOfficial, _name_sort),
|
||||||
# == User == #
|
# == User == #
|
||||||
"creator": (User, _name_sort),
|
"creator": (User, _name_sort),
|
||||||
"user": (User, _name_sort),
|
"user": (User, _name_sort),
|
||||||
|
@ -593,33 +598,6 @@ class ListHeaderAdmin(AuditedAdmin, OrderableFieldsMixin):
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
|
|
||||||
class UserContactInline(admin.StackedInline):
|
|
||||||
"""Edit a user's profile on the user page."""
|
|
||||||
|
|
||||||
model = models.Contact
|
|
||||||
|
|
||||||
# Read only that we'll leverage for CISA Analysts
|
|
||||||
analyst_readonly_fields = [
|
|
||||||
"user",
|
|
||||||
"email",
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
|
||||||
"""Set the read-only state on form elements.
|
|
||||||
We have 1 conditions that determine which fields are read-only:
|
|
||||||
admin user permissions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
readonly_fields = list(self.readonly_fields)
|
|
||||||
|
|
||||||
if request.user.has_perm("registrar.full_access_permission"):
|
|
||||||
return readonly_fields
|
|
||||||
# Return restrictive Read-only fields for analysts and
|
|
||||||
# users who might not belong to groups
|
|
||||||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
|
||||||
return readonly_fields # Read-only fields for analysts
|
|
||||||
|
|
||||||
|
|
||||||
class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||||
"""Custom user admin class to use our inlines."""
|
"""Custom user admin class to use our inlines."""
|
||||||
|
|
||||||
|
@ -636,8 +614,6 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
||||||
|
|
||||||
_meta = Meta()
|
_meta = Meta()
|
||||||
|
|
||||||
inlines = [UserContactInline]
|
|
||||||
|
|
||||||
list_display = (
|
list_display = (
|
||||||
"username",
|
"username",
|
||||||
"overridden_email_field",
|
"overridden_email_field",
|
||||||
|
@ -915,30 +891,20 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
list_display = [
|
list_display = [
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
"user_exists",
|
|
||||||
]
|
]
|
||||||
# this ordering effects the ordering of results
|
# this ordering effects the ordering of results
|
||||||
# in autocomplete_fields for user
|
# in autocomplete_fields
|
||||||
ordering = ["first_name", "last_name", "email"]
|
ordering = ["first_name", "last_name", "email"]
|
||||||
|
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
(
|
(
|
||||||
None,
|
None,
|
||||||
{"fields": ["user", "first_name", "middle_name", "last_name", "title", "email", "phone"]},
|
{"fields": ["first_name", "middle_name", "last_name", "title", "email", "phone"]},
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
autocomplete_fields = ["user"]
|
|
||||||
|
|
||||||
change_form_template = "django/admin/email_clipboard_change_form.html"
|
change_form_template = "django/admin/email_clipboard_change_form.html"
|
||||||
|
|
||||||
def user_exists(self, obj):
|
|
||||||
"""Check if the Contact has a related User"""
|
|
||||||
return "Yes" if obj.user is not None else "No"
|
|
||||||
|
|
||||||
user_exists.short_description = "Is user" # type: ignore
|
|
||||||
user_exists.admin_order_field = "user" # type: ignore
|
|
||||||
|
|
||||||
# We name the custom prop 'contact' because linter
|
# We name the custom prop 'contact' because linter
|
||||||
# is not allowing a short_description attr on it
|
# is not allowing a short_description attr on it
|
||||||
# This gets around the linter limitation, for now.
|
# This gets around the linter limitation, for now.
|
||||||
|
@ -956,10 +922,7 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
name.admin_order_field = "first_name" # type: ignore
|
name.admin_order_field = "first_name" # type: ignore
|
||||||
|
|
||||||
# Read only that we'll leverage for CISA Analysts
|
# Read only that we'll leverage for CISA Analysts
|
||||||
analyst_readonly_fields = [
|
analyst_readonly_fields: list[str] = ["email"]
|
||||||
"user",
|
|
||||||
"email",
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
"""Set the read-only state on form elements.
|
"""Set the read-only state on form elements.
|
||||||
|
@ -1039,6 +1002,19 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
return super().changelist_view(request, extra_context=extra_context)
|
return super().changelist_view(request, extra_context=extra_context)
|
||||||
|
|
||||||
|
|
||||||
|
class SeniorOfficialAdmin(ListHeaderAdmin):
|
||||||
|
"""Custom Senior Official Admin class."""
|
||||||
|
|
||||||
|
# NOTE: these are just placeholders. Not part of ACs (haven't been defined yet). Update in future tickets.
|
||||||
|
search_fields = ["first_name", "last_name", "email"]
|
||||||
|
search_help_text = "Search by first name, last name or email."
|
||||||
|
list_display = ["first_name", "last_name", "email"]
|
||||||
|
|
||||||
|
# this ordering effects the ordering of results
|
||||||
|
# in autocomplete_fields for Senior Official
|
||||||
|
ordering = ["first_name", "last_name"]
|
||||||
|
|
||||||
|
|
||||||
class WebsiteResource(resources.ModelResource):
|
class WebsiteResource(resources.ModelResource):
|
||||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
import/export file"""
|
import/export file"""
|
||||||
|
@ -1509,6 +1485,13 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
custom_election_board.admin_order_field = "is_election_board" # type: ignore
|
custom_election_board.admin_order_field = "is_election_board" # type: ignore
|
||||||
custom_election_board.short_description = "Election office" # type: ignore
|
custom_election_board.short_description = "Election office" # type: ignore
|
||||||
|
|
||||||
|
# This is just a placeholder. This field will be populated in the detail_table_fieldset view.
|
||||||
|
# This is not a field that exists on the model.
|
||||||
|
def status_history(self, obj):
|
||||||
|
return "No changelog to display."
|
||||||
|
|
||||||
|
status_history.short_description = "Status History" # type: ignore
|
||||||
|
|
||||||
# Filters
|
# Filters
|
||||||
list_filter = (
|
list_filter = (
|
||||||
StatusListFilter,
|
StatusListFilter,
|
||||||
|
@ -1535,9 +1518,11 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"fields": [
|
"fields": [
|
||||||
"portfolio",
|
"portfolio",
|
||||||
"sub_organization",
|
"sub_organization",
|
||||||
|
"status_history",
|
||||||
"status",
|
"status",
|
||||||
"rejection_reason",
|
"rejection_reason",
|
||||||
"action_needed_reason",
|
"action_needed_reason",
|
||||||
|
"action_needed_reason_email",
|
||||||
"investigator",
|
"investigator",
|
||||||
"creator",
|
"creator",
|
||||||
"submitter",
|
"submitter",
|
||||||
|
@ -1617,6 +1602,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"alternative_domains",
|
"alternative_domains",
|
||||||
"is_election_board",
|
"is_election_board",
|
||||||
"federal_agency",
|
"federal_agency",
|
||||||
|
"status_history",
|
||||||
|
"action_needed_reason_email",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Read only that we'll leverage for CISA Analysts
|
# Read only that we'll leverage for CISA Analysts
|
||||||
|
@ -1935,6 +1922,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
extra_context = extra_context or {}
|
extra_context = extra_context or {}
|
||||||
extra_context["filtered_audit_log_entries"] = filtered_audit_log_entries
|
extra_context["filtered_audit_log_entries"] = filtered_audit_log_entries
|
||||||
extra_context["action_needed_reason_emails"] = self.get_all_action_needed_reason_emails_as_json(obj)
|
extra_context["action_needed_reason_emails"] = self.get_all_action_needed_reason_emails_as_json(obj)
|
||||||
|
extra_context["has_profile_feature_flag"] = flag_is_active(request, "profile_feature")
|
||||||
|
|
||||||
# Call the superclass method with updated extra_context
|
# Call the superclass method with updated extra_context
|
||||||
return super().change_view(request, object_id, form_url, extra_context)
|
return super().change_view(request, object_id, form_url, extra_context)
|
||||||
|
@ -1965,9 +1953,13 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
template_subject_path = f"emails/action_needed_reasons/{action_needed_reason}_subject.txt"
|
template_subject_path = f"emails/action_needed_reasons/{action_needed_reason}_subject.txt"
|
||||||
subject_template = get_template(template_subject_path)
|
subject_template = get_template(template_subject_path)
|
||||||
|
|
||||||
# Return the content of the rendered views
|
if flag_is_active(None, "profile_feature"): # type: ignore
|
||||||
context = {"domain_request": domain_request}
|
recipient = domain_request.creator
|
||||||
|
else:
|
||||||
|
recipient = domain_request.submitter
|
||||||
|
|
||||||
|
# Return the content of the rendered views
|
||||||
|
context = {"domain_request": domain_request, "recipient": recipient}
|
||||||
return {
|
return {
|
||||||
"subject_text": subject_template.render(context=context),
|
"subject_text": subject_template.render(context=context),
|
||||||
"email_body_text": template.render(context=context),
|
"email_body_text": template.render(context=context),
|
||||||
|
@ -2779,6 +2771,7 @@ admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin)
|
||||||
admin.site.register(models.Portfolio, PortfolioAdmin)
|
admin.site.register(models.Portfolio, PortfolioAdmin)
|
||||||
admin.site.register(models.DomainGroup, DomainGroupAdmin)
|
admin.site.register(models.DomainGroup, DomainGroupAdmin)
|
||||||
admin.site.register(models.Suborganization, SuborganizationAdmin)
|
admin.site.register(models.Suborganization, SuborganizationAdmin)
|
||||||
|
admin.site.register(models.SeniorOfficial, SeniorOfficialAdmin)
|
||||||
|
|
||||||
# Register our custom waffle implementations
|
# Register our custom waffle implementations
|
||||||
admin.site.register(models.WaffleFlag, WaffleFlagAdmin)
|
admin.site.register(models.WaffleFlag, WaffleFlagAdmin)
|
||||||
|
|
|
@ -5,12 +5,3 @@ class RegistrarConfig(AppConfig):
|
||||||
"""Configure signal handling for our registrar Django application."""
|
"""Configure signal handling for our registrar Django application."""
|
||||||
|
|
||||||
name = "registrar"
|
name = "registrar"
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
"""Runs when all Django applications have been loaded.
|
|
||||||
|
|
||||||
We use it here to load signals that connect related models.
|
|
||||||
"""
|
|
||||||
# noqa here because we are importing something to make the signals
|
|
||||||
# get registered, but not using what we import
|
|
||||||
from . import signals # noqa
|
|
||||||
|
|
|
@ -361,9 +361,12 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
*/
|
*/
|
||||||
(function (){
|
(function (){
|
||||||
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
|
let rejectionReasonFormGroup = document.querySelector('.field-rejection_reason')
|
||||||
|
// This is the "action needed reason" field
|
||||||
let actionNeededReasonFormGroup = document.querySelector('.field-action_needed_reason');
|
let actionNeededReasonFormGroup = document.querySelector('.field-action_needed_reason');
|
||||||
|
// This is the "auto-generated email" field
|
||||||
|
let actionNeededReasonEmailFormGroup = document.querySelector('.field-action_needed_reason_email')
|
||||||
|
|
||||||
if (rejectionReasonFormGroup && actionNeededReasonFormGroup) {
|
if (rejectionReasonFormGroup && actionNeededReasonFormGroup && actionNeededReasonEmailFormGroup) {
|
||||||
let statusSelect = document.getElementById('id_status')
|
let statusSelect = document.getElementById('id_status')
|
||||||
let isRejected = statusSelect.value == "rejected"
|
let isRejected = statusSelect.value == "rejected"
|
||||||
let isActionNeeded = statusSelect.value == "action needed"
|
let isActionNeeded = statusSelect.value == "action needed"
|
||||||
|
@ -371,6 +374,7 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
// Initial handling of rejectionReasonFormGroup display
|
// Initial handling of rejectionReasonFormGroup display
|
||||||
showOrHideObject(rejectionReasonFormGroup, show=isRejected)
|
showOrHideObject(rejectionReasonFormGroup, show=isRejected)
|
||||||
showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded)
|
showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded)
|
||||||
|
showOrHideObject(actionNeededReasonEmailFormGroup, show=isActionNeeded)
|
||||||
|
|
||||||
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
// Listen to change events and handle rejectionReasonFormGroup display, then save status to session storage
|
||||||
statusSelect.addEventListener('change', function() {
|
statusSelect.addEventListener('change', function() {
|
||||||
|
@ -382,6 +386,7 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
|
|
||||||
isActionNeeded = statusSelect.value == "action needed"
|
isActionNeeded = statusSelect.value == "action needed"
|
||||||
showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded)
|
showOrHideObject(actionNeededReasonFormGroup, show=isActionNeeded)
|
||||||
|
showOrHideObject(actionNeededReasonEmailFormGroup, show=isActionNeeded)
|
||||||
addOrRemoveSessionBoolean("showActionNeededReason", add=isActionNeeded)
|
addOrRemoveSessionBoolean("showActionNeededReason", add=isActionNeeded)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -398,6 +403,7 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
|
|
||||||
let showActionNeededReason = sessionStorage.getItem("showActionNeededReason") !== null
|
let showActionNeededReason = sessionStorage.getItem("showActionNeededReason") !== null
|
||||||
showOrHideObject(actionNeededReasonFormGroup, show=showActionNeededReason)
|
showOrHideObject(actionNeededReasonFormGroup, show=showActionNeededReason)
|
||||||
|
showOrHideObject(actionNeededReasonEmailFormGroup, show=isActionNeeded)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -421,42 +427,6 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
sessionStorage.removeItem(name);
|
sessionStorage.removeItem(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
let statusSelect = document.getElementById('id_status');
|
|
||||||
|
|
||||||
function moveStatusChangelog(actionNeededReasonFormGroup, statusSelect) {
|
|
||||||
if (!actionNeededReasonFormGroup || !statusSelect) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let flexContainer = actionNeededReasonFormGroup.querySelector('.flex-container');
|
|
||||||
let statusChangelog = document.getElementById('dja-status-changelog');
|
|
||||||
|
|
||||||
// On action needed, show the email that will be sent out
|
|
||||||
let showReasonEmailContainer = document.querySelector("#action_needed_reason_email_readonly")
|
|
||||||
|
|
||||||
// Prepopulate values on page load.
|
|
||||||
if (statusSelect.value === "action needed") {
|
|
||||||
flexContainer.parentNode.insertBefore(statusChangelog, flexContainer.nextSibling);
|
|
||||||
showElement(showReasonEmailContainer);
|
|
||||||
} else {
|
|
||||||
// Move the changelog back to its original location
|
|
||||||
let statusFlexContainer = statusSelect.closest('.flex-container');
|
|
||||||
statusFlexContainer.parentNode.insertBefore(statusChangelog, statusFlexContainer.nextSibling);
|
|
||||||
hideElement(showReasonEmailContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the function on page load
|
|
||||||
moveStatusChangelog(actionNeededReasonFormGroup, statusSelect);
|
|
||||||
|
|
||||||
// Add event listener to handle changes to the selector itself
|
|
||||||
statusSelect.addEventListener('change', function() {
|
|
||||||
moveStatusChangelog(actionNeededReasonFormGroup, statusSelect);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/** An IIFE for toggling the submit bar on domain request forms
|
/** An IIFE for toggling the submit bar on domain request forms
|
||||||
|
@ -552,13 +522,13 @@ function initializeWidgetOnList(list, parentId) {
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
/** An IIFE that hooks up to the "show email" button.
|
/** An IIFE that hooks to the show/hide button underneath action needed reason.
|
||||||
* which shows the auto generated email on action needed reason.
|
* This shows the auto generated email on action needed reason.
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
let actionNeededReasonDropdown = document.querySelector("#id_action_needed_reason");
|
let actionNeededReasonDropdown = document.querySelector("#id_action_needed_reason");
|
||||||
let actionNeededEmail = document.querySelector("#action_needed_reason_email_view_more");
|
let actionNeededEmail = document.querySelector("#action_needed_reason_email_view_more");
|
||||||
if(actionNeededReasonDropdown && actionNeededEmail && container) {
|
if(actionNeededReasonDropdown && actionNeededEmail) {
|
||||||
// Add a change listener to the action needed reason dropdown
|
// Add a change listener to the action needed reason dropdown
|
||||||
handleChangeActionNeededEmail(actionNeededReasonDropdown, actionNeededEmail);
|
handleChangeActionNeededEmail(actionNeededReasonDropdown, actionNeededEmail);
|
||||||
}
|
}
|
||||||
|
|
|
@ -787,10 +787,16 @@ div.dja__model-description{
|
||||||
color: var(--link-fg);
|
color: var(--link-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.textarea-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 610px;
|
||||||
|
}
|
||||||
|
|
||||||
.dja-readonly-textarea-container {
|
.dja-readonly-textarea-container {
|
||||||
|
width: 100%;
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 610px;
|
max-width: 610px;
|
||||||
resize: none;
|
resize: none;
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
|
|
||||||
|
@ -827,3 +833,20 @@ div.dja__model-description{
|
||||||
// Many elements in django admin try to override this, so we need !important.
|
// Many elements in django admin try to override this, so we need !important.
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.margin-top-0 {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.padding-top-0 {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.flex-container {
|
||||||
|
@media screen and (min-width: 700px) and (max-width: 1150px) {
|
||||||
|
&.flex-container--mobile-inline {
|
||||||
|
display: inline !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -660,6 +660,7 @@ ALLOWED_HOSTS = [
|
||||||
"getgov-stable.app.cloud.gov",
|
"getgov-stable.app.cloud.gov",
|
||||||
"getgov-staging.app.cloud.gov",
|
"getgov-staging.app.cloud.gov",
|
||||||
"getgov-development.app.cloud.gov",
|
"getgov-development.app.cloud.gov",
|
||||||
|
"getgov-ms.app.cloud.gov",
|
||||||
"getgov-ag.app.cloud.gov",
|
"getgov-ag.app.cloud.gov",
|
||||||
"getgov-litterbox.app.cloud.gov",
|
"getgov-litterbox.app.cloud.gov",
|
||||||
"getgov-hotgov.app.cloud.gov",
|
"getgov-hotgov.app.cloud.gov",
|
||||||
|
|
|
@ -22,6 +22,11 @@ class UserFixture:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ADMINS = [
|
ADMINS = [
|
||||||
|
{
|
||||||
|
"username": "be17c826-e200-4999-9389-2ded48c43691",
|
||||||
|
"first_name": "Matthew",
|
||||||
|
"last_name": "Spence",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"username": "5f283494-31bd-49b5-b024-a7e7cae00848",
|
"username": "5f283494-31bd-49b5-b024-a7e7cae00848",
|
||||||
"first_name": "Rachid",
|
"first_name": "Rachid",
|
||||||
|
@ -115,6 +120,11 @@ class UserFixture:
|
||||||
]
|
]
|
||||||
|
|
||||||
STAFF = [
|
STAFF = [
|
||||||
|
{
|
||||||
|
"username": "d6bf296b-fac5-47ff-9c12-f88ccc5c1b99",
|
||||||
|
"first_name": "Matthew-Analyst",
|
||||||
|
"last_name": "Spence-Analyst",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"username": "319c490d-453b-43d9-bc4d-7d6cd8ff6844",
|
"username": "319c490d-453b-43d9-bc4d-7d6cd8ff6844",
|
||||||
"first_name": "Rachid-Analyst",
|
"first_name": "Rachid-Analyst",
|
||||||
|
|
|
@ -4,7 +4,7 @@ from .domain import (
|
||||||
NameserverFormset,
|
NameserverFormset,
|
||||||
DomainSecurityEmailForm,
|
DomainSecurityEmailForm,
|
||||||
DomainOrgNameAddressForm,
|
DomainOrgNameAddressForm,
|
||||||
ContactForm,
|
UserForm,
|
||||||
SeniorOfficialContactForm,
|
SeniorOfficialContactForm,
|
||||||
DomainDnssecForm,
|
DomainDnssecForm,
|
||||||
DomainDsdataFormset,
|
DomainDsdataFormset,
|
||||||
|
|
|
@ -16,7 +16,7 @@ from registrar.utility.errors import (
|
||||||
SecurityEmailErrorCodes,
|
SecurityEmailErrorCodes,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..models import Contact, DomainInformation, Domain
|
from ..models import Contact, DomainInformation, Domain, User
|
||||||
from .common import (
|
from .common import (
|
||||||
ALGORITHM_CHOICES,
|
ALGORITHM_CHOICES,
|
||||||
DIGEST_TYPE_CHOICES,
|
DIGEST_TYPE_CHOICES,
|
||||||
|
@ -203,6 +203,63 @@ NameserverFormset = formset_factory(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserForm(forms.ModelForm):
|
||||||
|
"""Form for updating users."""
|
||||||
|
|
||||||
|
email = forms.EmailField(max_length=None)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ["first_name", "middle_name", "last_name", "title", "email", "phone"]
|
||||||
|
widgets = {
|
||||||
|
"first_name": forms.TextInput,
|
||||||
|
"middle_name": forms.TextInput,
|
||||||
|
"last_name": forms.TextInput,
|
||||||
|
"title": forms.TextInput,
|
||||||
|
"email": forms.EmailInput,
|
||||||
|
"phone": RegionalPhoneNumberWidget,
|
||||||
|
}
|
||||||
|
|
||||||
|
# the database fields have blank=True so ModelForm doesn't create
|
||||||
|
# required fields by default. Use this list in __init__ to mark each
|
||||||
|
# of these fields as required
|
||||||
|
required = ["first_name", "last_name", "title", "email", "phone"]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
# take off maxlength attribute for the phone number field
|
||||||
|
# which interferes with out input_with_errors template tag
|
||||||
|
self.fields["phone"].widget.attrs.pop("maxlength", None)
|
||||||
|
|
||||||
|
# Define a custom validator for the email field with a custom error message
|
||||||
|
email_max_length_validator = MaxLengthValidator(320, message="Response must be less than 320 characters.")
|
||||||
|
self.fields["email"].validators.append(email_max_length_validator)
|
||||||
|
|
||||||
|
for field_name in self.required:
|
||||||
|
self.fields[field_name].required = True
|
||||||
|
|
||||||
|
# Set custom form label
|
||||||
|
self.fields["middle_name"].label = "Middle name (optional)"
|
||||||
|
|
||||||
|
# Set custom error messages
|
||||||
|
self.fields["first_name"].error_messages = {"required": "Enter your first name / given name."}
|
||||||
|
self.fields["last_name"].error_messages = {"required": "Enter your last name / family name."}
|
||||||
|
self.fields["title"].error_messages = {
|
||||||
|
"required": "Enter your title or role in your organization (e.g., Chief Information Officer)"
|
||||||
|
}
|
||||||
|
self.fields["email"].error_messages = {
|
||||||
|
"required": "Enter your email address in the required format, like name@example.com."
|
||||||
|
}
|
||||||
|
self.fields["phone"].error_messages["required"] = "Enter your phone number."
|
||||||
|
self.domainInfo = None
|
||||||
|
|
||||||
|
def set_domain_info(self, domainInfo):
|
||||||
|
"""Set the domain information for the form.
|
||||||
|
The form instance is associated with the contact itself. In order to access the associated
|
||||||
|
domain information object, this needs to be set in the form by the view."""
|
||||||
|
self.domainInfo = domainInfo
|
||||||
|
|
||||||
|
|
||||||
class ContactForm(forms.ModelForm):
|
class ContactForm(forms.ModelForm):
|
||||||
"""Form for updating contacts."""
|
"""Form for updating contacts."""
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from registrar.models.contact import Contact
|
from registrar.models.user import User
|
||||||
|
|
||||||
from django.core.validators import MaxLengthValidator
|
from django.core.validators import MaxLengthValidator
|
||||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||||
|
@ -13,7 +13,7 @@ class UserProfileForm(forms.ModelForm):
|
||||||
redirect = forms.CharField(widget=forms.HiddenInput(), required=False)
|
redirect = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Contact
|
model = User
|
||||||
fields = ["first_name", "middle_name", "last_name", "title", "email", "phone"]
|
fields = ["first_name", "middle_name", "last_name", "title", "email", "phone"]
|
||||||
widgets = {
|
widgets = {
|
||||||
"first_name": forms.TextInput,
|
"first_name": forms.TextInput,
|
||||||
|
|
|
@ -1,242 +0,0 @@
|
||||||
import logging
|
|
||||||
import argparse
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from django.core.management import BaseCommand
|
|
||||||
|
|
||||||
from registrar.management.commands.utility.terminal_helper import (
|
|
||||||
TerminalColors,
|
|
||||||
TerminalHelper,
|
|
||||||
)
|
|
||||||
from registrar.models.contact import Contact
|
|
||||||
from registrar.models.user import User
|
|
||||||
from registrar.models.utility.domain_helper import DomainHelper
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = """Copy first and last names from a contact to
|
|
||||||
a related user if it exists and if its first and last name
|
|
||||||
properties are null or blank strings."""
|
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# ===================== ARGUMENTS =====================
|
|
||||||
# ======================================================
|
|
||||||
def add_arguments(self, parser):
|
|
||||||
parser.add_argument("--debug", action=argparse.BooleanOptionalAction)
|
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# ===================== PRINTING ======================
|
|
||||||
# ======================================================
|
|
||||||
def print_debug_mode_statements(self, debug_on: bool):
|
|
||||||
"""Prints additional terminal statements to indicate if --debug
|
|
||||||
or --limitParse are in use"""
|
|
||||||
TerminalHelper.print_conditional(
|
|
||||||
debug_on,
|
|
||||||
f"""{TerminalColors.OKCYAN}
|
|
||||||
----------DEBUG MODE ON----------
|
|
||||||
Detailed print statements activated.
|
|
||||||
{TerminalColors.ENDC}
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
def print_summary_of_findings(
|
|
||||||
self,
|
|
||||||
skipped_contacts,
|
|
||||||
eligible_users,
|
|
||||||
processed_users,
|
|
||||||
debug_on,
|
|
||||||
):
|
|
||||||
"""Prints to terminal a summary of findings from
|
|
||||||
copying first and last names from contacts to users"""
|
|
||||||
|
|
||||||
total_eligible_users = len(eligible_users)
|
|
||||||
total_skipped_contacts = len(skipped_contacts)
|
|
||||||
total_processed_users = len(processed_users)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"""{TerminalColors.OKGREEN}
|
|
||||||
============= FINISHED ===============
|
|
||||||
Skipped {total_skipped_contacts} contacts
|
|
||||||
Found {total_eligible_users} users linked to contacts
|
|
||||||
Processed {total_processed_users} users
|
|
||||||
{TerminalColors.ENDC}
|
|
||||||
""" # noqa
|
|
||||||
)
|
|
||||||
|
|
||||||
# DEBUG:
|
|
||||||
TerminalHelper.print_conditional(
|
|
||||||
debug_on,
|
|
||||||
f"""{TerminalColors.YELLOW}
|
|
||||||
======= DEBUG OUTPUT =======
|
|
||||||
Users who have a linked contact:
|
|
||||||
{eligible_users}
|
|
||||||
|
|
||||||
Processed users (users who have a linked contact and a missing first or last name):
|
|
||||||
{processed_users}
|
|
||||||
|
|
||||||
===== SKIPPED CONTACTS =====
|
|
||||||
{skipped_contacts}
|
|
||||||
|
|
||||||
{TerminalColors.ENDC}
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# =================== USER =====================
|
|
||||||
# ======================================================
|
|
||||||
def update_user(self, contact: Contact, debug_on: bool):
|
|
||||||
"""Given a contact with a first_name and last_name, find & update an existing
|
|
||||||
corresponding user if her first_name and last_name are null.
|
|
||||||
|
|
||||||
Returns tuple of eligible (is linked to the contact) and processed
|
|
||||||
(first and last are blank) users.
|
|
||||||
"""
|
|
||||||
|
|
||||||
user_exists = User.objects.filter(contact=contact).exists()
|
|
||||||
if user_exists:
|
|
||||||
try:
|
|
||||||
# ----------------------- UPDATE USER -----------------------
|
|
||||||
# ---- GET THE USER
|
|
||||||
eligible_user = User.objects.get(contact=contact)
|
|
||||||
processed_user = None
|
|
||||||
# DEBUG:
|
|
||||||
TerminalHelper.print_conditional(
|
|
||||||
debug_on,
|
|
||||||
f"""{TerminalColors.YELLOW}
|
|
||||||
> Found linked user for contact:
|
|
||||||
{contact} {contact.email} {contact.first_name} {contact.last_name}
|
|
||||||
> The linked user is {eligible_user} {eligible_user.username}
|
|
||||||
{TerminalColors.ENDC}""", # noqa
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get the fields that exist on both User and Contact. Excludes id.
|
|
||||||
common_fields = DomainHelper.get_common_fields(User, Contact)
|
|
||||||
if "email" in common_fields:
|
|
||||||
# Don't change the email field.
|
|
||||||
common_fields.remove("email")
|
|
||||||
|
|
||||||
for field in common_fields:
|
|
||||||
# Grab the value that contact has stored for this field
|
|
||||||
new_value = getattr(contact, field)
|
|
||||||
|
|
||||||
# Set it on the user field
|
|
||||||
setattr(eligible_user, field, new_value)
|
|
||||||
|
|
||||||
eligible_user.save()
|
|
||||||
processed_user = eligible_user
|
|
||||||
|
|
||||||
return (
|
|
||||||
eligible_user,
|
|
||||||
processed_user,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as error:
|
|
||||||
logger.warning(
|
|
||||||
f"""
|
|
||||||
{TerminalColors.FAIL}
|
|
||||||
!!! ERROR: An exception occured in the
|
|
||||||
User table for the following user:
|
|
||||||
{contact.email} {contact.first_name} {contact.last_name}
|
|
||||||
|
|
||||||
Exception is: {error}
|
|
||||||
----------TERMINATING----------"""
|
|
||||||
)
|
|
||||||
sys.exit()
|
|
||||||
else:
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# ================= PROCESS CONTACTS ==================
|
|
||||||
# ======================================================
|
|
||||||
|
|
||||||
def process_contacts(
|
|
||||||
self,
|
|
||||||
debug_on,
|
|
||||||
skipped_contacts=[],
|
|
||||||
eligible_users=[],
|
|
||||||
processed_users=[],
|
|
||||||
):
|
|
||||||
for contact in Contact.objects.all():
|
|
||||||
TerminalHelper.print_conditional(
|
|
||||||
debug_on,
|
|
||||||
f"{TerminalColors.OKCYAN}"
|
|
||||||
"Processing Contact: "
|
|
||||||
f"{contact.email},"
|
|
||||||
f" {contact.first_name},"
|
|
||||||
f" {contact.last_name}"
|
|
||||||
f"{TerminalColors.ENDC}",
|
|
||||||
)
|
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# ====================== USER =======================
|
|
||||||
(eligible_user, processed_user) = self.update_user(contact, debug_on)
|
|
||||||
|
|
||||||
debug_string = ""
|
|
||||||
if eligible_user:
|
|
||||||
# ---------------- UPDATED ----------------
|
|
||||||
eligible_users.append(contact.email)
|
|
||||||
debug_string = f"eligible user: {eligible_user}"
|
|
||||||
if processed_user:
|
|
||||||
processed_users.append(contact.email)
|
|
||||||
debug_string = f"processed user: {processed_user}"
|
|
||||||
else:
|
|
||||||
skipped_contacts.append(contact.email)
|
|
||||||
debug_string = f"skipped user: {contact.email}"
|
|
||||||
|
|
||||||
# DEBUG:
|
|
||||||
TerminalHelper.print_conditional(
|
|
||||||
debug_on,
|
|
||||||
(f"{TerminalColors.OKCYAN} {debug_string} {TerminalColors.ENDC}"),
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
skipped_contacts,
|
|
||||||
eligible_users,
|
|
||||||
processed_users,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ======================================================
|
|
||||||
# ===================== HANDLE ========================
|
|
||||||
# ======================================================
|
|
||||||
def handle(
|
|
||||||
self,
|
|
||||||
**options,
|
|
||||||
):
|
|
||||||
"""Parse entries in Contact table
|
|
||||||
and update valid corresponding entries in the
|
|
||||||
User table."""
|
|
||||||
|
|
||||||
# grab command line arguments and store locally...
|
|
||||||
debug_on = options.get("debug")
|
|
||||||
|
|
||||||
self.print_debug_mode_statements(debug_on)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"""{TerminalColors.OKCYAN}
|
|
||||||
==========================
|
|
||||||
Beginning Data Transfer
|
|
||||||
==========================
|
|
||||||
{TerminalColors.ENDC}"""
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
f"""{TerminalColors.OKCYAN}
|
|
||||||
========= Adding Domains and Domain Invitations =========
|
|
||||||
{TerminalColors.ENDC}"""
|
|
||||||
)
|
|
||||||
(
|
|
||||||
skipped_contacts,
|
|
||||||
eligible_users,
|
|
||||||
processed_users,
|
|
||||||
) = self.process_contacts(
|
|
||||||
debug_on,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.print_summary_of_findings(
|
|
||||||
skipped_contacts,
|
|
||||||
eligible_users,
|
|
||||||
processed_users,
|
|
||||||
debug_on,
|
|
||||||
)
|
|
|
@ -50,7 +50,7 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
# Generate a file locally for upload
|
# Generate a file locally for upload
|
||||||
with open(file_path, "w") as file:
|
with open(file_path, "w") as file:
|
||||||
csv_export.export_data_federal_to_csv(file)
|
csv_export.DomainDataFederal.export_data_to_csv(file)
|
||||||
|
|
||||||
if check_path and not os.path.exists(file_path):
|
if check_path and not os.path.exists(file_path):
|
||||||
raise FileNotFoundError(f"Could not find newly created file at '{file_path}'")
|
raise FileNotFoundError(f"Could not find newly created file at '{file_path}'")
|
||||||
|
|
|
@ -49,7 +49,7 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
# Generate a file locally for upload
|
# Generate a file locally for upload
|
||||||
with open(file_path, "w") as file:
|
with open(file_path, "w") as file:
|
||||||
csv_export.export_data_full_to_csv(file)
|
csv_export.DomainDataFull.export_data_to_csv(file)
|
||||||
|
|
||||||
if check_path and not os.path.exists(file_path):
|
if check_path and not os.path.exists(file_path):
|
||||||
raise FileNotFoundError(f"Could not find newly created file at '{file_path}'")
|
raise FileNotFoundError(f"Could not find newly created file at '{file_path}'")
|
||||||
|
|
|
@ -65,13 +65,6 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
resourcename = f"{table_name}Resource"
|
resourcename = f"{table_name}Resource"
|
||||||
|
|
||||||
# if table_name is Contact, clean the table first
|
|
||||||
# User table is loaded before Contact, and signals create
|
|
||||||
# rows in Contact table which break the import, so need
|
|
||||||
# to be cleaned again before running import on Contact table
|
|
||||||
if table_name == "Contact":
|
|
||||||
self.clean_table(table_name)
|
|
||||||
|
|
||||||
# Define the directory and the pattern for csv filenames
|
# Define the directory and the pattern for csv filenames
|
||||||
tmp_dir = "tmp"
|
tmp_dir = "tmp"
|
||||||
pattern = f"{table_name}_"
|
pattern = f"{table_name}_"
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
from registrar.management.commands.utility.terminal_helper import PopulateScriptTemplate, TerminalColors
|
||||||
|
from registrar.models import DomainInformation
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand, PopulateScriptTemplate):
|
||||||
|
"""
|
||||||
|
This command uses the PopulateScriptTemplate,
|
||||||
|
which provides reusable logging and bulk updating functions for mass-updating fields.
|
||||||
|
"""
|
||||||
|
|
||||||
|
help = "Loops through each valid DomainInformation object and updates its Senior Official"
|
||||||
|
prompt_title = "Do you wish to update all Senior Officials for Domain Information?"
|
||||||
|
|
||||||
|
def handle(self, domain_info_csv_path, **kwargs):
|
||||||
|
"""Loops through each valid DomainInformation object and updates its senior official field"""
|
||||||
|
|
||||||
|
# Check if the provided file path is valid.
|
||||||
|
if not os.path.isfile(domain_info_csv_path):
|
||||||
|
raise argparse.ArgumentTypeError(f"Invalid file path '{domain_info_csv_path}'")
|
||||||
|
|
||||||
|
# Simple check to make sure we don't accidentally pass in the wrong file. Crude but it works.
|
||||||
|
if "information" not in domain_info_csv_path.lower():
|
||||||
|
raise argparse.ArgumentTypeError(f"Invalid file for domain information: '{domain_info_csv_path}'")
|
||||||
|
|
||||||
|
# Get all ao data.
|
||||||
|
self.ao_dict = {}
|
||||||
|
self.ao_dict = self.read_csv_file_and_get_contacts(domain_info_csv_path)
|
||||||
|
|
||||||
|
self.mass_update_records(
|
||||||
|
DomainInformation, filter_conditions={"senior_official__isnull": True}, fields_to_update=["senior_official"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
"""Add command line arguments."""
|
||||||
|
parser.add_argument(
|
||||||
|
"--domain_info_csv_path", help="A csv containing the domain information id and the contact id"
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_csv_file_and_get_contacts(self, file):
|
||||||
|
dict_data = {}
|
||||||
|
with open(file, "r") as requested_file:
|
||||||
|
reader = csv.DictReader(requested_file)
|
||||||
|
for row in reader:
|
||||||
|
domain_info_id = row.get("id")
|
||||||
|
ao_id = row.get("authorizing_official")
|
||||||
|
if ao_id:
|
||||||
|
ao_id = int(ao_id)
|
||||||
|
if domain_info_id and ao_id:
|
||||||
|
dict_data[int(domain_info_id)] = ao_id
|
||||||
|
|
||||||
|
return dict_data
|
||||||
|
|
||||||
|
def update_record(self, record: DomainInformation):
|
||||||
|
"""Defines how we update the senior official field on each record."""
|
||||||
|
record.senior_official_id = self.ao_dict.get(record.id)
|
||||||
|
logger.info(f"{TerminalColors.OKCYAN}Updating {str(record)} => {record.senior_official}{TerminalColors.ENDC}")
|
||||||
|
|
||||||
|
def should_skip_record(self, record) -> bool: # noqa
|
||||||
|
"""Defines the conditions in which we should skip updating a record."""
|
||||||
|
# Don't update this record if there isn't ao data to pull from
|
||||||
|
if self.ao_dict.get(record.id) is None:
|
||||||
|
logger.info(
|
||||||
|
f"{TerminalColors.YELLOW}Skipping update for {str(record)} => "
|
||||||
|
f"Missing authorizing_official data.{TerminalColors.ENDC}"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
|
@ -0,0 +1,81 @@
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
from registrar.management.commands.utility.terminal_helper import PopulateScriptTemplate, TerminalColors
|
||||||
|
from registrar.models import DomainRequest
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand, PopulateScriptTemplate):
|
||||||
|
"""
|
||||||
|
This command uses the PopulateScriptTemplate,
|
||||||
|
which provides reusable logging and bulk updating functions for mass-updating fields.
|
||||||
|
"""
|
||||||
|
|
||||||
|
help = """Loops through each valid DomainRequest object and updates its senior official field"""
|
||||||
|
prompt_title = "Do you wish to update all Senior Officials for Domain Requests?"
|
||||||
|
|
||||||
|
def handle(self, domain_request_csv_path, **kwargs):
|
||||||
|
"""Loops through each valid DomainRequest object and updates its senior official field"""
|
||||||
|
|
||||||
|
# Check if the provided file path is valid.
|
||||||
|
if not os.path.isfile(domain_request_csv_path):
|
||||||
|
raise argparse.ArgumentTypeError(f"Invalid file path '{domain_request_csv_path}'")
|
||||||
|
|
||||||
|
# Simple check to make sure we don't accidentally pass in the wrong file. Crude but it works.
|
||||||
|
if "request" not in domain_request_csv_path.lower():
|
||||||
|
raise argparse.ArgumentTypeError(f"Invalid file for domain requests: '{domain_request_csv_path}'")
|
||||||
|
|
||||||
|
# Get all ao data.
|
||||||
|
self.ao_dict = {}
|
||||||
|
self.ao_dict = self.read_csv_file_and_get_contacts(domain_request_csv_path)
|
||||||
|
|
||||||
|
self.mass_update_records(
|
||||||
|
DomainRequest,
|
||||||
|
filter_conditions={
|
||||||
|
"senior_official__isnull": True,
|
||||||
|
},
|
||||||
|
fields_to_update=["senior_official"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
"""Add command line arguments."""
|
||||||
|
parser.add_argument(
|
||||||
|
"--domain_request_csv_path", help="A csv containing the domain request id and the contact id"
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_csv_file_and_get_contacts(self, file):
|
||||||
|
dict_data: dict = {}
|
||||||
|
with open(file, "r") as requested_file:
|
||||||
|
reader = csv.DictReader(requested_file)
|
||||||
|
for row in reader:
|
||||||
|
domain_request_id = row.get("id")
|
||||||
|
ao_id = row.get("authorizing_official")
|
||||||
|
if ao_id:
|
||||||
|
ao_id = int(ao_id)
|
||||||
|
if domain_request_id and ao_id:
|
||||||
|
dict_data[int(domain_request_id)] = ao_id
|
||||||
|
|
||||||
|
return dict_data
|
||||||
|
|
||||||
|
def update_record(self, record: DomainRequest):
|
||||||
|
"""Defines how we update the federal_type field on each record."""
|
||||||
|
record.senior_official_id = self.ao_dict.get(record.id)
|
||||||
|
# record.senior_official = Contact.objects.get(id=contact_id)
|
||||||
|
logger.info(f"{TerminalColors.OKCYAN}Updating {str(record)} => {record.senior_official}{TerminalColors.ENDC}")
|
||||||
|
|
||||||
|
def should_skip_record(self, record) -> bool: # noqa
|
||||||
|
"""Defines the conditions in which we should skip updating a record."""
|
||||||
|
# Don't update this record if there isn't ao data to pull from
|
||||||
|
if self.ao_dict.get(record.id) is None:
|
||||||
|
logger.info(
|
||||||
|
f"{TerminalColors.YELLOW}Skipping update for {str(record)} => "
|
||||||
|
f"Missing authorizing_official data.{TerminalColors.ENDC}"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-07-02 21:03
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import phonenumber_field.modelfields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0109_domaininformation_sub_organization_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="SeniorOfficial",
|
||||||
|
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)),
|
||||||
|
("first_name", models.CharField(verbose_name="first name")),
|
||||||
|
("last_name", models.CharField(verbose_name="last name")),
|
||||||
|
("title", models.CharField(verbose_name="title / role")),
|
||||||
|
(
|
||||||
|
"phone",
|
||||||
|
phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None),
|
||||||
|
),
|
||||||
|
("email", models.EmailField(blank=True, max_length=320, null=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="portfolio",
|
||||||
|
name="senior_official",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="Associated senior official",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to="registrar.seniorofficial",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
37
src/registrar/migrations/0111_create_groups_v15.py
Normal file
37
src/registrar/migrations/0111_create_groups_v15.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
|
||||||
|
# It is dependent on 0079 (which populates federal agencies)
|
||||||
|
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
|
||||||
|
# in the user_group model then:
|
||||||
|
# [NOT RECOMMENDED]
|
||||||
|
# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions
|
||||||
|
# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
|
||||||
|
# step 3: fake run the latest migration in the migrations list
|
||||||
|
# [RECOMMENDED]
|
||||||
|
# Alternatively:
|
||||||
|
# step 1: duplicate the migration that loads data
|
||||||
|
# step 2: docker-compose exec app ./manage.py migrate
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from registrar.models import UserGroup
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
# For linting: RunPython expects a function reference,
|
||||||
|
# so let's give it one
|
||||||
|
def create_groups(apps, schema_editor) -> Any:
|
||||||
|
UserGroup.create_cisa_analyst_group(apps, schema_editor)
|
||||||
|
UserGroup.create_full_access_group(apps, schema_editor)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0110_seniorofficial_portfolio_senior_official"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
create_groups,
|
||||||
|
reverse_code=migrations.RunPython.noop,
|
||||||
|
atomic=True,
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-07-02 19:52
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0111_create_groups_v15"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveIndex(
|
||||||
|
model_name="contact",
|
||||||
|
name="registrar_c_user_id_4059c4_idx",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="contact",
|
||||||
|
name="user",
|
||||||
|
),
|
||||||
|
]
|
|
@ -19,6 +19,7 @@ from .waffle_flag import WaffleFlag
|
||||||
from .portfolio import Portfolio
|
from .portfolio import Portfolio
|
||||||
from .domain_group import DomainGroup
|
from .domain_group import DomainGroup
|
||||||
from .suborganization import Suborganization
|
from .suborganization import Suborganization
|
||||||
|
from .senior_official import SeniorOfficial
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -42,6 +43,7 @@ __all__ = [
|
||||||
"Portfolio",
|
"Portfolio",
|
||||||
"DomainGroup",
|
"DomainGroup",
|
||||||
"Suborganization",
|
"Suborganization",
|
||||||
|
"SeniorOfficial",
|
||||||
]
|
]
|
||||||
|
|
||||||
auditlog.register(Contact)
|
auditlog.register(Contact)
|
||||||
|
@ -64,3 +66,4 @@ auditlog.register(WaffleFlag)
|
||||||
auditlog.register(Portfolio)
|
auditlog.register(Portfolio)
|
||||||
auditlog.register(DomainGroup)
|
auditlog.register(DomainGroup)
|
||||||
auditlog.register(Suborganization)
|
auditlog.register(Suborganization)
|
||||||
|
auditlog.register(SeniorOfficial)
|
||||||
|
|
|
@ -8,30 +8,15 @@ from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
||||||
class Contact(TimeStampedModel):
|
class Contact(TimeStampedModel):
|
||||||
"""
|
"""
|
||||||
Contact information follows a similar pattern for each contact.
|
Contact information follows a similar pattern for each contact.
|
||||||
|
|
||||||
This model uses signals [as defined in [signals.py](../../src/registrar/signals.py)].
|
|
||||||
When a new user is created through Login.gov, a contact object will be created and
|
|
||||||
associated on the `user` field.
|
|
||||||
|
|
||||||
If the `user` object already exists, the underlying user object
|
|
||||||
will be updated if any updates are made to it through Login.gov.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Contains meta information about this class"""
|
"""Contains meta information about this class"""
|
||||||
|
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["user"]),
|
|
||||||
models.Index(fields=["email"]),
|
models.Index(fields=["email"]),
|
||||||
]
|
]
|
||||||
|
|
||||||
user = models.OneToOneField(
|
|
||||||
"registrar.User",
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
)
|
|
||||||
|
|
||||||
first_name = models.CharField(
|
first_name = models.CharField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
@ -110,38 +95,6 @@ class Contact(TimeStampedModel):
|
||||||
def has_contact_info(self):
|
def has_contact_info(self):
|
||||||
return bool(self.title or self.email or self.phone)
|
return bool(self.title or self.email or self.phone)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
# Call the parent class's save method to perform the actual save
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
if self.user:
|
|
||||||
updated = False
|
|
||||||
|
|
||||||
# Update first name and last name if necessary
|
|
||||||
if not self.user.first_name or not self.user.last_name:
|
|
||||||
self.user.first_name = self.first_name
|
|
||||||
self.user.last_name = self.last_name
|
|
||||||
updated = True
|
|
||||||
|
|
||||||
# Update middle_name if necessary
|
|
||||||
if not self.user.middle_name:
|
|
||||||
self.user.middle_name = self.middle_name
|
|
||||||
updated = True
|
|
||||||
|
|
||||||
# Update phone if necessary
|
|
||||||
if not self.user.phone:
|
|
||||||
self.user.phone = self.phone
|
|
||||||
updated = True
|
|
||||||
|
|
||||||
# Update title if necessary
|
|
||||||
if not self.user.title:
|
|
||||||
self.user.title = self.title
|
|
||||||
updated = True
|
|
||||||
|
|
||||||
# Save user if any updates were made
|
|
||||||
if updated:
|
|
||||||
self.user.save()
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.first_name or self.last_name:
|
if self.first_name or self.last_name:
|
||||||
return self.get_formatted_name()
|
return self.get_formatted_name()
|
||||||
|
|
|
@ -151,6 +151,11 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
# previously existed but has been deleted from the registry
|
# previously existed but has been deleted from the registry
|
||||||
DELETED = "deleted", "Deleted"
|
DELETED = "deleted", "Deleted"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_state_label(cls, state: str):
|
||||||
|
"""Returns the associated label for a given state value"""
|
||||||
|
return cls(state).label if state else None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_help_text(cls, state) -> str:
|
def get_help_text(cls, state) -> str:
|
||||||
"""Returns a help message for a desired state. If none is found, an empty string is returned"""
|
"""Returns a help message for a desired state. If none is found, an empty string is returned"""
|
||||||
|
|
|
@ -136,6 +136,13 @@ class DomainRequest(TimeStampedModel):
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_org_label(cls, org_name: str):
|
def get_org_label(cls, org_name: str):
|
||||||
"""Returns the associated label for a given org name"""
|
"""Returns the associated label for a given org name"""
|
||||||
|
# This is an edgecase on domains with no org.
|
||||||
|
# This unlikely to happen but
|
||||||
|
# a break will occur in certain edge cases without this.
|
||||||
|
# (more specifically, csv exports).
|
||||||
|
if not org_name:
|
||||||
|
return None
|
||||||
|
|
||||||
org_names = org_name.split("_election")
|
org_names = org_name.split("_election")
|
||||||
if len(org_names) > 0:
|
if len(org_names) > 0:
|
||||||
org_name = org_names[0]
|
org_name = org_names[0]
|
||||||
|
|
|
@ -38,6 +38,15 @@ class Portfolio(TimeStampedModel):
|
||||||
default=FederalAgency.get_non_federal_agency,
|
default=FederalAgency.get_non_federal_agency,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
senior_official = models.ForeignKey(
|
||||||
|
"registrar.SeniorOfficial",
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
help_text="Associated senior official",
|
||||||
|
unique=False,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
organization_type = models.CharField(
|
organization_type = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
choices=OrganizationChoices.choices,
|
choices=OrganizationChoices.choices,
|
||||||
|
|
50
src/registrar/models/senior_official.py
Normal file
50
src/registrar/models/senior_official.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class SeniorOfficial(TimeStampedModel):
|
||||||
|
"""
|
||||||
|
Senior Official is a distinct Contact-like entity (NOT to be inherited
|
||||||
|
from Contacts) developed for the unique role these individuals have in
|
||||||
|
managing Portfolios.
|
||||||
|
"""
|
||||||
|
|
||||||
|
first_name = models.CharField(
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
verbose_name="first name",
|
||||||
|
)
|
||||||
|
last_name = models.CharField(
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
verbose_name="last name",
|
||||||
|
)
|
||||||
|
title = models.CharField(
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
verbose_name="title / role",
|
||||||
|
)
|
||||||
|
phone = PhoneNumberField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
email = models.EmailField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
max_length=320,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_formatted_name(self):
|
||||||
|
"""Returns the contact's name in Western order."""
|
||||||
|
names = [n for n in [self.first_name, self.last_name] if n]
|
||||||
|
return " ".join(names) if names else "Unknown"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.first_name or self.last_name:
|
||||||
|
return self.get_formatted_name()
|
||||||
|
elif self.pk:
|
||||||
|
return str(self.pk)
|
||||||
|
else:
|
||||||
|
return ""
|
|
@ -23,10 +23,6 @@ class User(AbstractUser):
|
||||||
A custom user model that performs identically to the default user model
|
A custom user model that performs identically to the default user model
|
||||||
but can be customized later.
|
but can be customized later.
|
||||||
|
|
||||||
This model uses signals [as defined in [signals.py](../../src/registrar/signals.py)].
|
|
||||||
When a new user is created through Login.gov, a contact object will be created and
|
|
||||||
associated on the contacts `user` field.
|
|
||||||
|
|
||||||
If the `user` object already exists, said user object
|
If the `user` object already exists, said user object
|
||||||
will be updated if any updates are made to it through Login.gov.
|
will be updated if any updates are made to it through Login.gov.
|
||||||
"""
|
"""
|
||||||
|
@ -113,15 +109,11 @@ class User(AbstractUser):
|
||||||
Tracks if the user finished their profile setup or not. This is so
|
Tracks if the user finished their profile setup or not. This is so
|
||||||
we can globally enforce that new users provide additional account information before proceeding.
|
we can globally enforce that new users provide additional account information before proceeding.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Change this to self once the user and contact objects are merged.
|
|
||||||
# For now, since they are linked, lets test on the underlying contact object.
|
|
||||||
user_info = self.contact # noqa
|
|
||||||
user_values = [
|
user_values = [
|
||||||
user_info.first_name,
|
self.first_name,
|
||||||
user_info.last_name,
|
self.last_name,
|
||||||
user_info.title,
|
self.title,
|
||||||
user_info.phone,
|
self.phone,
|
||||||
]
|
]
|
||||||
return None not in user_values
|
return None not in user_values
|
||||||
|
|
||||||
|
@ -169,8 +161,13 @@ class User(AbstractUser):
|
||||||
"""Return count of ineligible requests"""
|
"""Return count of ineligible requests"""
|
||||||
return self.domain_requests_created.filter(status=DomainRequest.DomainRequestStatus.INELIGIBLE).count()
|
return self.domain_requests_created.filter(status=DomainRequest.DomainRequestStatus.INELIGIBLE).count()
|
||||||
|
|
||||||
|
def get_formatted_name(self):
|
||||||
|
"""Returns the contact's name in Western order."""
|
||||||
|
names = [n for n in [self.first_name, self.middle_name, self.last_name] if n]
|
||||||
|
return " ".join(names) if names else "Unknown"
|
||||||
|
|
||||||
def has_contact_info(self):
|
def has_contact_info(self):
|
||||||
return bool(self.contact.title or self.contact.email or self.contact.phone)
|
return bool(self.title or self.email or self.phone)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def needs_identity_verification(cls, email, uuid):
|
def needs_identity_verification(cls, email, uuid):
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.db.models.signals import post_save
|
|
||||||
from django.dispatch import receiver
|
|
||||||
|
|
||||||
from .models import User, Contact
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=User)
|
|
||||||
def handle_profile(sender, instance, **kwargs):
|
|
||||||
"""Method for when a User is saved.
|
|
||||||
|
|
||||||
A first time registrant may have been invited, so we'll search for a matching
|
|
||||||
Contact record, by email address, and associate them, if possible.
|
|
||||||
|
|
||||||
A first time registrant may not have a matching Contact, so we'll create one,
|
|
||||||
copying the contact values we received from Login.gov in order to initialize it.
|
|
||||||
|
|
||||||
During subsequent login, a User record may be updated with new data from Login.gov,
|
|
||||||
but in no case will we update contact values on an existing Contact record.
|
|
||||||
"""
|
|
||||||
|
|
||||||
first_name = getattr(instance, "first_name", "")
|
|
||||||
middle_name = getattr(instance, "middle_name", "")
|
|
||||||
last_name = getattr(instance, "last_name", "")
|
|
||||||
email = getattr(instance, "email", "")
|
|
||||||
phone = getattr(instance, "phone", "")
|
|
||||||
title = getattr(instance, "title", "")
|
|
||||||
|
|
||||||
is_new_user = kwargs.get("created", False)
|
|
||||||
|
|
||||||
if is_new_user:
|
|
||||||
contacts = Contact.objects.filter(email=email)
|
|
||||||
else:
|
|
||||||
contacts = Contact.objects.filter(user=instance)
|
|
||||||
|
|
||||||
if len(contacts) == 0: # no matching contact
|
|
||||||
Contact.objects.create(
|
|
||||||
user=instance,
|
|
||||||
first_name=first_name,
|
|
||||||
middle_name=middle_name,
|
|
||||||
last_name=last_name,
|
|
||||||
email=email,
|
|
||||||
phone=phone,
|
|
||||||
title=title,
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(contacts) >= 1 and is_new_user: # a matching contact
|
|
||||||
contacts[0].user = instance
|
|
||||||
contacts[0].save()
|
|
||||||
|
|
||||||
if len(contacts) > 1: # multiple matches
|
|
||||||
logger.warning(
|
|
||||||
"There are multiple Contacts with the same email address."
|
|
||||||
f" Picking #{contacts[0].id} for User #{instance.id}."
|
|
||||||
)
|
|
|
@ -32,7 +32,9 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/
|
||||||
{% for field in line %}
|
{% for field in line %}
|
||||||
<div>
|
<div>
|
||||||
{% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %}
|
{% if not line.fields|length == 1 and not field.is_readonly %}{{ field.errors }}{% endif %}
|
||||||
|
{% block flex_container_start %}
|
||||||
<div class="flex-container{% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% elif field.is_checkbox %} checkbox-row{% endif %}">
|
<div class="flex-container{% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% elif field.is_checkbox %} checkbox-row{% endif %}">
|
||||||
|
{% endblock flex_container_start %}
|
||||||
{% if field.is_checkbox %}
|
{% if field.is_checkbox %}
|
||||||
{# .gov override #}
|
{# .gov override #}
|
||||||
{% block field_checkbox %}
|
{% block field_checkbox %}
|
||||||
|
@ -52,7 +54,9 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/
|
||||||
{% endblock field_other%}
|
{% endblock field_other%}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% block flex_container_end %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock flex_container_end %}
|
||||||
|
|
||||||
{% if field.field.help_text %}
|
{% if field.field.help_text %}
|
||||||
{# .gov override #}
|
{# .gov override #}
|
||||||
|
|
|
@ -12,37 +12,24 @@
|
||||||
|
|
||||||
{% if user.has_contact_info %}
|
{% if user.has_contact_info %}
|
||||||
{# Title #}
|
{# Title #}
|
||||||
{% if user.title or user.contact.title %}
|
{% if user.title %}
|
||||||
{% if user.contact.title %}
|
{{ user.title }}
|
||||||
{{ user.contact.title }}
|
|
||||||
{% else %}
|
|
||||||
{{ user.title }}
|
|
||||||
{% endif %}
|
|
||||||
<br>
|
<br>
|
||||||
{% else %}
|
{% else %}
|
||||||
None<br>
|
None<br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# Email #}
|
{# Email #}
|
||||||
{% if user.email or user.contact.email %}
|
{% if user.email %}
|
||||||
{% if user.contact.email %}
|
{{ user.email }}
|
||||||
{{ user.contact.email }}
|
{% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %}
|
||||||
{% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %}
|
|
||||||
{% else %}
|
|
||||||
{{ user.email }}
|
|
||||||
{% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %}
|
|
||||||
{% endif %}
|
|
||||||
<br class="admin-icon-group__br">
|
<br class="admin-icon-group__br">
|
||||||
{% else %}
|
{% else %}
|
||||||
None<br>
|
None<br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Phone #}
|
{# Phone #}
|
||||||
{% if user.phone or user.contact.phone %}
|
{% if user.phone %}
|
||||||
{% if user.contact.phone %}
|
{{ user.phone }}
|
||||||
{{ user.contact.phone }}
|
|
||||||
{% else %}
|
|
||||||
{{ user.phone }}
|
|
||||||
{% endif %}
|
|
||||||
<br>
|
<br>
|
||||||
{% else %}
|
{% else %}
|
||||||
None<br>
|
None<br>
|
||||||
|
|
|
@ -6,9 +6,85 @@
|
||||||
This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
|
{% block flex_container_start %}
|
||||||
|
{% if field.field.name == "status_history" %}
|
||||||
|
<div class="flex-container flex-container--mobile-inline {% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% elif field.is_checkbox %} checkbox-row{% endif %}">
|
||||||
|
{% else %}
|
||||||
|
{% comment %} Default flex container element {% endcomment %}
|
||||||
|
<div class="flex-container{% if not line.fields|length == 1 %} fieldBox{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}{% if field.field.is_hidden %} hidden{% endif %}{% elif field.is_checkbox %} checkbox-row{% endif %}">
|
||||||
|
{% endif %}
|
||||||
|
{% endblock flex_container_start %}
|
||||||
|
|
||||||
{% block field_readonly %}
|
{% block field_readonly %}
|
||||||
{% with all_contacts=original_object.other_contacts.all %}
|
{% with all_contacts=original_object.other_contacts.all %}
|
||||||
{% if field.field.name == "other_contacts" %}
|
{% if field.field.name == "status_history" %}
|
||||||
|
{% if filtered_audit_log_entries %}
|
||||||
|
<div class="readonly">
|
||||||
|
<div class="usa-table-container--scrollable collapse--dgsimple collapsed margin-top-0" tabindex="0">
|
||||||
|
<table class="usa-table usa-table--borderless">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Changed at</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for entry in filtered_audit_log_entries %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{% if entry.status %}
|
||||||
|
{{ entry.status|default:"Error" }}
|
||||||
|
{% else %}
|
||||||
|
Error
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if entry.rejection_reason %}
|
||||||
|
- {{ entry.rejection_reason|default:"Error" }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if entry.action_needed_reason %}
|
||||||
|
- {{ entry.action_needed_reason|default:"Error" }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ entry.actor|default:"Error" }}</td>
|
||||||
|
<td>{{ entry.timestamp|date:"Y-m-d H:i:s" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="collapse-toggle--dgsimple usa-button usa-button--unstyled margin-top-0">
|
||||||
|
<span>Show details</span>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="readonly">
|
||||||
|
No changelog to display.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% elif field.field.name == "action_needed_reason_email" %}
|
||||||
|
<div class="readonly textarea-wrapper">
|
||||||
|
<div id="action_needed_reason_email_readonly" class="dja-readonly-textarea-container padding-1 margin-top-0 padding-top-0 margin-bottom-1 thin-border collapse--dgsimple collapsed">
|
||||||
|
<label class="max-full" for="action_needed_reason_email_view_more">
|
||||||
|
<strong>Sent to {% if has_profile_feature_flag %}creator{%else%}submitter{%endif%}</strong>
|
||||||
|
</label>
|
||||||
|
<textarea id="action_needed_reason_email_view_more" cols="40" rows="20" class="{% if not original_object.action_needed_reason %}display-none{% endif %}" readonly>
|
||||||
|
{{ original_object.action_needed_reason_email }}
|
||||||
|
</textarea>
|
||||||
|
<p id="no-email-message" class="{% if original_object.action_needed_reason %}display-none{% endif %}">No email will be sent.</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="collapse-toggle--dgsimple usa-button usa-button--unstyled margin-top-0 margin-bottom-1 margin-left-1">
|
||||||
|
<span>Show details</span>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% elif field.field.name == "other_contacts" %}
|
||||||
{% if all_contacts.count > 2 %}
|
{% if all_contacts.count > 2 %}
|
||||||
<div class="readonly">
|
<div class="readonly">
|
||||||
{% for contact in all_contacts %}
|
{% for contact in all_contacts %}
|
||||||
|
@ -68,80 +144,19 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
{% endblock field_readonly %}
|
{% endblock field_readonly %}
|
||||||
|
|
||||||
{% block after_help_text %}
|
{% block after_help_text %}
|
||||||
{% if field.field.name == "status" %}
|
{% if field.field.name == "action_needed_reason_email" %}
|
||||||
<div class="flex-container" id="dja-status-changelog">
|
{% comment %}
|
||||||
<label aria-label="Status changelog"></label>
|
Store the action needed reason emails in a json-based dictionary.
|
||||||
<div>
|
This allows us to change the action_needed_reason_email field dynamically, depending on value.
|
||||||
<div class="usa-table-container--scrollable collapse--dgsimple collapsed" tabindex="0">
|
The alternative to this is an API endpoint.
|
||||||
{% if filtered_audit_log_entries %}
|
|
||||||
<table class="usa-table usa-table--borderless">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>User</th>
|
|
||||||
<th>Changed at</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for entry in filtered_audit_log_entries %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{% if entry.status %}
|
|
||||||
{{ entry.status|default:"Error" }}
|
|
||||||
{% else %}
|
|
||||||
Error
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if entry.rejection_reason %}
|
|
||||||
- {{ entry.rejection_reason|default:"Error" }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if entry.action_needed_reason %}
|
|
||||||
- {{ entry.action_needed_reason|default:"Error" }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{ entry.actor|default:"Error" }}</td>
|
|
||||||
<td>{{ entry.timestamp|date:"Y-m-d H:i:s" }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% else %}
|
|
||||||
<p>No changelog to display.</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% comment %}
|
|
||||||
Store the action needed reason emails in a json-based dictionary.
|
|
||||||
This allows us to change the action_needed_reason_email field dynamically, depending on value.
|
|
||||||
The alternative to this is an API endpoint.
|
|
||||||
|
|
||||||
Given that we have a limited number of emails, doing it this way makes sense.
|
Given that we have a limited number of emails, doing it this way makes sense.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
{% if action_needed_reason_emails %}
|
{% if action_needed_reason_emails %}
|
||||||
<script id="action-needed-emails-data" type="application/json">
|
<script id="action-needed-emails-data" type="application/json">
|
||||||
{{ action_needed_reason_emails|safe }}
|
{{ action_needed_reason_emails|safe }}
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div id="action_needed_reason_email_readonly" class="dja-readonly-textarea-container padding-1 margin-top-2 thin-border display-none">
|
|
||||||
<label class="max-full" for="action_needed_reason_email_view_more">
|
|
||||||
<strong>Auto-generated email (sent to submitter)</strong>
|
|
||||||
</label>
|
|
||||||
<textarea id="action_needed_reason_email_view_more" cols="40" rows="20" class="{% if not original_object.action_needed_reason %}display-none{% endif %}" readonly>
|
|
||||||
{{ original_object.action_needed_reason_email }}
|
|
||||||
</textarea>
|
|
||||||
<p id="no-email-message" class="{% if original_object.action_needed_reason %}display-none{% endif %}">No email will be sent.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<button type="button" class="collapse-toggle--dgsimple usa-button usa-button--unstyled margin-top-2 margin-bottom-1 margin-left-1">
|
|
||||||
<span>Show details</span>
|
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
|
||||||
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% elif field.field.name == "creator" %}
|
{% elif field.field.name == "creator" %}
|
||||||
<div class="flex-container tablet:margin-top-2">
|
<div class="flex-container tablet:margin-top-2">
|
||||||
<label aria-label="Creator contact details"></label>
|
<label aria-label="Creator contact details"></label>
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
{# Conditionally display profile #}
|
{# Conditionally display profile #}
|
||||||
{% if not has_profile_feature_flag %}
|
{% if not has_profile_feature_flag %}
|
||||||
{% url 'domain-your-contact-information' pk=domain.id as url %}
|
{% url 'domain-your-contact-information' pk=domain.id as url %}
|
||||||
{% include "includes/summary_item.html" with title='Your contact information' value=request.user.contact contact='true' edit_link=url editable=domain.is_editable %}
|
{% include "includes/summary_item.html" with title='Your contact information' value=request.user contact='true' edit_link=url editable=domain.is_editable %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% url 'domain-security-email' pk=domain.id as url %}
|
{% url 'domain-security-email' pk=domain.id as url %}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
completing your domain request might take around 15 minutes.</p>
|
completing your domain request might take around 15 minutes.</p>
|
||||||
{% if has_profile_feature_flag %}
|
{% if has_profile_feature_flag %}
|
||||||
<h2>How we’ll reach you</h2>
|
<h2>How we’ll reach you</h2>
|
||||||
<p>While reviewing your domain request, we may need to reach out with questions. We’ll also email you when we complete our review If the contact information below is not correct, visit <a href="{% url 'user-profile' %}?redirect=domain-request:" class="usa-link">your profile</a> to make updates.</p>
|
<p>While reviewing your domain request, we may need to reach out with questions. We’ll also email you when we complete our review. If the contact information below is not correct, visit <a href="{% url 'user-profile' %}?redirect=domain-request:" class="usa-link">your profile</a> to make updates.</p>
|
||||||
{% include "includes/profile_information.html" with user=user%}
|
{% include "includes/profile_information.html" with user=user%}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -77,11 +77,11 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<button type="submit" name="contact_setup_save_button" class="usa-button ">
|
<button type="submit" name="user_setup_save_button" class="usa-button ">
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
{% if user_finished_setup and going_to_specific_page %}
|
{% if user_finished_setup and going_to_specific_page %}
|
||||||
<button type="submit" name="contact_setup_submit_button" class="usa-button usa-button--outline">
|
<button type="submit" name="user_setup_submit_button" class="usa-button usa-button--outline">
|
||||||
{{redirect_button_text }}
|
{{redirect_button_text }}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -13,10 +13,10 @@
|
||||||
</h3>
|
</h3>
|
||||||
<div class="usa-summary-box__text">
|
<div class="usa-summary-box__text">
|
||||||
<ul>
|
<ul>
|
||||||
<li>Full name: <b>{{ user.contact.get_formatted_name }}</b></li>
|
<li>Full name: <b>{{ user.get_formatted_name }}</b></li>
|
||||||
<li>Organization email: <b>{{ user.email }}</b></li>
|
<li>Organization email: <b>{{ user.email }}</b></li>
|
||||||
<li>Title or role in your organization: <b>{{ user.contact.title }}</b></li>
|
<li>Title or role in your organization: <b>{{ user.title }}</b></li>
|
||||||
<li>Phone: <b>{{ user.contact.phone.as_national }}</b></li>
|
<li>Phone: <b>{{ user.phone.as_national }}</b></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -810,6 +810,8 @@ def create_superuser():
|
||||||
user = User.objects.create_user(
|
user = User.objects.create_user(
|
||||||
username="superuser",
|
username="superuser",
|
||||||
email="admin@example.com",
|
email="admin@example.com",
|
||||||
|
first_name="first",
|
||||||
|
last_name="last",
|
||||||
is_staff=True,
|
is_staff=True,
|
||||||
password=p,
|
password=p,
|
||||||
)
|
)
|
||||||
|
@ -826,6 +828,8 @@ def create_user():
|
||||||
user = User.objects.create_user(
|
user = User.objects.create_user(
|
||||||
username="staffuser",
|
username="staffuser",
|
||||||
email="staff@example.com",
|
email="staff@example.com",
|
||||||
|
first_name="first",
|
||||||
|
last_name="last",
|
||||||
is_staff=True,
|
is_staff=True,
|
||||||
password=p,
|
password=p,
|
||||||
)
|
)
|
||||||
|
@ -841,12 +845,10 @@ def create_test_user():
|
||||||
last_name = "Last"
|
last_name = "Last"
|
||||||
email = "info@example.com"
|
email = "info@example.com"
|
||||||
phone = "8003111234"
|
phone = "8003111234"
|
||||||
user = get_user_model().objects.create(
|
|
||||||
username=username, first_name=first_name, last_name=last_name, email=email, phone=phone
|
|
||||||
)
|
|
||||||
title = "test title"
|
title = "test title"
|
||||||
user.contact.title = title
|
user = get_user_model().objects.create(
|
||||||
user.contact.save()
|
username=username, first_name=first_name, last_name=last_name, email=email, phone=phone, title=title
|
||||||
|
)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def create_ready_domain():
|
def create_ready_domain():
|
||||||
|
|
|
@ -39,6 +39,7 @@ from registrar.models import (
|
||||||
UserGroup,
|
UserGroup,
|
||||||
TransitionDomain,
|
TransitionDomain,
|
||||||
)
|
)
|
||||||
|
from registrar.models.senior_official import SeniorOfficial
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
from registrar.models.verified_by_staff import VerifiedByStaff
|
from registrar.models.verified_by_staff import VerifiedByStaff
|
||||||
from .common import (
|
from .common import (
|
||||||
|
@ -347,6 +348,39 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
SeniorOfficial.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_domain_information_senior_official_is_alphabetically_sorted(self):
|
||||||
|
"""Tests if the senior offical dropdown is alphanetically sorted in the django admin display"""
|
||||||
|
|
||||||
|
SeniorOfficial.objects.get_or_create(first_name="mary", last_name="joe", title="some other guy")
|
||||||
|
SeniorOfficial.objects.get_or_create(first_name="alex", last_name="smoe", title="some guy")
|
||||||
|
SeniorOfficial.objects.get_or_create(first_name="Zoup", last_name="Soup", title="title")
|
||||||
|
|
||||||
|
contact, _ = Contact.objects.get_or_create(first_name="Henry", last_name="McFakerson")
|
||||||
|
domain_request = completed_domain_request(
|
||||||
|
submitter=contact, name="city1244.gov", status=DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||||
|
)
|
||||||
|
domain_request.approve()
|
||||||
|
|
||||||
|
domain_info = DomainInformation.objects.get(domain_request=domain_request)
|
||||||
|
request = self.factory.post("/admin/registrar/domaininformation/{}/change/".format(domain_info.pk))
|
||||||
|
model_admin = AuditedAdmin(DomainInformation, self.site)
|
||||||
|
|
||||||
|
# Get the queryset that would be returned for the list
|
||||||
|
senior_offical_queryset = model_admin.formfield_for_foreignkey(
|
||||||
|
DomainInformation.senior_official.field, request
|
||||||
|
).queryset
|
||||||
|
|
||||||
|
# Make the list we're comparing on a bit prettier display-wise. Optional step.
|
||||||
|
current_sort_order = []
|
||||||
|
for official in senior_offical_queryset:
|
||||||
|
current_sort_order.append(f"{official.first_name} {official.last_name}")
|
||||||
|
|
||||||
|
expected_sort_order = ["alex smoe", "mary joe", "Zoup Soup"]
|
||||||
|
|
||||||
|
self.assertEqual(current_sort_order, expected_sort_order)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_admin_can_see_cisa_region_federal(self):
|
def test_admin_can_see_cisa_region_federal(self):
|
||||||
|
@ -536,15 +570,11 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
username="MrMeoward",
|
username="MrMeoward",
|
||||||
first_name="Meoward",
|
first_name="Meoward",
|
||||||
last_name="Jones",
|
last_name="Jones",
|
||||||
|
email="meoward.jones@igorville.gov",
|
||||||
|
phone="(555) 123 12345",
|
||||||
|
title="Treat inspector",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Due to the relation between User <==> Contact,
|
|
||||||
# the underlying contact has to be modified this way.
|
|
||||||
_creator.contact.email = "meoward.jones@igorville.gov"
|
|
||||||
_creator.contact.phone = "(555) 123 12345"
|
|
||||||
_creator.contact.title = "Treat inspector"
|
|
||||||
_creator.contact.save()
|
|
||||||
|
|
||||||
# Create a fake domain request
|
# Create a fake domain request
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||||
domain_request.approve()
|
domain_request.approve()
|
||||||
|
@ -565,16 +595,16 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
|
|
||||||
# == Check for the creator == #
|
# == Check for the creator == #
|
||||||
|
|
||||||
# Check for the right title, email, and phone number in the response.
|
# Check for the right title and phone number in the response.
|
||||||
# We only need to check for the end tag
|
# We only need to check for the end tag
|
||||||
# (Otherwise this test will fail if we change classes, etc)
|
# (Otherwise this test will fail if we change classes, etc)
|
||||||
expected_creator_fields = [
|
expected_creator_fields = [
|
||||||
# Field, expected value
|
# Field, expected value
|
||||||
("title", "Treat inspector"),
|
("title", "Treat inspector"),
|
||||||
("email", "meoward.jones@igorville.gov"),
|
|
||||||
("phone", "(555) 123 12345"),
|
("phone", "(555) 123 12345"),
|
||||||
]
|
]
|
||||||
self.test_helper.assert_response_contains_distinct_values(response, expected_creator_fields)
|
self.test_helper.assert_response_contains_distinct_values(response, expected_creator_fields)
|
||||||
|
self.assertContains(response, "meoward.jones@igorville.gov")
|
||||||
|
|
||||||
# Check for the field itself
|
# Check for the field itself
|
||||||
self.assertContains(response, "Meoward Jones")
|
self.assertContains(response, "Meoward Jones")
|
||||||
|
@ -1150,6 +1180,7 @@ class AuditedAdminTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.client = Client(HTTP_HOST="localhost:8080")
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
self.staffuser = create_user()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
@ -1208,7 +1239,9 @@ class AuditedAdminTest(TestCase):
|
||||||
def test_alphabetically_sorted_fk_fields_domain_request(self):
|
def test_alphabetically_sorted_fk_fields_domain_request(self):
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
tested_fields = [
|
tested_fields = [
|
||||||
DomainRequest.senior_official.field,
|
# Senior offical is commented out for now - this is alphabetized
|
||||||
|
# and this test does not accurately reflect that.
|
||||||
|
# DomainRequest.senior_official.field,
|
||||||
DomainRequest.submitter.field,
|
DomainRequest.submitter.field,
|
||||||
# DomainRequest.investigator.field,
|
# DomainRequest.investigator.field,
|
||||||
DomainRequest.creator.field,
|
DomainRequest.creator.field,
|
||||||
|
@ -1266,7 +1299,9 @@ class AuditedAdminTest(TestCase):
|
||||||
def test_alphabetically_sorted_fk_fields_domain_information(self):
|
def test_alphabetically_sorted_fk_fields_domain_information(self):
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
tested_fields = [
|
tested_fields = [
|
||||||
DomainInformation.senior_official.field,
|
# Senior offical is commented out for now - this is alphabetized
|
||||||
|
# and this test does not accurately reflect that.
|
||||||
|
# DomainInformation.senior_official.field,
|
||||||
DomainInformation.submitter.field,
|
DomainInformation.submitter.field,
|
||||||
# DomainInformation.creator.field,
|
# DomainInformation.creator.field,
|
||||||
(DomainInformation.domain.field, ["name"]),
|
(DomainInformation.domain.field, ["name"]),
|
||||||
|
@ -1299,7 +1334,6 @@ class AuditedAdminTest(TestCase):
|
||||||
|
|
||||||
# Conforms to the same object structure as desired_order
|
# Conforms to the same object structure as desired_order
|
||||||
current_sort_order_coerced_type = []
|
current_sort_order_coerced_type = []
|
||||||
|
|
||||||
# This is necessary as .queryset and get_queryset
|
# This is necessary as .queryset and get_queryset
|
||||||
# return lists of different types/structures.
|
# return lists of different types/structures.
|
||||||
# We need to parse this data and coerce them into the same type.
|
# We need to parse this data and coerce them into the same type.
|
||||||
|
@ -1376,7 +1410,8 @@ class AuditedAdminTest(TestCase):
|
||||||
if last_name is None:
|
if last_name is None:
|
||||||
return (first_name,)
|
return (first_name,)
|
||||||
|
|
||||||
if first_name.split(queryset_shorthand)[1] == field_name:
|
split_name = first_name.split(queryset_shorthand)
|
||||||
|
if len(split_name) == 2 and split_name[1] == field_name:
|
||||||
return returned_tuple
|
return returned_tuple
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
@ -1561,7 +1596,7 @@ class TestContactAdmin(TestCase):
|
||||||
|
|
||||||
readonly_fields = self.admin.get_readonly_fields(request)
|
readonly_fields = self.admin.get_readonly_fields(request)
|
||||||
|
|
||||||
expected_fields = ["user", "email"]
|
expected_fields = ["email"]
|
||||||
|
|
||||||
self.assertEqual(readonly_fields, expected_fields)
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
|
||||||
|
@ -1577,15 +1612,18 @@ class TestContactAdmin(TestCase):
|
||||||
self.assertEqual(readonly_fields, expected_fields)
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
|
||||||
def test_change_view_for_joined_contact_five_or_less(self):
|
def test_change_view_for_joined_contact_five_or_less(self):
|
||||||
"""Create a contact, join it to 4 domain requests. The 5th join will be a user.
|
"""Create a contact, join it to 4 domain requests.
|
||||||
Assert that the warning on the contact form lists 5 joins."""
|
Assert that the warning on the contact form lists 4 joins."""
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
self.client.force_login(self.superuser)
|
self.client.force_login(self.superuser)
|
||||||
|
|
||||||
# Create an instance of the model
|
# Create an instance of the model
|
||||||
contact, _ = Contact.objects.get_or_create(user=self.staffuser)
|
contact, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Henry",
|
||||||
|
last_name="McFakerson",
|
||||||
|
)
|
||||||
|
|
||||||
# join it to 4 domain requests. The 5th join will be a user.
|
# join it to 4 domain requests.
|
||||||
domain_request1 = completed_domain_request(submitter=contact, name="city1.gov")
|
domain_request1 = completed_domain_request(submitter=contact, name="city1.gov")
|
||||||
domain_request2 = completed_domain_request(submitter=contact, name="city2.gov")
|
domain_request2 = completed_domain_request(submitter=contact, name="city2.gov")
|
||||||
domain_request3 = completed_domain_request(submitter=contact, name="city3.gov")
|
domain_request3 = completed_domain_request(submitter=contact, name="city3.gov")
|
||||||
|
@ -1608,8 +1646,6 @@ class TestContactAdmin(TestCase):
|
||||||
f"domainrequest/{domain_request3.pk}/change/'>city3.gov</a></li>"
|
f"domainrequest/{domain_request3.pk}/change/'>city3.gov</a></li>"
|
||||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
||||||
f"domainrequest/{domain_request4.pk}/change/'>city4.gov</a></li>"
|
f"domainrequest/{domain_request4.pk}/change/'>city4.gov</a></li>"
|
||||||
"<li>Joined to User: <a href='/admin/registrar/"
|
|
||||||
f"user/{self.staffuser.pk}/change/'>staff@example.com</a></li>"
|
|
||||||
"</ul>",
|
"</ul>",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1618,18 +1654,22 @@ class TestContactAdmin(TestCase):
|
||||||
contact.delete()
|
contact.delete()
|
||||||
|
|
||||||
def test_change_view_for_joined_contact_five_or_more(self):
|
def test_change_view_for_joined_contact_five_or_more(self):
|
||||||
"""Create a contact, join it to 5 domain requests. The 6th join will be a user.
|
"""Create a contact, join it to 6 domain requests.
|
||||||
Assert that the warning on the contact form lists 5 joins and a '1 more' ellispsis."""
|
Assert that the warning on the contact form lists 5 joins and a '1 more' ellispsis."""
|
||||||
with less_console_noise():
|
with less_console_noise():
|
||||||
self.client.force_login(self.superuser)
|
self.client.force_login(self.superuser)
|
||||||
# Create an instance of the model
|
# Create an instance of the model
|
||||||
# join it to 5 domain requests. The 6th join will be a user.
|
# join it to 6 domain requests.
|
||||||
contact, _ = Contact.objects.get_or_create(user=self.staffuser)
|
contact, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Henry",
|
||||||
|
last_name="McFakerson",
|
||||||
|
)
|
||||||
domain_request1 = completed_domain_request(submitter=contact, name="city1.gov")
|
domain_request1 = completed_domain_request(submitter=contact, name="city1.gov")
|
||||||
domain_request2 = completed_domain_request(submitter=contact, name="city2.gov")
|
domain_request2 = completed_domain_request(submitter=contact, name="city2.gov")
|
||||||
domain_request3 = completed_domain_request(submitter=contact, name="city3.gov")
|
domain_request3 = completed_domain_request(submitter=contact, name="city3.gov")
|
||||||
domain_request4 = completed_domain_request(submitter=contact, name="city4.gov")
|
domain_request4 = completed_domain_request(submitter=contact, name="city4.gov")
|
||||||
domain_request5 = completed_domain_request(submitter=contact, name="city5.gov")
|
domain_request5 = completed_domain_request(submitter=contact, name="city5.gov")
|
||||||
|
completed_domain_request(submitter=contact, name="city6.gov")
|
||||||
with patch("django.contrib.messages.warning") as mock_warning:
|
with patch("django.contrib.messages.warning") as mock_warning:
|
||||||
# Use the test client to simulate the request
|
# Use the test client to simulate the request
|
||||||
response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk]))
|
response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk]))
|
||||||
|
|
|
@ -418,15 +418,11 @@ class TestDomainAdminWClient(TestCase):
|
||||||
username="MrMeoward",
|
username="MrMeoward",
|
||||||
first_name="Meoward",
|
first_name="Meoward",
|
||||||
last_name="Jones",
|
last_name="Jones",
|
||||||
|
email="meoward.jones@igorville.gov",
|
||||||
|
phone="(555) 123 12345",
|
||||||
|
title="Treat inspector",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Due to the relation between User <==> Contact,
|
|
||||||
# the underlying contact has to be modified this way.
|
|
||||||
_creator.contact.email = "meoward.jones@igorville.gov"
|
|
||||||
_creator.contact.phone = "(555) 123 12345"
|
|
||||||
_creator.contact.title = "Treat inspector"
|
|
||||||
_creator.contact.save()
|
|
||||||
|
|
||||||
# Create a fake domain request
|
# Create a fake domain request
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||||
domain_request.approve()
|
domain_request.approve()
|
||||||
|
|
|
@ -64,8 +64,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_has_model_description(self):
|
def test_has_model_description(self):
|
||||||
"""Tests if this model has a model description on the table view"""
|
"""Tests if this model has a model description on the table view"""
|
||||||
p = "adminpass"
|
self.client.force_login(self.superuser)
|
||||||
self.client.login(username="superuser", password=p)
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
"/admin/registrar/domainrequest/",
|
"/admin/registrar/domainrequest/",
|
||||||
follow=True,
|
follow=True,
|
||||||
|
@ -87,8 +86,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
# Create a fake domain request and domain
|
# Create a fake domain request and domain
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||||
|
|
||||||
p = "adminpass"
|
self.client.force_login(self.superuser)
|
||||||
self.client.login(username="superuser", password=p)
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
||||||
follow=True,
|
follow=True,
|
||||||
|
@ -135,8 +133,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
# Create a fake domain request and domain
|
# Create a fake domain request and domain
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.STARTED)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.STARTED)
|
||||||
|
|
||||||
p = "adminpass"
|
self.client.force_login(self.superuser)
|
||||||
self.client.login(username="superuser", password=p)
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
||||||
follow=True,
|
follow=True,
|
||||||
|
@ -204,8 +201,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
# Create a fake domain request and domain
|
# Create a fake domain request and domain
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||||
|
|
||||||
p = "adminpass"
|
self.client.force_login(self.superuser)
|
||||||
self.client.login(username="superuser", password=p)
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
||||||
follow=True,
|
follow=True,
|
||||||
|
@ -234,8 +230,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
_domain_request.alternative_domains.add(fake_website)
|
_domain_request.alternative_domains.add(fake_website)
|
||||||
_domain_request.save()
|
_domain_request.save()
|
||||||
|
|
||||||
p = "userpass"
|
self.client.force_login(self.staffuser)
|
||||||
self.client.login(username="staffuser", password=p)
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
|
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
|
||||||
follow=True,
|
follow=True,
|
||||||
|
@ -281,8 +276,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
# Create a fake domain request
|
# Create a fake domain request
|
||||||
_domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
_domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||||
|
|
||||||
p = "userpass"
|
self.client.force_login(self.staffuser)
|
||||||
self.client.login(username="staffuser", password=p)
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
|
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
|
||||||
follow=True,
|
follow=True,
|
||||||
|
@ -332,8 +326,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
_domain_request.current_websites.add(fake_website)
|
_domain_request.current_websites.add(fake_website)
|
||||||
_domain_request.save()
|
_domain_request.save()
|
||||||
|
|
||||||
p = "userpass"
|
self.client.force_login(self.staffuser)
|
||||||
self.client.login(username="staffuser", password=p)
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
|
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
|
||||||
follow=True,
|
follow=True,
|
||||||
|
@ -354,8 +347,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_domain_sortable(self):
|
def test_domain_sortable(self):
|
||||||
"""Tests if the DomainRequest sorts by domain correctly"""
|
"""Tests if the DomainRequest sorts by domain correctly"""
|
||||||
p = "adminpass"
|
self.client.force_login(self.superuser)
|
||||||
self.client.login(username="superuser", password=p)
|
|
||||||
|
|
||||||
multiple_unalphabetical_domain_objects("domain_request")
|
multiple_unalphabetical_domain_objects("domain_request")
|
||||||
|
|
||||||
|
@ -368,8 +360,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_submitter_sortable(self):
|
def test_submitter_sortable(self):
|
||||||
"""Tests if the DomainRequest sorts by submitter correctly"""
|
"""Tests if the DomainRequest sorts by submitter correctly"""
|
||||||
p = "adminpass"
|
self.client.force_login(self.superuser)
|
||||||
self.client.login(username="superuser", password=p)
|
|
||||||
|
|
||||||
multiple_unalphabetical_domain_objects("domain_request")
|
multiple_unalphabetical_domain_objects("domain_request")
|
||||||
|
|
||||||
|
@ -402,8 +393,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_investigator_sortable(self):
|
def test_investigator_sortable(self):
|
||||||
"""Tests if the DomainRequest sorts by investigator correctly"""
|
"""Tests if the DomainRequest sorts by investigator correctly"""
|
||||||
p = "adminpass"
|
self.client.force_login(self.superuser)
|
||||||
self.client.login(username="superuser", password=p)
|
|
||||||
|
|
||||||
multiple_unalphabetical_domain_objects("domain_request")
|
multiple_unalphabetical_domain_objects("domain_request")
|
||||||
additional_domain_request = generic_domain_object("domain_request", "Xylophone")
|
additional_domain_request = generic_domain_object("domain_request", "Xylophone")
|
||||||
|
@ -693,8 +683,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
action_needed_reason=DomainRequest.ActionNeededReasons.BAD_NAME,
|
action_needed_reason=DomainRequest.ActionNeededReasons.BAD_NAME,
|
||||||
)
|
)
|
||||||
|
|
||||||
p = "userpass"
|
self.client.force_login(self.staffuser)
|
||||||
self.client.login(username="staffuser", password=p)
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
|
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
|
||||||
follow=True,
|
follow=True,
|
||||||
|
@ -1165,8 +1154,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
# Get the other contact
|
# Get the other contact
|
||||||
other_contact = domain_request.other_contacts.all().first()
|
other_contact = domain_request.other_contacts.all().first()
|
||||||
|
|
||||||
p = "userpass"
|
self.client.force_login(self.staffuser)
|
||||||
self.client.login(username="staffuser", password=p)
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
||||||
follow=True,
|
follow=True,
|
||||||
|
@ -1193,8 +1181,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
# Create a fake domain request
|
# Create a fake domain request
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||||
|
|
||||||
p = "userpass"
|
self.client.force_login(self.staffuser)
|
||||||
self.client.login(username="staffuser", password=p)
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
||||||
follow=True,
|
follow=True,
|
||||||
|
@ -1217,20 +1204,15 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
username="MrMeoward",
|
username="MrMeoward",
|
||||||
first_name="Meoward",
|
first_name="Meoward",
|
||||||
last_name="Jones",
|
last_name="Jones",
|
||||||
|
email="meoward.jones@igorville.gov",
|
||||||
|
phone="(555) 123 12345",
|
||||||
|
title = "Treat inspector"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Due to the relation between User <==> Contact,
|
|
||||||
# the underlying contact has to be modified this way.
|
|
||||||
_creator.contact.email = "meoward.jones@igorville.gov"
|
|
||||||
_creator.contact.phone = "(555) 123 12345"
|
|
||||||
_creator.contact.title = "Treat inspector"
|
|
||||||
_creator.contact.save()
|
|
||||||
|
|
||||||
# Create a fake domain request
|
# Create a fake domain request
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||||
|
|
||||||
p = "userpass"
|
self.client.force_login(self.staffuser)
|
||||||
self.client.login(username="staffuser", password=p)
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
||||||
follow=True,
|
follow=True,
|
||||||
|
@ -1246,10 +1228,10 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
expected_creator_fields = [
|
expected_creator_fields = [
|
||||||
# Field, expected value
|
# Field, expected value
|
||||||
("title", "Treat inspector"),
|
("title", "Treat inspector"),
|
||||||
("email", "meoward.jones@igorville.gov"),
|
|
||||||
("phone", "(555) 123 12345"),
|
("phone", "(555) 123 12345"),
|
||||||
]
|
]
|
||||||
self.test_helper.assert_response_contains_distinct_values(response, expected_creator_fields)
|
self.test_helper.assert_response_contains_distinct_values(response, expected_creator_fields)
|
||||||
|
self.assertContains(response, "meoward.jones@igorville.gov")
|
||||||
|
|
||||||
# Check for the field itself
|
# Check for the field itself
|
||||||
self.assertContains(response, "Meoward Jones")
|
self.assertContains(response, "Meoward Jones")
|
||||||
|
@ -1328,8 +1310,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
# Create a sample domain request
|
# Create a sample domain request
|
||||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||||
|
|
||||||
p = "userpass"
|
self.client.force_login(self.staffuser)
|
||||||
self.client.login(username="staffuser", password=p)
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
||||||
follow=True,
|
follow=True,
|
||||||
|
@ -1394,6 +1375,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"alternative_domains",
|
"alternative_domains",
|
||||||
"is_election_board",
|
"is_election_board",
|
||||||
"federal_agency",
|
"federal_agency",
|
||||||
|
"status_history",
|
||||||
|
"action_needed_reason_email",
|
||||||
"id",
|
"id",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
@ -1454,6 +1437,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"alternative_domains",
|
"alternative_domains",
|
||||||
"is_election_board",
|
"is_election_board",
|
||||||
"federal_agency",
|
"federal_agency",
|
||||||
|
"status_history",
|
||||||
|
"action_needed_reason_email",
|
||||||
"creator",
|
"creator",
|
||||||
"about_your_organization",
|
"about_your_organization",
|
||||||
"requested_domain",
|
"requested_domain",
|
||||||
|
@ -1484,6 +1469,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
"alternative_domains",
|
"alternative_domains",
|
||||||
"is_election_board",
|
"is_election_board",
|
||||||
"federal_agency",
|
"federal_agency",
|
||||||
|
"status_history",
|
||||||
|
"action_needed_reason_email",
|
||||||
]
|
]
|
||||||
|
|
||||||
self.assertEqual(readonly_fields, expected_fields)
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
@ -1694,8 +1681,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
investigator_user.is_staff = True
|
investigator_user.is_staff = True
|
||||||
investigator_user.save()
|
investigator_user.save()
|
||||||
|
|
||||||
p = "userpass"
|
self.client.force_login(self.staffuser)
|
||||||
self.client.login(username="staffuser", password=p)
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
"/admin/registrar/domainrequest/",
|
"/admin/registrar/domainrequest/",
|
||||||
{
|
{
|
||||||
|
@ -1745,8 +1731,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
investigator_user_2.is_staff = False
|
investigator_user_2.is_staff = False
|
||||||
investigator_user_2.save()
|
investigator_user_2.save()
|
||||||
|
|
||||||
p = "userpass"
|
self.client.force_login(self.staffuser)
|
||||||
self.client.login(username="staffuser", password=p)
|
|
||||||
|
|
||||||
request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk))
|
request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk))
|
||||||
|
|
||||||
|
@ -1795,8 +1780,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
investigator_user_3.is_staff = True
|
investigator_user_3.is_staff = True
|
||||||
investigator_user_3.save()
|
investigator_user_3.save()
|
||||||
|
|
||||||
p = "userpass"
|
self.client.force_login(self.staffuser)
|
||||||
self.client.login(username="staffuser", password=p)
|
|
||||||
request = RequestFactory().get("/")
|
request = RequestFactory().get("/")
|
||||||
|
|
||||||
# These names have metadata embedded in them. :investigator implicitly tests if
|
# These names have metadata embedded in them. :investigator implicitly tests if
|
||||||
|
@ -1819,8 +1803,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
# Create a fake domain request
|
# Create a fake domain request
|
||||||
_domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
_domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||||
|
|
||||||
p = "userpass"
|
self.client.force_login(self.staffuser)
|
||||||
self.client.login(username="staffuser", password=p)
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
|
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
|
||||||
follow=True,
|
follow=True,
|
||||||
|
@ -1850,8 +1833,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW, generic_org_type="interstate"
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW, generic_org_type="interstate"
|
||||||
)
|
)
|
||||||
|
|
||||||
p = "userpass"
|
self.client.force_login(self.staffuser)
|
||||||
self.client.login(username="staffuser", password=p)
|
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
|
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
|
||||||
follow=True,
|
follow=True,
|
||||||
|
|
|
@ -1,124 +0,0 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
from registrar.models import (
|
|
||||||
User,
|
|
||||||
Contact,
|
|
||||||
)
|
|
||||||
|
|
||||||
from registrar.management.commands.copy_names_from_contacts_to_users import Command
|
|
||||||
|
|
||||||
|
|
||||||
class TestDataUpdates(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
"""We cannot setup the user details because contacts will override the first and last names in its save method
|
|
||||||
so we will initiate the users, setup the contacts and link them, and leave the rest of the setup to the test(s).
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.user1 = User.objects.create(username="user1")
|
|
||||||
self.user2 = User.objects.create(username="user2")
|
|
||||||
self.user3 = User.objects.create(username="user3")
|
|
||||||
self.user4 = User.objects.create(username="user4")
|
|
||||||
# The last user created triggers the creation of a contact and attaches itself to it. See signals.
|
|
||||||
# This bs_user defuses that situation.
|
|
||||||
self.bs_user = User.objects.create()
|
|
||||||
|
|
||||||
self.contact1 = Contact.objects.create(
|
|
||||||
user=self.user1,
|
|
||||||
email="email1@igorville.gov",
|
|
||||||
first_name="first1",
|
|
||||||
last_name="last1",
|
|
||||||
middle_name="middle1",
|
|
||||||
title="title1",
|
|
||||||
)
|
|
||||||
self.contact2 = Contact.objects.create(
|
|
||||||
user=self.user2,
|
|
||||||
email="email2@igorville.gov",
|
|
||||||
first_name="first2",
|
|
||||||
last_name="last2",
|
|
||||||
middle_name="middle2",
|
|
||||||
title="title2",
|
|
||||||
)
|
|
||||||
self.contact3 = Contact.objects.create(
|
|
||||||
user=self.user3,
|
|
||||||
email="email3@igorville.gov",
|
|
||||||
first_name="first3",
|
|
||||||
last_name="last3",
|
|
||||||
middle_name="middle3",
|
|
||||||
title="title3",
|
|
||||||
)
|
|
||||||
self.contact4 = Contact.objects.create(
|
|
||||||
email="email4@igorville.gov", first_name="first4", last_name="last4", middle_name="middle4", title="title4"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.command = Command()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
"""Clean up"""
|
|
||||||
# Delete users and contacts
|
|
||||||
User.objects.all().delete()
|
|
||||||
Contact.objects.all().delete()
|
|
||||||
|
|
||||||
def test_script_updates_linked_users(self):
|
|
||||||
"""Test the script that copies contact information to the user object"""
|
|
||||||
|
|
||||||
# Set up the users' first and last names here so
|
|
||||||
# they that they don't get overwritten by Contact's save()
|
|
||||||
# User with no first or last names
|
|
||||||
self.user1.first_name = ""
|
|
||||||
self.user1.last_name = ""
|
|
||||||
self.user1.title = "dummytitle"
|
|
||||||
self.user1.middle_name = "dummymiddle"
|
|
||||||
self.user1.save()
|
|
||||||
|
|
||||||
# User with a first name but no last name
|
|
||||||
self.user2.first_name = "First name but no last name"
|
|
||||||
self.user2.last_name = ""
|
|
||||||
self.user2.save()
|
|
||||||
|
|
||||||
# User with a first and last name
|
|
||||||
self.user3.first_name = "An existing first name"
|
|
||||||
self.user3.last_name = "An existing last name"
|
|
||||||
self.user3.save()
|
|
||||||
|
|
||||||
# Call the parent method the same way we do it in the script
|
|
||||||
skipped_contacts = []
|
|
||||||
eligible_users = []
|
|
||||||
processed_users = []
|
|
||||||
(
|
|
||||||
skipped_contacts,
|
|
||||||
eligible_users,
|
|
||||||
processed_users,
|
|
||||||
) = self.command.process_contacts(
|
|
||||||
# Set debugging to False
|
|
||||||
False,
|
|
||||||
skipped_contacts,
|
|
||||||
eligible_users,
|
|
||||||
processed_users,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Trigger DB refresh
|
|
||||||
self.user1.refresh_from_db()
|
|
||||||
self.user2.refresh_from_db()
|
|
||||||
self.user3.refresh_from_db()
|
|
||||||
|
|
||||||
# Asserts
|
|
||||||
# The user that has no first and last names will get them from the contact
|
|
||||||
self.assertEqual(self.user1.first_name, "first1")
|
|
||||||
self.assertEqual(self.user1.last_name, "last1")
|
|
||||||
self.assertEqual(self.user1.middle_name, "middle1")
|
|
||||||
self.assertEqual(self.user1.title, "title1")
|
|
||||||
# The user that has a first but no last will be updated
|
|
||||||
self.assertEqual(self.user2.first_name, "first2")
|
|
||||||
self.assertEqual(self.user2.last_name, "last2")
|
|
||||||
self.assertEqual(self.user2.middle_name, "middle2")
|
|
||||||
self.assertEqual(self.user2.title, "title2")
|
|
||||||
# The user that has a first and a last will be updated
|
|
||||||
self.assertEqual(self.user3.first_name, "first3")
|
|
||||||
self.assertEqual(self.user3.last_name, "last3")
|
|
||||||
self.assertEqual(self.user3.middle_name, "middle3")
|
|
||||||
self.assertEqual(self.user3.title, "title3")
|
|
||||||
# The unlinked user will be left alone
|
|
||||||
self.assertEqual(self.user4.first_name, "")
|
|
||||||
self.assertEqual(self.user4.last_name, "")
|
|
||||||
self.assertEqual(self.user4.middle_name, None)
|
|
||||||
self.assertEqual(self.user4.title, None)
|
|
|
@ -1088,11 +1088,6 @@ class TestImportTables(TestCase):
|
||||||
for table_name in table_names:
|
for table_name in table_names:
|
||||||
mock_path_exists.assert_any_call(f"{table_name}_1.csv")
|
mock_path_exists.assert_any_call(f"{table_name}_1.csv")
|
||||||
|
|
||||||
# Check that clean_tables is called for Contact
|
|
||||||
mock_get_model.assert_any_call("registrar", "Contact")
|
|
||||||
model_mock = mock_get_model.return_value
|
|
||||||
model_mock.objects.all().delete.assert_called()
|
|
||||||
|
|
||||||
# Check that logger.info was called for each successful import
|
# Check that logger.info was called for each successful import
|
||||||
for table_name in table_names:
|
for table_name in table_names:
|
||||||
mock_logger.info.assert_any_call(f"Successfully imported {table_name}_1.csv into {table_name}")
|
mock_logger.info.assert_any_call(f"Successfully imported {table_name}_1.csv into {table_name}")
|
||||||
|
|
|
@ -1267,30 +1267,23 @@ class TestUser(TestCase):
|
||||||
# test with a user with contact info defined
|
# test with a user with contact info defined
|
||||||
self.assertTrue(self.user.has_contact_info())
|
self.assertTrue(self.user.has_contact_info())
|
||||||
# test with a user without contact info defined
|
# test with a user without contact info defined
|
||||||
self.user.contact.title = None
|
self.user.title = None
|
||||||
self.user.contact.email = None
|
self.user.email = None
|
||||||
self.user.contact.phone = None
|
self.user.phone = None
|
||||||
self.assertFalse(self.user.has_contact_info())
|
self.assertFalse(self.user.has_contact_info())
|
||||||
|
|
||||||
|
|
||||||
class TestContact(TestCase):
|
class TestContact(TestCase):
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.email_for_invalid = "intern@igorville.gov"
|
|
||||||
self.invalid_user, _ = User.objects.get_or_create(
|
|
||||||
username=self.email_for_invalid,
|
|
||||||
email=self.email_for_invalid,
|
|
||||||
first_name="",
|
|
||||||
last_name="",
|
|
||||||
phone="",
|
|
||||||
)
|
|
||||||
self.invalid_contact, _ = Contact.objects.get_or_create(user=self.invalid_user)
|
|
||||||
|
|
||||||
self.email = "mayor@igorville.gov"
|
self.email = "mayor@igorville.gov"
|
||||||
self.user, _ = User.objects.get_or_create(
|
self.user, _ = User.objects.get_or_create(
|
||||||
email=self.email, first_name="Jeff", last_name="Lebowski", phone="123456789"
|
email=self.email, first_name="Jeff", last_name="Lebowski", phone="123456789"
|
||||||
)
|
)
|
||||||
self.contact, _ = Contact.objects.get_or_create(user=self.user)
|
self.contact, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Jeff",
|
||||||
|
last_name="Lebowski",
|
||||||
|
)
|
||||||
|
|
||||||
self.contact_as_so, _ = Contact.objects.get_or_create(email="newguy@igorville.gov")
|
self.contact_as_so, _ = Contact.objects.get_or_create(email="newguy@igorville.gov")
|
||||||
self.domain_request = DomainRequest.objects.create(creator=self.user, senior_official=self.contact_as_so)
|
self.domain_request = DomainRequest.objects.create(creator=self.user, senior_official=self.contact_as_so)
|
||||||
|
@ -1301,97 +1294,8 @@ class TestContact(TestCase):
|
||||||
Contact.objects.all().delete()
|
Contact.objects.all().delete()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def test_saving_contact_updates_user_first_last_names_and_phone(self):
|
|
||||||
"""When a contact is updated, we propagate the changes to the linked user if it exists."""
|
|
||||||
|
|
||||||
# User and Contact are created and linked as expected.
|
|
||||||
# An empty User object should create an empty contact.
|
|
||||||
self.assertEqual(self.invalid_contact.first_name, "")
|
|
||||||
self.assertEqual(self.invalid_contact.last_name, "")
|
|
||||||
self.assertEqual(self.invalid_contact.phone, "")
|
|
||||||
self.assertEqual(self.invalid_user.first_name, "")
|
|
||||||
self.assertEqual(self.invalid_user.last_name, "")
|
|
||||||
self.assertEqual(self.invalid_user.phone, "")
|
|
||||||
|
|
||||||
# Manually update the contact - mimicking production (pre-existing data)
|
|
||||||
self.invalid_contact.first_name = "Joey"
|
|
||||||
self.invalid_contact.last_name = "Baloney"
|
|
||||||
self.invalid_contact.phone = "123456789"
|
|
||||||
self.invalid_contact.save()
|
|
||||||
|
|
||||||
# Refresh the user object to reflect the changes made in the database
|
|
||||||
self.invalid_user.refresh_from_db()
|
|
||||||
|
|
||||||
# Updating the contact's first and last names propagate to the user
|
|
||||||
self.assertEqual(self.invalid_contact.first_name, "Joey")
|
|
||||||
self.assertEqual(self.invalid_contact.last_name, "Baloney")
|
|
||||||
self.assertEqual(self.invalid_contact.phone, "123456789")
|
|
||||||
self.assertEqual(self.invalid_user.first_name, "Joey")
|
|
||||||
self.assertEqual(self.invalid_user.last_name, "Baloney")
|
|
||||||
self.assertEqual(self.invalid_user.phone, "123456789")
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def test_saving_contact_does_not_update_user_first_last_names_and_phone(self):
|
|
||||||
"""When a contact is updated, we avoid propagating the changes to the linked user if it already has a value"""
|
|
||||||
|
|
||||||
# User and Contact are created and linked as expected
|
|
||||||
self.assertEqual(self.contact.first_name, "Jeff")
|
|
||||||
self.assertEqual(self.contact.last_name, "Lebowski")
|
|
||||||
self.assertEqual(self.contact.phone, "123456789")
|
|
||||||
self.assertEqual(self.user.first_name, "Jeff")
|
|
||||||
self.assertEqual(self.user.last_name, "Lebowski")
|
|
||||||
self.assertEqual(self.user.phone, "123456789")
|
|
||||||
|
|
||||||
self.contact.first_name = "Joey"
|
|
||||||
self.contact.last_name = "Baloney"
|
|
||||||
self.contact.phone = "987654321"
|
|
||||||
self.contact.save()
|
|
||||||
|
|
||||||
# Refresh the user object to reflect the changes made in the database
|
|
||||||
self.user.refresh_from_db()
|
|
||||||
|
|
||||||
# Updating the contact's first and last names propagate to the user
|
|
||||||
self.assertEqual(self.contact.first_name, "Joey")
|
|
||||||
self.assertEqual(self.contact.last_name, "Baloney")
|
|
||||||
self.assertEqual(self.contact.phone, "987654321")
|
|
||||||
self.assertEqual(self.user.first_name, "Jeff")
|
|
||||||
self.assertEqual(self.user.last_name, "Lebowski")
|
|
||||||
self.assertEqual(self.user.phone, "123456789")
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def test_saving_contact_does_not_update_user_email(self):
|
|
||||||
"""When a contact's email is updated, the change is not propagated to the user."""
|
|
||||||
self.contact.email = "joey.baloney@diaperville.com"
|
|
||||||
self.contact.save()
|
|
||||||
|
|
||||||
# Refresh the user object to reflect the changes made in the database
|
|
||||||
self.user.refresh_from_db()
|
|
||||||
|
|
||||||
# Updating the contact's email does not propagate
|
|
||||||
self.assertEqual(self.contact.email, "joey.baloney@diaperville.com")
|
|
||||||
self.assertEqual(self.user.email, "mayor@igorville.gov")
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def test_saving_contact_does_not_update_user_email_when_none(self):
|
|
||||||
"""When a contact's email is updated, and the first/last name is none,
|
|
||||||
the change is not propagated to the user."""
|
|
||||||
self.invalid_contact.email = "joey.baloney@diaperville.com"
|
|
||||||
self.invalid_contact.save()
|
|
||||||
|
|
||||||
# Refresh the user object to reflect the changes made in the database
|
|
||||||
self.invalid_user.refresh_from_db()
|
|
||||||
|
|
||||||
# Updating the contact's email does not propagate
|
|
||||||
self.assertEqual(self.invalid_contact.email, "joey.baloney@diaperville.com")
|
|
||||||
self.assertEqual(self.invalid_user.email, "intern@igorville.gov")
|
|
||||||
|
|
||||||
@less_console_noise_decorator
|
|
||||||
def test_has_more_than_one_join(self):
|
def test_has_more_than_one_join(self):
|
||||||
"""Test the Contact model method, has_more_than_one_join"""
|
"""Test the Contact model method, has_more_than_one_join"""
|
||||||
# test for a contact which has one user defined
|
|
||||||
self.assertFalse(self.contact.has_more_than_one_join("user"))
|
|
||||||
self.assertTrue(self.contact.has_more_than_one_join("senior_official"))
|
|
||||||
# test for a contact which is assigned as a senior official on a domain request
|
# test for a contact which is assigned as a senior official on a domain request
|
||||||
self.assertFalse(self.contact_as_so.has_more_than_one_join("senior_official"))
|
self.assertFalse(self.contact_as_so.has_more_than_one_join("senior_official"))
|
||||||
self.assertTrue(self.contact_as_so.has_more_than_one_join("submitted_domain_requests"))
|
self.assertTrue(self.contact_as_so.has_more_than_one_join("submitted_domain_requests"))
|
||||||
|
@ -1399,6 +1303,7 @@ class TestContact(TestCase):
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_has_contact_info(self):
|
def test_has_contact_info(self):
|
||||||
"""Test that has_contact_info properly returns"""
|
"""Test that has_contact_info properly returns"""
|
||||||
|
self.contact.title = "Title"
|
||||||
# test with a contact with contact info defined
|
# test with a contact with contact info defined
|
||||||
self.assertTrue(self.contact.has_contact_info())
|
self.assertTrue(self.contact.has_contact_info())
|
||||||
# test with a contact without contact info defined
|
# test with a contact without contact info defined
|
||||||
|
|
|
@ -1,21 +1,23 @@
|
||||||
import csv
|
|
||||||
import io
|
import io
|
||||||
from django.test import Client, RequestFactory
|
from django.test import Client, RequestFactory
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from registrar.models.domain_request import DomainRequest
|
from registrar.models.domain_request import DomainRequest
|
||||||
from registrar.models.domain import Domain
|
from registrar.models.domain import Domain
|
||||||
from registrar.models.utility.generic_helper import convert_queryset_to_dict
|
|
||||||
from registrar.utility.csv_export import (
|
from registrar.utility.csv_export import (
|
||||||
export_data_managed_domains_to_csv,
|
DomainDataFull,
|
||||||
export_data_unmanaged_domains_to_csv,
|
DomainDataType,
|
||||||
get_sliced_domains,
|
DomainDataFederal,
|
||||||
get_sliced_requests,
|
DomainGrowth,
|
||||||
write_csv_for_domains,
|
DomainManaged,
|
||||||
|
DomainUnmanaged,
|
||||||
|
DomainExport,
|
||||||
|
DomainRequestExport,
|
||||||
|
DomainRequestGrowth,
|
||||||
|
DomainRequestDataFull,
|
||||||
get_default_start_date,
|
get_default_start_date,
|
||||||
get_default_end_date,
|
get_default_end_date,
|
||||||
DomainRequestExport,
|
|
||||||
)
|
)
|
||||||
|
from django.db.models import Case, When
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from unittest.mock import MagicMock, call, mock_open, patch
|
from unittest.mock import MagicMock, call, mock_open, patch
|
||||||
from api.views import get_current_federal, get_current_full
|
from api.views import get_current_federal, get_current_full
|
||||||
|
@ -46,10 +48,10 @@ class CsvReportsTest(MockDb):
|
||||||
fake_open = mock_open()
|
fake_open = mock_open()
|
||||||
expected_file_content = [
|
expected_file_content = [
|
||||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||||
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,\r\n"),
|
||||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\r\n"),
|
||||||
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,\r\n"),
|
||||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,\r\n"),
|
||||||
]
|
]
|
||||||
# We don't actually want to write anything for a test case,
|
# We don't actually want to write anything for a test case,
|
||||||
# we just want to verify what is being written.
|
# we just want to verify what is being written.
|
||||||
|
@ -68,11 +70,12 @@ class CsvReportsTest(MockDb):
|
||||||
fake_open = mock_open()
|
fake_open = mock_open()
|
||||||
expected_file_content = [
|
expected_file_content = [
|
||||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||||
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
call("cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,\r\n"),
|
||||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\r\n"),
|
||||||
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
call("adomain10.gov,Federal,Armed Forces Retirement Home,,,,\r\n"),
|
||||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,,\r\n"),
|
||||||
call("adomain2.gov,Interstate,,,,, \r\n"),
|
call("adomain2.gov,Interstate,,,,,\r\n"),
|
||||||
|
call("zdomain12.gov,Interstate,,,,,\r\n"),
|
||||||
]
|
]
|
||||||
# We don't actually want to write anything for a test case,
|
# We don't actually want to write anything for a test case,
|
||||||
# we just want to verify what is being written.
|
# we just want to verify what is being written.
|
||||||
|
@ -200,494 +203,299 @@ class ExportDataTest(MockDb, MockEppLib):
|
||||||
"""Test the ExportData class from csv_export.
|
"""Test the ExportData class from csv_export.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def test_export_domains_to_writer_security_emails_and_first_ready(self):
|
@less_console_noise_decorator
|
||||||
"""Test that export_domains_to_writer returns the
|
def test_domain_data_type(self):
|
||||||
expected security email and first_ready value"""
|
"""Shows security contacts, domain managers, so"""
|
||||||
|
# Add security email information
|
||||||
|
self.domain_1.name = "defaultsecurity.gov"
|
||||||
|
self.domain_1.save()
|
||||||
|
# Invoke setter
|
||||||
|
self.domain_1.security_contact
|
||||||
|
# Invoke setter
|
||||||
|
self.domain_2.security_contact
|
||||||
|
# Invoke setter
|
||||||
|
self.domain_3.security_contact
|
||||||
|
# Add a first ready date on the first domain. Leaving the others blank.
|
||||||
|
self.domain_1.first_ready = get_default_start_date()
|
||||||
|
self.domain_1.save()
|
||||||
|
# Create a CSV file in memory
|
||||||
|
csv_file = StringIO()
|
||||||
|
# Call the export functions
|
||||||
|
DomainDataType.export_data_to_csv(csv_file)
|
||||||
|
# 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,First ready on,Expiration date,Domain type,Agency,Organization name,City,State,SO,"
|
||||||
|
"SO email,Security contact email,Domain managers,Invited domain managers\n"
|
||||||
|
"cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,World War I Centennial Commission,,,, ,,,"
|
||||||
|
"meoward@rocks.com,\n"
|
||||||
|
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,World War I Centennial Commission,,,"
|
||||||
|
', ,,dotgov@cisa.dhs.gov,"meoward@rocks.com, info@example.com, big_lebowski@dude.co",'
|
||||||
|
"woofwardthethird@rocks.com\n"
|
||||||
|
"adomain10.gov,Ready,2024-04-03,(blank),Federal,Armed Forces Retirement Home,,,, ,,,,"
|
||||||
|
"squeaker@rocks.com\n"
|
||||||
|
"bdomain4.gov,Unknown,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,,,\n"
|
||||||
|
"bdomain5.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,,,\n"
|
||||||
|
"bdomain6.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,,,\n"
|
||||||
|
"ddomain3.gov,On hold,(blank),2023-11-15,Federal,Armed Forces Retirement Home,,,, ,,"
|
||||||
|
"security@mail.gov,,\n"
|
||||||
|
"sdomain8.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,,,\n"
|
||||||
|
"xdomain7.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,,,\n"
|
||||||
|
"zdomain9.gov,Deleted,(blank),(blank),Federal,Armed Forces Retirement Home,,,, ,,,,\n"
|
||||||
|
"adomain2.gov,Dns needed,(blank),(blank),Interstate,,,,, ,,registrar@dotgov.gov,"
|
||||||
|
"meoward@rocks.com,squeaker@rocks.com\n"
|
||||||
|
"zdomain12.gov,Ready,2024-04-02,(blank),Interstate,,,,, ,,,meoward@rocks.com,\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)
|
||||||
|
|
||||||
with less_console_noise():
|
@less_console_noise_decorator
|
||||||
# Add security email information
|
def test_domain_data_full(self):
|
||||||
self.domain_1.name = "defaultsecurity.gov"
|
"""Shows security contacts, filtered by state"""
|
||||||
self.domain_1.save()
|
# Add security email information
|
||||||
# Invoke setter
|
self.domain_1.name = "defaultsecurity.gov"
|
||||||
self.domain_1.security_contact
|
self.domain_1.save()
|
||||||
# Invoke setter
|
# Invoke setter
|
||||||
self.domain_2.security_contact
|
self.domain_1.security_contact
|
||||||
# Invoke setter
|
# Invoke setter
|
||||||
self.domain_3.security_contact
|
self.domain_2.security_contact
|
||||||
|
# Invoke setter
|
||||||
|
self.domain_3.security_contact
|
||||||
|
# Add a first ready date on the first domain. Leaving the others blank.
|
||||||
|
self.domain_1.first_ready = get_default_start_date()
|
||||||
|
self.domain_1.save()
|
||||||
|
# Create a CSV file in memory
|
||||||
|
csv_file = StringIO()
|
||||||
|
# Call the export functions
|
||||||
|
DomainDataFull.export_data_to_csv(csv_file)
|
||||||
|
# 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,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
||||||
|
"cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,\n"
|
||||||
|
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,dotgov@cisa.dhs.gov\n"
|
||||||
|
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,\n"
|
||||||
|
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\n"
|
||||||
|
"adomain2.gov,Interstate,,,,,registrar@dotgov.gov\n"
|
||||||
|
"zdomain12.gov,Interstate,,,,,\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)
|
||||||
|
|
||||||
# Add a first ready date on the first domain. Leaving the others blank.
|
@less_console_noise_decorator
|
||||||
self.domain_1.first_ready = get_default_start_date()
|
def test_domain_data_federal(self):
|
||||||
self.domain_1.save()
|
"""Shows security contacts, filtered by state and org type"""
|
||||||
|
# Add security email information
|
||||||
|
self.domain_1.name = "defaultsecurity.gov"
|
||||||
|
self.domain_1.save()
|
||||||
|
# Invoke setter
|
||||||
|
self.domain_1.security_contact
|
||||||
|
# Invoke setter
|
||||||
|
self.domain_2.security_contact
|
||||||
|
# Invoke setter
|
||||||
|
self.domain_3.security_contact
|
||||||
|
# Add a first ready date on the first domain. Leaving the others blank.
|
||||||
|
self.domain_1.first_ready = get_default_start_date()
|
||||||
|
self.domain_1.save()
|
||||||
|
# Create a CSV file in memory
|
||||||
|
csv_file = StringIO()
|
||||||
|
# Call the export functions
|
||||||
|
DomainDataFederal.export_data_to_csv(csv_file)
|
||||||
|
# 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,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
||||||
|
"cdomain11.gov,Federal - Executive,World War I Centennial Commission,,,,\n"
|
||||||
|
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,,,,dotgov@cisa.dhs.gov\n"
|
||||||
|
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,\n"
|
||||||
|
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,security@mail.gov\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)
|
||||||
|
|
||||||
# Create a CSV file in memory
|
@less_console_noise_decorator
|
||||||
csv_file = StringIO()
|
def test_domain_growth(self):
|
||||||
writer = csv.writer(csv_file)
|
"""Shows ready and deleted domains within a date range, sorted"""
|
||||||
# Define columns, sort fields, and filter condition
|
# Remove "Created at" and "First ready" because we can't guess this immutable, dynamically generated test data
|
||||||
columns = [
|
columns = [
|
||||||
"Domain name",
|
"Domain name",
|
||||||
"Domain type",
|
"Domain type",
|
||||||
"Agency",
|
"Agency",
|
||||||
"Organization name",
|
"Organization name",
|
||||||
"City",
|
"City",
|
||||||
"State",
|
"State",
|
||||||
"SO",
|
"Status",
|
||||||
"SO email",
|
"Expiration date",
|
||||||
"Security contact email",
|
# "Created at",
|
||||||
"Status",
|
# "First ready",
|
||||||
"Expiration date",
|
"Deleted",
|
||||||
"First ready on",
|
]
|
||||||
]
|
sort = {
|
||||||
sort_fields = ["domain__name"]
|
"custom_sort": Case(
|
||||||
filter_condition = {
|
When(domain__state=Domain.State.READY, then="domain__created_at"),
|
||||||
"domain__state__in": [
|
When(domain__state=Domain.State.DELETED, then="domain__deleted"),
|
||||||
Domain.State.READY,
|
|
||||||
Domain.State.DNS_NEEDED,
|
|
||||||
Domain.State.ON_HOLD,
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
# Call the export functions
|
|
||||||
write_csv_for_domains(
|
|
||||||
writer,
|
|
||||||
columns,
|
|
||||||
sort_fields,
|
|
||||||
filter_condition,
|
|
||||||
should_get_domain_managers=False,
|
|
||||||
should_write_header=True,
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
with patch("registrar.utility.csv_export.DomainGrowth.get_columns", return_value=columns):
|
||||||
|
with patch("registrar.utility.csv_export.DomainGrowth.get_annotations_for_sort", return_value=sort):
|
||||||
|
# Create a CSV file in memory
|
||||||
|
csv_file = StringIO()
|
||||||
|
# Call the export functions
|
||||||
|
DomainGrowth.export_data_to_csv(
|
||||||
|
csv_file,
|
||||||
|
self.start_date.strftime("%Y-%m-%d"),
|
||||||
|
self.end_date.strftime("%Y-%m-%d"),
|
||||||
|
)
|
||||||
|
# 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 first, created between day-2 and day+2, sorted by created_at then name
|
||||||
|
# and DELETED domains deleted between day-2 and day+2, sorted by deleted then name
|
||||||
|
expected_content = (
|
||||||
|
"Domain name,Domain type,Agency,Organization name,City,"
|
||||||
|
"State,Status,Expiration date, Deleted\n"
|
||||||
|
"cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,(blank)\n"
|
||||||
|
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,(blank)\n"
|
||||||
|
"cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady(blank)\n"
|
||||||
|
"zdomain12.govInterstateReady(blank)\n"
|
||||||
|
"zdomain9.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank),2024-04-01\n"
|
||||||
|
"sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,(blank),2024-04-02\n"
|
||||||
|
"xdomain7.gov,FederalArmedForcesRetirementHome,Deleted,(blank),2024-04-02\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)
|
||||||
|
|
||||||
# Reset the CSV file's position to the beginning
|
@less_console_noise_decorator
|
||||||
csv_file.seek(0)
|
def test_domain_managed(self):
|
||||||
# Read the content into a variable
|
"""Shows ready and deleted domains by an end date, sorted
|
||||||
csv_content = csv_file.read()
|
|
||||||
# We expect READY domains,
|
|
||||||
# sorted alphabetially by domain name
|
|
||||||
expected_content = (
|
|
||||||
"Domain name,Domain type,Agency,Organization name,City,State,SO,"
|
|
||||||
"SO email,Security contact email,Status,Expiration date, First ready on\n"
|
|
||||||
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready,(blank),2024-04-03\n"
|
|
||||||
"adomain2.gov,Interstate,(blank),Dns needed,(blank),(blank)\n"
|
|
||||||
"cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank),2024-04-02\n"
|
|
||||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,security@mail.gov,On hold,2023-11-15,(blank)\n"
|
|
||||||
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,"
|
|
||||||
"(blank),Ready,(blank),2023-11-01\n"
|
|
||||||
"zdomain12.govInterstateReady,(blank),2024-04-02\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)
|
|
||||||
|
|
||||||
def test_write_csv_for_domains(self):
|
|
||||||
"""Test that write_body returns the
|
|
||||||
existing domain, test that sort by domain name works,
|
|
||||||
test that filter works"""
|
|
||||||
|
|
||||||
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",
|
|
||||||
"Domain type",
|
|
||||||
"Agency",
|
|
||||||
"Organization name",
|
|
||||||
"City",
|
|
||||||
"State",
|
|
||||||
"SO",
|
|
||||||
"SO email",
|
|
||||||
"Submitter",
|
|
||||||
"Submitter title",
|
|
||||||
"Submitter email",
|
|
||||||
"Submitter phone",
|
|
||||||
"Security contact email",
|
|
||||||
"Status",
|
|
||||||
]
|
|
||||||
sort_fields = ["domain__name"]
|
|
||||||
filter_condition = {
|
|
||||||
"domain__state__in": [
|
|
||||||
Domain.State.READY,
|
|
||||||
Domain.State.DNS_NEEDED,
|
|
||||||
Domain.State.ON_HOLD,
|
|
||||||
],
|
|
||||||
}
|
|
||||||
# Call the export functions
|
|
||||||
write_csv_for_domains(
|
|
||||||
writer,
|
|
||||||
columns,
|
|
||||||
sort_fields,
|
|
||||||
filter_condition,
|
|
||||||
should_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
|
|
||||||
csv_content = csv_file.read()
|
|
||||||
# We expect READY domains,
|
|
||||||
# sorted alphabetially by domain name
|
|
||||||
expected_content = (
|
|
||||||
"Domain name,Domain type,Agency,Organization name,City,State,SO,"
|
|
||||||
"SO email,Submitter,Submitter title,Submitter email,Submitter phone,"
|
|
||||||
"Security contact email,Status\n"
|
|
||||||
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n"
|
|
||||||
"adomain2.gov,Interstate,Dns needed\n"
|
|
||||||
"cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady\n"
|
|
||||||
"cdomain1.gov,Federal - Executive,World War I Centennial Commission,Ready\n"
|
|
||||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,On hold\n"
|
|
||||||
"zdomain12.govInterstateReady\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)
|
|
||||||
|
|
||||||
def test_write_domains_body_additional(self):
|
|
||||||
"""An additional test for filters and multi-column sort"""
|
|
||||||
|
|
||||||
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",
|
|
||||||
"Domain type",
|
|
||||||
"Agency",
|
|
||||||
"Organization name",
|
|
||||||
"City",
|
|
||||||
"State",
|
|
||||||
"Security contact email",
|
|
||||||
]
|
|
||||||
sort_fields = ["domain__name", "federal_agency", "generic_org_type"]
|
|
||||||
filter_condition = {
|
|
||||||
"generic_org_type__icontains": "federal",
|
|
||||||
"domain__state__in": [
|
|
||||||
Domain.State.READY,
|
|
||||||
Domain.State.DNS_NEEDED,
|
|
||||||
Domain.State.ON_HOLD,
|
|
||||||
],
|
|
||||||
}
|
|
||||||
# Call the export functions
|
|
||||||
write_csv_for_domains(
|
|
||||||
writer,
|
|
||||||
columns,
|
|
||||||
sort_fields,
|
|
||||||
filter_condition,
|
|
||||||
should_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
|
|
||||||
csv_content = csv_file.read()
|
|
||||||
# We expect READY domains,
|
|
||||||
# federal only
|
|
||||||
# sorted alphabetially by domain name
|
|
||||||
expected_content = (
|
|
||||||
"Domain name,Domain type,Agency,Organization name,City,"
|
|
||||||
"State,Security contact email\n"
|
|
||||||
"adomain10.gov,Federal,Armed Forces Retirement Home\n"
|
|
||||||
"cdomain11.govFederal-ExecutiveWorldWarICentennialCommission\n"
|
|
||||||
"cdomain1.gov,Federal - Executive,World War I Centennial Commission\n"
|
|
||||||
"ddomain3.gov,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)
|
|
||||||
|
|
||||||
def test_write_domains_body_with_date_filter_pulls_domains_in_range(self):
|
|
||||||
"""Test that domains that are
|
|
||||||
1. READY and their first_ready dates are in range
|
|
||||||
2. DELETED and their deleted dates are in range
|
|
||||||
are pulled when the growth report conditions are applied to export_domains_to_writed.
|
|
||||||
Test that ready domains are sorted by first_ready/deleted dates first, names second.
|
|
||||||
|
|
||||||
We considered testing export_data_domain_growth_to_csv which calls write_body
|
|
||||||
and would have been easy to set up, but expected_content would contain created_at dates
|
|
||||||
which are hard to mock.
|
|
||||||
|
|
||||||
TODO: Simplify if created_at is not needed for the report."""
|
|
||||||
|
|
||||||
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",
|
|
||||||
"Domain type",
|
|
||||||
"Agency",
|
|
||||||
"Organization name",
|
|
||||||
"City",
|
|
||||||
"State",
|
|
||||||
"Status",
|
|
||||||
"Expiration date",
|
|
||||||
]
|
|
||||||
sort_fields = [
|
|
||||||
"created_at",
|
|
||||||
"domain__name",
|
|
||||||
]
|
|
||||||
sort_fields_for_deleted_domains = [
|
|
||||||
"domain__deleted",
|
|
||||||
"domain__name",
|
|
||||||
]
|
|
||||||
filter_condition = {
|
|
||||||
"domain__state__in": [
|
|
||||||
Domain.State.READY,
|
|
||||||
],
|
|
||||||
"domain__first_ready__lte": self.end_date,
|
|
||||||
"domain__first_ready__gte": self.start_date,
|
|
||||||
}
|
|
||||||
filter_conditions_for_deleted_domains = {
|
|
||||||
"domain__state__in": [
|
|
||||||
Domain.State.DELETED,
|
|
||||||
],
|
|
||||||
"domain__deleted__lte": self.end_date,
|
|
||||||
"domain__deleted__gte": self.start_date,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Call the export functions
|
|
||||||
write_csv_for_domains(
|
|
||||||
writer,
|
|
||||||
columns,
|
|
||||||
sort_fields,
|
|
||||||
filter_condition,
|
|
||||||
should_get_domain_managers=False,
|
|
||||||
should_write_header=True,
|
|
||||||
)
|
|
||||||
write_csv_for_domains(
|
|
||||||
writer,
|
|
||||||
columns,
|
|
||||||
sort_fields_for_deleted_domains,
|
|
||||||
filter_conditions_for_deleted_domains,
|
|
||||||
should_get_domain_managers=False,
|
|
||||||
should_write_header=False,
|
|
||||||
)
|
|
||||||
# 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 first, created between day-2 and day+2, sorted by created_at then name
|
|
||||||
# and DELETED domains deleted between day-2 and day+2, sorted by deleted then name
|
|
||||||
expected_content = (
|
|
||||||
"Domain name,Domain type,Agency,Organization name,City,"
|
|
||||||
"State,Status,Expiration date\n"
|
|
||||||
"cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,(blank)\n"
|
|
||||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,(blank)\n"
|
|
||||||
"cdomain11.govFederal-ExecutiveWorldWarICentennialCommissionReady(blank)\n"
|
|
||||||
"zdomain12.govInterstateReady(blank)\n"
|
|
||||||
"zdomain9.gov,Federal,ArmedForcesRetirementHome,Deleted,(blank)\n"
|
|
||||||
"sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,(blank)\n"
|
|
||||||
"xdomain7.gov,FederalArmedForcesRetirementHome,Deleted,(blank)\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)
|
|
||||||
|
|
||||||
def test_export_domains_to_writer_domain_managers(self):
|
|
||||||
"""Test that export_domains_to_writer returns the
|
|
||||||
expected domain managers.
|
|
||||||
|
|
||||||
An invited user, woofwardthethird, should also be pulled into this report.
|
An invited user, woofwardthethird, should also be pulled into this report.
|
||||||
|
|
||||||
squeaker@rocks.com is invited to domain2 (DNS_NEEDED) and domain10 (No managers).
|
squeaker@rocks.com is invited to domain2 (DNS_NEEDED) and domain10 (No managers).
|
||||||
She should show twice in this report but not in test_export_data_managed_domains_to_csv."""
|
She should show twice in this report but not in test_DomainManaged."""
|
||||||
|
# Create a CSV file in memory
|
||||||
|
csv_file = StringIO()
|
||||||
|
# Call the export functions
|
||||||
|
DomainManaged.export_data_to_csv(
|
||||||
|
csv_file,
|
||||||
|
self.start_date.strftime("%Y-%m-%d"),
|
||||||
|
self.end_date.strftime("%Y-%m-%d"),
|
||||||
|
)
|
||||||
|
# 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 the READY domain names with the domain managers: Their counts, and listing at end_date.
|
||||||
|
expected_content = (
|
||||||
|
"MANAGED DOMAINS COUNTS AT START DATE\n"
|
||||||
|
"Total,Federal,Interstate,State or territory,Tribal,County,City,Special district,"
|
||||||
|
"School district,Election office\n"
|
||||||
|
"0,0,0,0,0,0,0,0,0,0\n"
|
||||||
|
"\n"
|
||||||
|
"MANAGED DOMAINS COUNTS AT END DATE\n"
|
||||||
|
"Total,Federal,Interstate,State or territory,Tribal,County,City,"
|
||||||
|
"Special district,School district,Election office\n"
|
||||||
|
"3,2,1,0,0,0,0,0,0,0\n"
|
||||||
|
"\n"
|
||||||
|
"Domain name,Domain type,Domain managers,Invited domain managers\n"
|
||||||
|
"cdomain11.gov,Federal - Executive,meoward@rocks.com,\n"
|
||||||
|
'cdomain1.gov,Federal - Executive,"meoward@rocks.com, info@example.com, big_lebowski@dude.co",'
|
||||||
|
"woofwardthethird@rocks.com\n"
|
||||||
|
"zdomain12.gov,Interstate,meoward@rocks.com,\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)
|
||||||
|
|
||||||
with less_console_noise():
|
@less_console_noise_decorator
|
||||||
|
def test_domain_unmanaged(self):
|
||||||
|
"""Shows unmanaged domains by an end date, sorted"""
|
||||||
|
# Create a CSV file in memory
|
||||||
|
csv_file = StringIO()
|
||||||
|
DomainUnmanaged.export_data_to_csv(
|
||||||
|
csv_file, self.start_date.strftime("%Y-%m-%d"), self.end_date.strftime("%Y-%m-%d")
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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 the READY domain names with the domain managers: Their counts, and listing at end_date.
|
||||||
|
expected_content = (
|
||||||
|
"UNMANAGED DOMAINS AT START DATE\n"
|
||||||
|
"Total,Federal,Interstate,State or territory,Tribal,County,City,Special district,"
|
||||||
|
"School district,Election office\n"
|
||||||
|
"0,0,0,0,0,0,0,0,0,0\n"
|
||||||
|
"\n"
|
||||||
|
"UNMANAGED DOMAINS AT END DATE\n"
|
||||||
|
"Total,Federal,Interstate,State or territory,Tribal,County,City,Special district,"
|
||||||
|
"School district,Election office\n"
|
||||||
|
"1,1,0,0,0,0,0,0,0,0\n"
|
||||||
|
"\n"
|
||||||
|
"Domain name,Domain type\n"
|
||||||
|
"adomain10.gov,Federal\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)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_domain_request_growth(self):
|
||||||
|
"""Shows submitted requests within a date range, sorted"""
|
||||||
|
# Remove "Submitted at" because we can't guess this immutable, dynamically generated test data
|
||||||
|
columns = [
|
||||||
|
"Domain request",
|
||||||
|
"Domain type",
|
||||||
|
"Federal type",
|
||||||
|
# "Submitted at",
|
||||||
|
]
|
||||||
|
with patch("registrar.utility.csv_export.DomainRequestGrowth.get_columns", return_value=columns):
|
||||||
# Create a CSV file in memory
|
# Create a CSV file in memory
|
||||||
csv_file = StringIO()
|
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",
|
|
||||||
"SO",
|
|
||||||
"SO email",
|
|
||||||
"Security contact email",
|
|
||||||
]
|
|
||||||
sort_fields = ["domain__name"]
|
|
||||||
filter_condition = {
|
|
||||||
"domain__state__in": [
|
|
||||||
Domain.State.READY,
|
|
||||||
Domain.State.DNS_NEEDED,
|
|
||||||
Domain.State.ON_HOLD,
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
# Call the export functions
|
# Call the export functions
|
||||||
write_csv_for_domains(
|
DomainRequestGrowth.export_data_to_csv(
|
||||||
writer,
|
csv_file,
|
||||||
columns,
|
self.start_date.strftime("%Y-%m-%d"),
|
||||||
sort_fields,
|
self.end_date.strftime("%Y-%m-%d"),
|
||||||
filter_condition,
|
|
||||||
should_get_domain_managers=True,
|
|
||||||
should_write_header=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Reset the CSV file's position to the beginning
|
# Reset the CSV file's position to the beginning
|
||||||
csv_file.seek(0)
|
csv_file.seek(0)
|
||||||
# Read the content into a variable
|
# Read the content into a variable
|
||||||
csv_content = csv_file.read()
|
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,SO,SO email,"
|
|
||||||
"Security contact email,Domain manager 1,DM1 status,Domain manager 2,DM2 status,"
|
|
||||||
"Domain manager 3,DM3 status,Domain manager 4,DM4 status\n"
|
|
||||||
"adomain10.gov,Ready,(blank),Federal,Armed Forces Retirement Home,,,, , ,squeaker@rocks.com, I\n"
|
|
||||||
"adomain2.gov,Dns needed,(blank),Interstate,,,,, , , ,meoward@rocks.com, R,squeaker@rocks.com, I\n"
|
|
||||||
"cdomain11.govReady,(blank),Federal-ExecutiveWorldWarICentennialCommissionmeoward@rocks.comR\n"
|
|
||||||
"cdomain1.gov,Ready,(blank),Federal - Executive,World War I Centennial Commission,,,"
|
|
||||||
", , , ,meoward@rocks.com,R,info@example.com,R,big_lebowski@dude.co,R,"
|
|
||||||
"woofwardthethird@rocks.com,I\n"
|
|
||||||
"ddomain3.gov,On hold,(blank),Federal,Armed Forces Retirement Home,,,, , , ,,\n"
|
|
||||||
"zdomain12.gov,Ready,(blank),Interstate,meoward@rocks.com,R\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)
|
|
||||||
|
|
||||||
def test_export_data_managed_domains_to_csv(self):
|
|
||||||
"""Test get counts for domains that have domain managers for two different dates,
|
|
||||||
get list of managed domains at end_date.
|
|
||||||
|
|
||||||
An invited user, woofwardthethird, should also be pulled into this report."""
|
|
||||||
|
|
||||||
with less_console_noise():
|
|
||||||
# Create a CSV file in memory
|
|
||||||
csv_file = StringIO()
|
|
||||||
export_data_managed_domains_to_csv(
|
|
||||||
csv_file, self.start_date.strftime("%Y-%m-%d"), self.end_date.strftime("%Y-%m-%d")
|
|
||||||
)
|
|
||||||
|
|
||||||
# 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 the READY domain names with the domain managers: Their counts, and listing at end_date.
|
|
||||||
expected_content = (
|
|
||||||
"MANAGED DOMAINS COUNTS AT START DATE\n"
|
|
||||||
"Total,Federal,Interstate,State or territory,Tribal,County,City,Special district,"
|
|
||||||
"School district,Election office\n"
|
|
||||||
"0,0,0,0,0,0,0,0,0,0\n"
|
|
||||||
"\n"
|
|
||||||
"MANAGED DOMAINS COUNTS AT END DATE\n"
|
|
||||||
"Total,Federal,Interstate,State or territory,Tribal,County,City,"
|
|
||||||
"Special district,School district,Election office\n"
|
|
||||||
"3,2,1,0,0,0,0,0,0,0\n"
|
|
||||||
"\n"
|
|
||||||
"Domain name,Domain type,Domain manager 1,DM1 status,Domain manager 2,DM2 status,"
|
|
||||||
"Domain manager 3,DM3 status,Domain manager 4,DM4 status\n"
|
|
||||||
"cdomain11.govFederal-Executivemeoward@rocks.com, R\n"
|
|
||||||
"cdomain1.gov,Federal - Executive,meoward@rocks.com,R,info@example.com,R,"
|
|
||||||
"big_lebowski@dude.co,R,woofwardthethird@rocks.com,I\n"
|
|
||||||
"zdomain12.govInterstatemeoward@rocks.com,R\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)
|
|
||||||
|
|
||||||
def test_export_data_unmanaged_domains_to_csv(self):
|
|
||||||
"""Test get counts for domains that do not have domain managers for two different dates,
|
|
||||||
get list of unmanaged domains at end_date."""
|
|
||||||
|
|
||||||
with less_console_noise():
|
|
||||||
# Create a CSV file in memory
|
|
||||||
csv_file = StringIO()
|
|
||||||
export_data_unmanaged_domains_to_csv(
|
|
||||||
csv_file, self.start_date.strftime("%Y-%m-%d"), self.end_date.strftime("%Y-%m-%d")
|
|
||||||
)
|
|
||||||
|
|
||||||
# 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 the READY domain names with the domain managers: Their counts, and listing at end_date.
|
|
||||||
expected_content = (
|
|
||||||
"UNMANAGED DOMAINS AT START DATE\n"
|
|
||||||
"Total,Federal,Interstate,State or territory,Tribal,County,City,Special district,"
|
|
||||||
"School district,Election office\n"
|
|
||||||
"0,0,0,0,0,0,0,0,0,0\n"
|
|
||||||
"\n"
|
|
||||||
"UNMANAGED DOMAINS AT END DATE\n"
|
|
||||||
"Total,Federal,Interstate,State or territory,Tribal,County,City,Special district,"
|
|
||||||
"School district,Election office\n"
|
|
||||||
"1,1,0,0,0,0,0,0,0,0\n"
|
|
||||||
"\n"
|
|
||||||
"Domain name,Domain type\n"
|
|
||||||
"adomain10.gov,Federal\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)
|
|
||||||
|
|
||||||
def test_write_requests_body_with_date_filter_pulls_requests_in_range(self):
|
|
||||||
"""Test that requests that are
|
|
||||||
1. SUBMITTED and their submission_date are in range
|
|
||||||
are pulled when the growth report conditions are applied to export_requests_to_writed.
|
|
||||||
Test that requests are sorted by requested domain name.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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
|
|
||||||
# We'll skip submission date because it's dynamic and therefore
|
|
||||||
# impossible to set in expected_content
|
|
||||||
columns = ["Domain request", "Domain type", "Federal type"]
|
|
||||||
sort_fields = [
|
|
||||||
"requested_domain__name",
|
|
||||||
]
|
|
||||||
filter_condition = {
|
|
||||||
"status": DomainRequest.DomainRequestStatus.SUBMITTED,
|
|
||||||
"submission_date__lte": self.end_date,
|
|
||||||
"submission_date__gte": self.start_date,
|
|
||||||
}
|
|
||||||
|
|
||||||
additional_values = ["requested_domain__name"]
|
|
||||||
all_requests = DomainRequest.objects.filter(**filter_condition).order_by(*sort_fields).distinct()
|
|
||||||
annotated_requests = DomainRequestExport.annotate_and_retrieve_fields(all_requests, {}, additional_values)
|
|
||||||
requests_dict = convert_queryset_to_dict(annotated_requests, is_model=False)
|
|
||||||
DomainRequestExport.write_csv_for_requests(writer, columns, requests_dict)
|
|
||||||
# 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 first, created between today-2 and today+2, sorted by created_at then name
|
|
||||||
# and DELETED domains deleted between today-2 and today+2, sorted by deleted then name
|
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"Domain request,Domain type,Federal type\n"
|
"Domain request,Domain type,Federal type\n"
|
||||||
"city3.gov,Federal,Executive\n"
|
"city3.gov,Federal,Executive\n"
|
||||||
|
@ -703,68 +511,81 @@ class ExportDataTest(MockDb, MockEppLib):
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_full_domain_request_report(self):
|
def test_domain_request_data_full(self):
|
||||||
"""Tests the full domain request report."""
|
"""Tests the full domain request report."""
|
||||||
|
# Remove "Submitted at" because we can't guess this immutable, dynamically generated test data
|
||||||
# Create a CSV file in memory
|
columns = [
|
||||||
csv_file = StringIO()
|
"Domain request",
|
||||||
writer = csv.writer(csv_file)
|
# "Submitted at",
|
||||||
|
"Status",
|
||||||
# Call the report. Get existing fields from the report itself.
|
"Domain type",
|
||||||
annotations = DomainRequestExport._full_domain_request_annotations()
|
"Federal type",
|
||||||
additional_values = [
|
"Federal agency",
|
||||||
"requested_domain__name",
|
"Organization name",
|
||||||
"federal_agency__agency",
|
"Election office",
|
||||||
"senior_official__first_name",
|
"City",
|
||||||
"senior_official__last_name",
|
"State/territory",
|
||||||
"senior_official__email",
|
"Region",
|
||||||
"senior_official__title",
|
"Creator first name",
|
||||||
"creator__first_name",
|
"Creator last name",
|
||||||
"creator__last_name",
|
"Creator email",
|
||||||
"creator__email",
|
"Creator approved domains count",
|
||||||
"investigator__email",
|
"Creator active requests count",
|
||||||
|
"Alternative domains",
|
||||||
|
"SO first name",
|
||||||
|
"SO last name",
|
||||||
|
"SO email",
|
||||||
|
"SO title/role",
|
||||||
|
"Request purpose",
|
||||||
|
"Request additional details",
|
||||||
|
"Other contacts",
|
||||||
|
"CISA regional representative",
|
||||||
|
"Current websites",
|
||||||
|
"Investigator",
|
||||||
]
|
]
|
||||||
requests = DomainRequest.objects.exclude(status=DomainRequest.DomainRequestStatus.STARTED)
|
with patch("registrar.utility.csv_export.DomainRequestDataFull.get_columns", return_value=columns):
|
||||||
annotated_requests = DomainRequestExport.annotate_and_retrieve_fields(requests, annotations, additional_values)
|
# Create a CSV file in memory
|
||||||
requests_dict = convert_queryset_to_dict(annotated_requests, is_model=False)
|
csv_file = StringIO()
|
||||||
DomainRequestExport.write_csv_for_requests(writer, DomainRequestExport.all_columns, requests_dict)
|
# Call the export functions
|
||||||
|
DomainRequestDataFull.export_data_to_csv(csv_file)
|
||||||
# Reset the CSV file's position to the beginning
|
# Reset the CSV file's position to the beginning
|
||||||
csv_file.seek(0)
|
csv_file.seek(0)
|
||||||
# Read the content into a variable
|
# Read the content into a variable
|
||||||
csv_content = csv_file.read()
|
csv_content = csv_file.read()
|
||||||
expected_content = (
|
expected_content = (
|
||||||
# Header
|
# Header
|
||||||
"Domain request,Submitted at,Status,Domain type,Federal type,"
|
"Domain request,Status,Domain type,Federal type,"
|
||||||
"Federal agency,Organization name,Election office,City,State/territory,"
|
"Federal agency,Organization name,Election office,City,State/territory,"
|
||||||
"Region,Creator first name,Creator last name,Creator email,Creator approved domains count,"
|
"Region,Creator first name,Creator last name,Creator email,Creator approved domains count,"
|
||||||
"Creator active requests count,Alternative domains,SO first name,SO last name,SO email,"
|
"Creator active requests count,Alternative domains,SO first name,SO last name,SO email,"
|
||||||
"SO title/role,Request purpose,Request additional details,Other contacts,"
|
"SO title/role,Request purpose,Request additional details,Other contacts,"
|
||||||
"CISA regional representative,Current websites,Investigator\n"
|
"CISA regional representative,Current websites,Investigator\n"
|
||||||
# Content
|
# Content
|
||||||
"city2.gov,,In review,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,testy@town.com,"
|
"city5.gov,,Approved,Federal,Executive,,Testorg,N/A,,NY,2,,,,1,0,city1.gov,Testy,Tester,testy@town.com,"
|
||||||
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
||||||
"city3.gov,2024-04-02,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,"
|
"city2.gov,,In review,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,"
|
||||||
"cheeseville.gov | city1.gov | igorville.gov,Testy,Tester,testy@town.com,Chief Tester,"
|
"testy@town.com,"
|
||||||
"Purpose of the site,CISA-first-name CISA-last-name | There is more,Meow Tester24 te2@town.com | "
|
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
||||||
"Testy1232 Tester24 te2@town.com | Testy Tester testy2@town.com,test@igorville.com,"
|
'city3.gov,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,"cheeseville.gov, city1.gov,'
|
||||||
"city.com | https://www.example2.com | https://www.example.com,\n"
|
'igorville.gov",Testy,Tester,testy@town.com,Chief Tester,Purpose of the site,CISA-first-name '
|
||||||
"city4.gov,2024-04-02,Submitted,City,Executive,,Testorg,Yes,,NY,2,,,,0,1,city1.gov,Testy,Tester,"
|
"CISA-last-name "
|
||||||
"testy@town.com,Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
|
'| There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, Testy Tester '
|
||||||
"Testy Tester testy2@town.com,cisaRep@igorville.gov,city.com,\n"
|
'testy2@town.com"'
|
||||||
"city5.gov,,Approved,Federal,Executive,,Testorg,N/A,,NY,2,,,,1,0,city1.gov,Testy,Tester,testy@town.com,"
|
',test@igorville.com,"city.com, https://www.example2.com, https://www.example.com",\n'
|
||||||
"Chief Tester,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
"city4.gov,Submitted,City,Executive,,Testorg,Yes,,NY,2,,,,0,1,city1.gov,Testy,Tester,testy@town.com,"
|
||||||
"city6.gov,2024-04-02,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,"
|
"Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester "
|
||||||
"testy@town.com,Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
|
"testy2@town.com"
|
||||||
"Testy Tester testy2@town.com,cisaRep@igorville.gov,city.com,"
|
",cisaRep@igorville.gov,city.com,\n"
|
||||||
)
|
"city6.gov,Submitted,Federal,Executive,,Testorg,N/A,,NY,2,,,,0,1,city1.gov,Testy,Tester,testy@town.com,"
|
||||||
|
"Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester "
|
||||||
# Normalize line endings and remove commas,
|
"testy2@town.com,"
|
||||||
# spaces and leading/trailing whitespace
|
"cisaRep@igorville.gov,city.com,\n"
|
||||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
)
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
# Normalize line endings and remove commas,
|
||||||
|
# spaces and leading/trailing whitespace
|
||||||
self.assertEqual(csv_content, expected_content)
|
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(MockDb):
|
class HelperFunctions(MockDb):
|
||||||
|
@ -790,12 +611,12 @@ class HelperFunctions(MockDb):
|
||||||
"domain__first_ready__lte": self.end_date,
|
"domain__first_ready__lte": self.end_date,
|
||||||
}
|
}
|
||||||
# Test with distinct
|
# Test with distinct
|
||||||
managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
|
managed_domains_sliced_at_end_date = DomainExport.get_sliced_domains(filter_condition)
|
||||||
expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
|
expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
|
||||||
self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
|
self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
|
||||||
|
|
||||||
# Test without distinct
|
# Test without distinct
|
||||||
managed_domains_sliced_at_end_date = get_sliced_domains(filter_condition)
|
managed_domains_sliced_at_end_date = DomainExport.get_sliced_domains(filter_condition)
|
||||||
expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
|
expected_content = [3, 2, 1, 0, 0, 0, 0, 0, 0, 0]
|
||||||
self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
|
self.assertEqual(managed_domains_sliced_at_end_date, expected_content)
|
||||||
|
|
||||||
|
@ -807,6 +628,6 @@ class HelperFunctions(MockDb):
|
||||||
"status": DomainRequest.DomainRequestStatus.SUBMITTED,
|
"status": DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||||
"submission_date__lte": self.end_date,
|
"submission_date__lte": self.end_date,
|
||||||
}
|
}
|
||||||
submitted_requests_sliced_at_end_date = get_sliced_requests(filter_condition)
|
submitted_requests_sliced_at_end_date = DomainRequestExport.get_sliced_requests(filter_condition)
|
||||||
expected_content = [3, 2, 0, 0, 0, 0, 1, 0, 0, 1]
|
expected_content = [3, 2, 0, 0, 0, 0, 1, 0, 0, 1]
|
||||||
self.assertEqual(submitted_requests_sliced_at_end_date, expected_content)
|
self.assertEqual(submitted_requests_sliced_at_end_date, expected_content)
|
||||||
|
|
|
@ -1,105 +0,0 @@
|
||||||
from django.test import TestCase
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from registrar.models import Contact, User
|
|
||||||
|
|
||||||
|
|
||||||
class TestUserPostSave(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
Contact.objects.all().delete()
|
|
||||||
self.username = "test_signal_user"
|
|
||||||
self.first_name = "First"
|
|
||||||
self.last_name = "Last"
|
|
||||||
self.email = "signal@example.com"
|
|
||||||
self.phone = "202-555-0133"
|
|
||||||
|
|
||||||
self.preferred_first_name = "One"
|
|
||||||
self.preferred_last_name = "Two"
|
|
||||||
self.preferred_email = "front_desk@example.com"
|
|
||||||
self.preferred_phone = "202-555-0134"
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
Contact.objects.all().delete()
|
|
||||||
User.objects.all().delete()
|
|
||||||
|
|
||||||
def test_user_created_without_matching_contact(self):
|
|
||||||
"""Expect 1 Contact containing data copied from User."""
|
|
||||||
self.assertEqual(len(Contact.objects.all()), 0)
|
|
||||||
user = get_user_model().objects.create(
|
|
||||||
username=self.username,
|
|
||||||
first_name=self.first_name,
|
|
||||||
last_name=self.last_name,
|
|
||||||
email=self.email,
|
|
||||||
phone=self.phone,
|
|
||||||
)
|
|
||||||
actual = Contact.objects.get(user=user)
|
|
||||||
self.assertEqual(actual.first_name, self.first_name)
|
|
||||||
self.assertEqual(actual.last_name, self.last_name)
|
|
||||||
self.assertEqual(actual.email, self.email)
|
|
||||||
self.assertEqual(actual.phone, self.phone)
|
|
||||||
|
|
||||||
def test_user_created_with_matching_contact(self):
|
|
||||||
"""Expect 1 Contact associated, but with no data copied from User."""
|
|
||||||
self.assertEqual(len(Contact.objects.all()), 0)
|
|
||||||
Contact.objects.create(
|
|
||||||
first_name=self.preferred_first_name,
|
|
||||||
last_name=self.preferred_last_name,
|
|
||||||
email=self.email, # must be the same, to find the match!
|
|
||||||
phone=self.preferred_phone,
|
|
||||||
)
|
|
||||||
user = get_user_model().objects.create(
|
|
||||||
username=self.username,
|
|
||||||
first_name=self.first_name,
|
|
||||||
last_name=self.last_name,
|
|
||||||
email=self.email,
|
|
||||||
)
|
|
||||||
actual = Contact.objects.get(user=user)
|
|
||||||
self.assertEqual(actual.first_name, self.preferred_first_name)
|
|
||||||
self.assertEqual(actual.last_name, self.preferred_last_name)
|
|
||||||
self.assertEqual(actual.email, self.email)
|
|
||||||
self.assertEqual(actual.phone, self.preferred_phone)
|
|
||||||
|
|
||||||
def test_user_updated_without_matching_contact(self):
|
|
||||||
"""Expect 1 Contact containing data copied from User."""
|
|
||||||
# create the user
|
|
||||||
self.assertEqual(len(Contact.objects.all()), 0)
|
|
||||||
user = get_user_model().objects.create(username=self.username, first_name="", last_name="", email="", phone="")
|
|
||||||
# delete the contact
|
|
||||||
Contact.objects.all().delete()
|
|
||||||
self.assertEqual(len(Contact.objects.all()), 0)
|
|
||||||
# modify the user
|
|
||||||
user.username = self.username
|
|
||||||
user.first_name = self.first_name
|
|
||||||
user.last_name = self.last_name
|
|
||||||
user.email = self.email
|
|
||||||
user.phone = self.phone
|
|
||||||
user.save()
|
|
||||||
# test
|
|
||||||
actual = Contact.objects.get(user=user)
|
|
||||||
self.assertEqual(actual.first_name, self.first_name)
|
|
||||||
self.assertEqual(actual.last_name, self.last_name)
|
|
||||||
self.assertEqual(actual.email, self.email)
|
|
||||||
self.assertEqual(actual.phone, self.phone)
|
|
||||||
|
|
||||||
def test_user_updated_with_matching_contact(self):
|
|
||||||
"""Expect 1 Contact associated, but with no data copied from User."""
|
|
||||||
# create the user
|
|
||||||
self.assertEqual(len(Contact.objects.all()), 0)
|
|
||||||
user = get_user_model().objects.create(
|
|
||||||
username=self.username,
|
|
||||||
first_name=self.first_name,
|
|
||||||
last_name=self.last_name,
|
|
||||||
email=self.email,
|
|
||||||
phone=self.phone,
|
|
||||||
)
|
|
||||||
# modify the user
|
|
||||||
user.first_name = self.preferred_first_name
|
|
||||||
user.last_name = self.preferred_last_name
|
|
||||||
user.email = self.preferred_email
|
|
||||||
user.phone = self.preferred_phone
|
|
||||||
user.save()
|
|
||||||
# test
|
|
||||||
actual = Contact.objects.get(user=user)
|
|
||||||
self.assertEqual(actual.first_name, self.first_name)
|
|
||||||
self.assertEqual(actual.last_name, self.last_name)
|
|
||||||
self.assertEqual(actual.email, self.email)
|
|
||||||
self.assertEqual(actual.phone, self.phone)
|
|
|
@ -30,10 +30,9 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class TestViews(TestCase):
|
class TestViews(TestCase):
|
||||||
|
|
||||||
@classmethod
|
def setUp(self):
|
||||||
def setUpClass(cls):
|
super().setUp()
|
||||||
super().setUpClass()
|
self.client = Client()
|
||||||
cls.client = Client()
|
|
||||||
|
|
||||||
def test_health_check_endpoint(self):
|
def test_health_check_endpoint(self):
|
||||||
response = self.client.get("/health")
|
response = self.client.get("/health")
|
||||||
|
@ -74,8 +73,6 @@ class TestWithUser(MockEppLib):
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
super().tearDownClass()
|
super().tearDownClass()
|
||||||
#remove all contacts - this can be removed after signals are deleted
|
|
||||||
Contact.objects.all().delete()
|
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
|
@ -98,8 +95,6 @@ class TestEnvironmentVariablesEffects(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
super().tearDownClass()
|
super().tearDownClass()
|
||||||
# remove all contacts - this can be removed after signals are deleted
|
|
||||||
Contact.objects.all().delete()
|
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
@override_settings(IS_PRODUCTION=True)
|
@override_settings(IS_PRODUCTION=True)
|
||||||
|
@ -125,7 +120,7 @@ class TestEnvironmentVariablesEffects(TestCase):
|
||||||
fake_domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
fake_domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||||
|
|
||||||
# Add a role
|
# Add a role
|
||||||
fake_role, _ = UserDomainRole.objects.get_or_create(
|
UserDomainRole.objects.get_or_create(
|
||||||
user=self.user, domain=fake_domain, role=UserDomainRole.Roles.MANAGER
|
user=self.user, domain=fake_domain, role=UserDomainRole.Roles.MANAGER
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -148,7 +143,7 @@ class TestEnvironmentVariablesEffects(TestCase):
|
||||||
fake_domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
fake_domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||||
|
|
||||||
# Add a role
|
# Add a role
|
||||||
fake_role, _ = UserDomainRole.objects.get_or_create(
|
UserDomainRole.objects.get_or_create(
|
||||||
user=self.user, domain=fake_domain, role=UserDomainRole.Roles.MANAGER
|
user=self.user, domain=fake_domain, role=UserDomainRole.Roles.MANAGER
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -178,6 +173,7 @@ class HomeTests(TestWithUser):
|
||||||
# Domain.objects.all().delete()
|
# Domain.objects.all().delete()
|
||||||
# DomainRequest.objects.all().delete()
|
# DomainRequest.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_empty_domain_table(self):
|
def test_empty_domain_table(self):
|
||||||
response = self.client.get("/")
|
response = self.client.get("/")
|
||||||
self.assertContains(response, "You don't have any registered domains.")
|
self.assertContains(response, "You don't have any registered domains.")
|
||||||
|
@ -376,7 +372,10 @@ class HomeTests(TestWithUser):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Attach a user object to a contact (should not be deleted)
|
# Attach a user object to a contact (should not be deleted)
|
||||||
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
contact_user, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Hank",
|
||||||
|
last_name="McFakey",
|
||||||
|
)
|
||||||
|
|
||||||
site = DraftDomain.objects.create(name="igorville.gov")
|
site = DraftDomain.objects.create(name="igorville.gov")
|
||||||
domain_request = DomainRequest.objects.create(
|
domain_request = DomainRequest.objects.create(
|
||||||
|
@ -409,17 +408,12 @@ class HomeTests(TestWithUser):
|
||||||
igorville = DomainRequest.objects.filter(requested_domain__name="igorville.gov")
|
igorville = DomainRequest.objects.filter(requested_domain__name="igorville.gov")
|
||||||
self.assertFalse(igorville.exists())
|
self.assertFalse(igorville.exists())
|
||||||
|
|
||||||
# Check if the orphaned contact was deleted
|
# Check if the orphaned contacts were deleted
|
||||||
orphan = Contact.objects.filter(id=contact.id)
|
orphan = Contact.objects.filter(id=contact.id)
|
||||||
self.assertFalse(orphan.exists())
|
self.assertFalse(orphan.exists())
|
||||||
|
orphan = Contact.objects.filter(id=contact_user.id)
|
||||||
|
self.assertFalse(orphan.exists())
|
||||||
|
|
||||||
# All non-orphan contacts should still exist and are unaltered
|
|
||||||
try:
|
|
||||||
current_user = Contact.objects.filter(id=contact_user.id).get()
|
|
||||||
except Contact.DoesNotExist:
|
|
||||||
self.fail("contact_user (a non-orphaned contact) was deleted")
|
|
||||||
|
|
||||||
self.assertEqual(current_user, contact_user)
|
|
||||||
try:
|
try:
|
||||||
edge_case = Contact.objects.filter(id=contact_2.id).get()
|
edge_case = Contact.objects.filter(id=contact_2.id).get()
|
||||||
except Contact.DoesNotExist:
|
except Contact.DoesNotExist:
|
||||||
|
@ -451,7 +445,10 @@ class HomeTests(TestWithUser):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Attach a user object to a contact (should not be deleted)
|
# Attach a user object to a contact (should not be deleted)
|
||||||
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
contact_user, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Hank",
|
||||||
|
last_name="McFakey",
|
||||||
|
)
|
||||||
|
|
||||||
site = DraftDomain.objects.create(name="igorville.gov")
|
site = DraftDomain.objects.create(name="igorville.gov")
|
||||||
domain_request = DomainRequest.objects.create(
|
domain_request = DomainRequest.objects.create(
|
||||||
|
@ -590,7 +587,7 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
||||||
self.assertContains(finish_setup_page, "Enter your phone number.")
|
self.assertContains(finish_setup_page, "Enter your phone number.")
|
||||||
|
|
||||||
# Check for the name of the save button
|
# Check for the name of the save button
|
||||||
self.assertContains(finish_setup_page, "contact_setup_save_button")
|
self.assertContains(finish_setup_page, "user_setup_save_button")
|
||||||
|
|
||||||
# Add a phone number
|
# Add a phone number
|
||||||
finish_setup_form = finish_setup_page.form
|
finish_setup_form = finish_setup_page.form
|
||||||
|
@ -637,10 +634,11 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
||||||
self.assertContains(finish_setup_page, "Enter your phone number.")
|
self.assertContains(finish_setup_page, "Enter your phone number.")
|
||||||
|
|
||||||
# Check for the name of the save button
|
# Check for the name of the save button
|
||||||
self.assertContains(finish_setup_page, "contact_setup_save_button")
|
self.assertContains(finish_setup_page, "user_setup_save_button")
|
||||||
|
|
||||||
# Add a phone number
|
# Add a phone number
|
||||||
finish_setup_form = finish_setup_page.form
|
finish_setup_form = finish_setup_page.form
|
||||||
|
finish_setup_form["first_name"] = "firstname"
|
||||||
finish_setup_form["phone"] = "(201) 555-0123"
|
finish_setup_form["phone"] = "(201) 555-0123"
|
||||||
finish_setup_form["title"] = "CEO"
|
finish_setup_form["title"] = "CEO"
|
||||||
finish_setup_form["last_name"] = "example"
|
finish_setup_form["last_name"] = "example"
|
||||||
|
@ -652,7 +650,7 @@ class FinishUserProfileTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
# Submit the form using the specific submit button to execute the redirect
|
# Submit the form using the specific submit button to execute the redirect
|
||||||
completed_setup_page = self._submit_form_webtest(
|
completed_setup_page = self._submit_form_webtest(
|
||||||
finish_setup_form, follow=True, name="contact_setup_submit_button"
|
finish_setup_form, follow=True, name="user_setup_submit_button"
|
||||||
)
|
)
|
||||||
self.assertEqual(completed_setup_page.status_code, 200)
|
self.assertEqual(completed_setup_page.status_code, 200)
|
||||||
|
|
||||||
|
@ -783,6 +781,8 @@ class FinishUserProfileForOtherUsersTests(TestWithUser, WebTest):
|
||||||
self.assertContains(save_page, "Your profile has been updated.")
|
self.assertContains(save_page, "Your profile has been updated.")
|
||||||
|
|
||||||
# We need to assert that logo is not clickable and links to manage your domain are not present
|
# We need to assert that logo is not clickable and links to manage your domain are not present
|
||||||
|
# NOTE: "anage" is not a typo. It is to accomodate the fact that the "m" is uppercase in one
|
||||||
|
# instance and lowercase in the other.
|
||||||
self.assertContains(save_page, "anage your domains", count=2)
|
self.assertContains(save_page, "anage your domains", count=2)
|
||||||
self.assertNotContains(
|
self.assertNotContains(
|
||||||
save_page, "Before you can manage your domains, we need you to add contact information"
|
save_page, "Before you can manage your domains, we need you to add contact information"
|
||||||
|
@ -918,7 +918,10 @@ class UserProfileTests(TestWithUser, WebTest):
|
||||||
def test_request_when_profile_feature_on(self):
|
def test_request_when_profile_feature_on(self):
|
||||||
"""test that Your profile is in request page when profile feature is on"""
|
"""test that Your profile is in request page when profile feature is on"""
|
||||||
|
|
||||||
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
contact_user, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Hank",
|
||||||
|
last_name="McFakerson",
|
||||||
|
)
|
||||||
site = DraftDomain.objects.create(name="igorville.gov")
|
site = DraftDomain.objects.create(name="igorville.gov")
|
||||||
domain_request = DomainRequest.objects.create(
|
domain_request = DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=self.user,
|
||||||
|
@ -937,7 +940,10 @@ class UserProfileTests(TestWithUser, WebTest):
|
||||||
def test_request_when_profile_feature_off(self):
|
def test_request_when_profile_feature_off(self):
|
||||||
"""test that Your profile is not in request page when profile feature is off"""
|
"""test that Your profile is not in request page when profile feature is off"""
|
||||||
|
|
||||||
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
contact_user, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Hank",
|
||||||
|
last_name="McFakerson",
|
||||||
|
)
|
||||||
site = DraftDomain.objects.create(name="igorville.gov")
|
site = DraftDomain.objects.create(name="igorville.gov")
|
||||||
domain_request = DomainRequest.objects.create(
|
domain_request = DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=self.user,
|
||||||
|
|
|
@ -209,10 +209,6 @@ class TestDomainPermissions(TestWithDomainPermissions):
|
||||||
|
|
||||||
class TestDomainOverview(TestWithDomainPermissions, WebTest):
|
class TestDomainOverview(TestWithDomainPermissions, WebTest):
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
super().setUpClass()
|
|
||||||
cls.client.force_login(cls.user)
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.app.set_user(self.user.username)
|
self.app.set_user(self.user.username)
|
||||||
|
@ -1505,8 +1501,8 @@ class TestDomainContactInformation(TestDomainOverview):
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_domain_your_contact_information_content(self):
|
def test_domain_your_contact_information_content(self):
|
||||||
"""Logged-in user's contact information appears on the page."""
|
"""Logged-in user's contact information appears on the page."""
|
||||||
self.user.contact.first_name = "Testy"
|
self.user.first_name = "Testy"
|
||||||
self.user.contact.save()
|
self.user.save()
|
||||||
page = self.app.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id}))
|
page = self.app.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id}))
|
||||||
self.assertContains(page, "Testy")
|
self.assertContains(page, "Testy")
|
||||||
|
|
||||||
|
|
|
@ -3026,7 +3026,10 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Attach a user object to a contact (should not be deleted)
|
# Attach a user object to a contact (should not be deleted)
|
||||||
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
contact_user, _ = Contact.objects.get_or_create(
|
||||||
|
first_name="Hank",
|
||||||
|
last_name="McFakey",
|
||||||
|
)
|
||||||
|
|
||||||
site = DraftDomain.objects.create(name="igorville.gov")
|
site = DraftDomain.objects.create(name="igorville.gov")
|
||||||
domain_request = DomainRequest.objects.create(
|
domain_request = DomainRequest.objects.create(
|
||||||
|
|
|
@ -12,99 +12,102 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.app.set_user(self.user.username)
|
self.app.set_user(self.user.username)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
lamb_chops, _ = DraftDomain.objects.get_or_create(name="lamb-chops.gov")
|
lamb_chops, _ = DraftDomain.objects.get_or_create(name="lamb-chops.gov")
|
||||||
short_ribs, _ = DraftDomain.objects.get_or_create(name="short-ribs.gov")
|
short_ribs, _ = DraftDomain.objects.get_or_create(name="short-ribs.gov")
|
||||||
beef_chuck, _ = DraftDomain.objects.get_or_create(name="beef-chuck.gov")
|
beef_chuck, _ = DraftDomain.objects.get_or_create(name="beef-chuck.gov")
|
||||||
stew_beef, _ = DraftDomain.objects.get_or_create(name="stew-beef.gov")
|
stew_beef, _ = DraftDomain.objects.get_or_create(name="stew-beef.gov")
|
||||||
|
|
||||||
# Create domain requests for the user
|
# Create domain requests for the user
|
||||||
self.domain_requests = [
|
cls.domain_requests = [
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=cls.user,
|
||||||
requested_domain=lamb_chops,
|
requested_domain=lamb_chops,
|
||||||
submission_date="2024-01-01",
|
submission_date="2024-01-01",
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
created_at="2024-01-01",
|
created_at="2024-01-01",
|
||||||
),
|
),
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=cls.user,
|
||||||
requested_domain=short_ribs,
|
requested_domain=short_ribs,
|
||||||
submission_date="2024-02-01",
|
submission_date="2024-02-01",
|
||||||
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
created_at="2024-02-01",
|
created_at="2024-02-01",
|
||||||
),
|
),
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=cls.user,
|
||||||
requested_domain=beef_chuck,
|
requested_domain=beef_chuck,
|
||||||
submission_date="2024-03-01",
|
submission_date="2024-03-01",
|
||||||
status=DomainRequest.DomainRequestStatus.REJECTED,
|
status=DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
created_at="2024-03-01",
|
created_at="2024-03-01",
|
||||||
),
|
),
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=cls.user,
|
||||||
requested_domain=stew_beef,
|
requested_domain=stew_beef,
|
||||||
submission_date="2024-04-01",
|
submission_date="2024-04-01",
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
created_at="2024-04-01",
|
created_at="2024-04-01",
|
||||||
),
|
),
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=cls.user,
|
||||||
requested_domain=None,
|
requested_domain=None,
|
||||||
submission_date="2024-05-01",
|
submission_date="2024-05-01",
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
created_at="2024-05-01",
|
created_at="2024-05-01",
|
||||||
),
|
),
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=cls.user,
|
||||||
requested_domain=None,
|
requested_domain=None,
|
||||||
submission_date="2024-06-01",
|
submission_date="2024-06-01",
|
||||||
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
created_at="2024-06-01",
|
created_at="2024-06-01",
|
||||||
),
|
),
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=cls.user,
|
||||||
requested_domain=None,
|
requested_domain=None,
|
||||||
submission_date="2024-07-01",
|
submission_date="2024-07-01",
|
||||||
status=DomainRequest.DomainRequestStatus.REJECTED,
|
status=DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
created_at="2024-07-01",
|
created_at="2024-07-01",
|
||||||
),
|
),
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=cls.user,
|
||||||
requested_domain=None,
|
requested_domain=None,
|
||||||
submission_date="2024-08-01",
|
submission_date="2024-08-01",
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
created_at="2024-08-01",
|
created_at="2024-08-01",
|
||||||
),
|
),
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=cls.user,
|
||||||
requested_domain=None,
|
requested_domain=None,
|
||||||
submission_date="2024-09-01",
|
submission_date="2024-09-01",
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
created_at="2024-09-01",
|
created_at="2024-09-01",
|
||||||
),
|
),
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=cls.user,
|
||||||
requested_domain=None,
|
requested_domain=None,
|
||||||
submission_date="2024-10-01",
|
submission_date="2024-10-01",
|
||||||
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
created_at="2024-10-01",
|
created_at="2024-10-01",
|
||||||
),
|
),
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=cls.user,
|
||||||
requested_domain=None,
|
requested_domain=None,
|
||||||
submission_date="2024-11-01",
|
submission_date="2024-11-01",
|
||||||
status=DomainRequest.DomainRequestStatus.REJECTED,
|
status=DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
created_at="2024-11-01",
|
created_at="2024-11-01",
|
||||||
),
|
),
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=cls.user,
|
||||||
requested_domain=None,
|
requested_domain=None,
|
||||||
submission_date="2024-11-02",
|
submission_date="2024-11-02",
|
||||||
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||||
created_at="2024-11-02",
|
created_at="2024-11-02",
|
||||||
),
|
),
|
||||||
DomainRequest.objects.create(
|
DomainRequest.objects.create(
|
||||||
creator=self.user,
|
creator=cls.user,
|
||||||
requested_domain=None,
|
requested_domain=None,
|
||||||
submission_date="2024-12-01",
|
submission_date="2024-12-01",
|
||||||
status=DomainRequest.DomainRequestStatus.APPROVED,
|
status=DomainRequest.DomainRequestStatus.APPROVED,
|
||||||
|
@ -112,9 +115,11 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def tearDown(self):
|
@classmethod
|
||||||
super().tearDown()
|
def tearDownClass(cls):
|
||||||
|
super().tearDownClass()
|
||||||
DomainRequest.objects.all().delete()
|
DomainRequest.objects.all().delete()
|
||||||
|
DraftDomain.objects.all().delete()
|
||||||
|
|
||||||
def test_get_domain_requests_json_authenticated(self):
|
def test_get_domain_requests_json_authenticated(self):
|
||||||
"""Test that domain requests are returned properly for an authenticated user."""
|
"""Test that domain requests are returned properly for an authenticated user."""
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -49,8 +49,10 @@ class AnalyticsView(View):
|
||||||
"domain__permissions__isnull": False,
|
"domain__permissions__isnull": False,
|
||||||
"domain__first_ready__lte": end_date_formatted,
|
"domain__first_ready__lte": end_date_formatted,
|
||||||
}
|
}
|
||||||
managed_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_managed_domains_start_date)
|
managed_domains_sliced_at_start_date = csv_export.DomainExport.get_sliced_domains(
|
||||||
managed_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_managed_domains_end_date)
|
filter_managed_domains_start_date
|
||||||
|
)
|
||||||
|
managed_domains_sliced_at_end_date = csv_export.DomainExport.get_sliced_domains(filter_managed_domains_end_date)
|
||||||
|
|
||||||
filter_unmanaged_domains_start_date = {
|
filter_unmanaged_domains_start_date = {
|
||||||
"domain__permissions__isnull": True,
|
"domain__permissions__isnull": True,
|
||||||
|
@ -60,8 +62,12 @@ class AnalyticsView(View):
|
||||||
"domain__permissions__isnull": True,
|
"domain__permissions__isnull": True,
|
||||||
"domain__first_ready__lte": end_date_formatted,
|
"domain__first_ready__lte": end_date_formatted,
|
||||||
}
|
}
|
||||||
unmanaged_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_unmanaged_domains_start_date)
|
unmanaged_domains_sliced_at_start_date = csv_export.DomainExport.get_sliced_domains(
|
||||||
unmanaged_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_unmanaged_domains_end_date)
|
filter_unmanaged_domains_start_date
|
||||||
|
)
|
||||||
|
unmanaged_domains_sliced_at_end_date = csv_export.DomainExport.get_sliced_domains(
|
||||||
|
filter_unmanaged_domains_end_date
|
||||||
|
)
|
||||||
|
|
||||||
filter_ready_domains_start_date = {
|
filter_ready_domains_start_date = {
|
||||||
"domain__state__in": [models.Domain.State.READY],
|
"domain__state__in": [models.Domain.State.READY],
|
||||||
|
@ -71,8 +77,8 @@ class AnalyticsView(View):
|
||||||
"domain__state__in": [models.Domain.State.READY],
|
"domain__state__in": [models.Domain.State.READY],
|
||||||
"domain__first_ready__lte": end_date_formatted,
|
"domain__first_ready__lte": end_date_formatted,
|
||||||
}
|
}
|
||||||
ready_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_ready_domains_start_date)
|
ready_domains_sliced_at_start_date = csv_export.DomainExport.get_sliced_domains(filter_ready_domains_start_date)
|
||||||
ready_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_ready_domains_end_date)
|
ready_domains_sliced_at_end_date = csv_export.DomainExport.get_sliced_domains(filter_ready_domains_end_date)
|
||||||
|
|
||||||
filter_deleted_domains_start_date = {
|
filter_deleted_domains_start_date = {
|
||||||
"domain__state__in": [models.Domain.State.DELETED],
|
"domain__state__in": [models.Domain.State.DELETED],
|
||||||
|
@ -82,8 +88,10 @@ class AnalyticsView(View):
|
||||||
"domain__state__in": [models.Domain.State.DELETED],
|
"domain__state__in": [models.Domain.State.DELETED],
|
||||||
"domain__deleted__lte": end_date_formatted,
|
"domain__deleted__lte": end_date_formatted,
|
||||||
}
|
}
|
||||||
deleted_domains_sliced_at_start_date = csv_export.get_sliced_domains(filter_deleted_domains_start_date)
|
deleted_domains_sliced_at_start_date = csv_export.DomainExport.get_sliced_domains(
|
||||||
deleted_domains_sliced_at_end_date = csv_export.get_sliced_domains(filter_deleted_domains_end_date)
|
filter_deleted_domains_start_date
|
||||||
|
)
|
||||||
|
deleted_domains_sliced_at_end_date = csv_export.DomainExport.get_sliced_domains(filter_deleted_domains_end_date)
|
||||||
|
|
||||||
filter_requests_start_date = {
|
filter_requests_start_date = {
|
||||||
"created_at__lte": start_date_formatted,
|
"created_at__lte": start_date_formatted,
|
||||||
|
@ -91,8 +99,8 @@ class AnalyticsView(View):
|
||||||
filter_requests_end_date = {
|
filter_requests_end_date = {
|
||||||
"created_at__lte": end_date_formatted,
|
"created_at__lte": end_date_formatted,
|
||||||
}
|
}
|
||||||
requests_sliced_at_start_date = csv_export.get_sliced_requests(filter_requests_start_date)
|
requests_sliced_at_start_date = csv_export.DomainRequestExport.get_sliced_requests(filter_requests_start_date)
|
||||||
requests_sliced_at_end_date = csv_export.get_sliced_requests(filter_requests_end_date)
|
requests_sliced_at_end_date = csv_export.DomainRequestExport.get_sliced_requests(filter_requests_end_date)
|
||||||
|
|
||||||
filter_submitted_requests_start_date = {
|
filter_submitted_requests_start_date = {
|
||||||
"status": models.DomainRequest.DomainRequestStatus.SUBMITTED,
|
"status": models.DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||||
|
@ -102,8 +110,12 @@ class AnalyticsView(View):
|
||||||
"status": models.DomainRequest.DomainRequestStatus.SUBMITTED,
|
"status": models.DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||||
"submission_date__lte": end_date_formatted,
|
"submission_date__lte": end_date_formatted,
|
||||||
}
|
}
|
||||||
submitted_requests_sliced_at_start_date = csv_export.get_sliced_requests(filter_submitted_requests_start_date)
|
submitted_requests_sliced_at_start_date = csv_export.DomainRequestExport.get_sliced_requests(
|
||||||
submitted_requests_sliced_at_end_date = csv_export.get_sliced_requests(filter_submitted_requests_end_date)
|
filter_submitted_requests_start_date
|
||||||
|
)
|
||||||
|
submitted_requests_sliced_at_end_date = csv_export.DomainRequestExport.get_sliced_requests(
|
||||||
|
filter_submitted_requests_end_date
|
||||||
|
)
|
||||||
|
|
||||||
context = dict(
|
context = dict(
|
||||||
# Generate a dictionary of context variables that are common across all admin templates
|
# Generate a dictionary of context variables that are common across all admin templates
|
||||||
|
@ -142,7 +154,7 @@ class ExportDataType(View):
|
||||||
# match the CSV example with all the fields
|
# match the CSV example with all the fields
|
||||||
response = HttpResponse(content_type="text/csv")
|
response = HttpResponse(content_type="text/csv")
|
||||||
response["Content-Disposition"] = 'attachment; filename="domains-by-type.csv"'
|
response["Content-Disposition"] = 'attachment; filename="domains-by-type.csv"'
|
||||||
csv_export.export_data_type_to_csv(response)
|
csv_export.DomainDataType.export_data_to_csv(response)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@ -151,7 +163,7 @@ class ExportDataFull(View):
|
||||||
# Smaller export based on 1
|
# Smaller export based on 1
|
||||||
response = HttpResponse(content_type="text/csv")
|
response = HttpResponse(content_type="text/csv")
|
||||||
response["Content-Disposition"] = 'attachment; filename="current-full.csv"'
|
response["Content-Disposition"] = 'attachment; filename="current-full.csv"'
|
||||||
csv_export.export_data_full_to_csv(response)
|
csv_export.DomainDataFull.export_data_to_csv(response)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@ -160,7 +172,7 @@ class ExportDataFederal(View):
|
||||||
# Federal only
|
# Federal only
|
||||||
response = HttpResponse(content_type="text/csv")
|
response = HttpResponse(content_type="text/csv")
|
||||||
response["Content-Disposition"] = 'attachment; filename="current-federal.csv"'
|
response["Content-Disposition"] = 'attachment; filename="current-federal.csv"'
|
||||||
csv_export.export_data_federal_to_csv(response)
|
csv_export.DomainDataFederal.export_data_to_csv(response)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@ -171,63 +183,51 @@ class ExportDomainRequestDataFull(View):
|
||||||
"""Returns a content disposition response for current-full-domain-request.csv"""
|
"""Returns a content disposition response for current-full-domain-request.csv"""
|
||||||
response = HttpResponse(content_type="text/csv")
|
response = HttpResponse(content_type="text/csv")
|
||||||
response["Content-Disposition"] = 'attachment; filename="current-full-domain-request.csv"'
|
response["Content-Disposition"] = 'attachment; filename="current-full-domain-request.csv"'
|
||||||
csv_export.DomainRequestExport.export_full_domain_request_report(response)
|
csv_export.DomainRequestDataFull.export_data_to_csv(response)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class ExportDataDomainsGrowth(View):
|
class ExportDataDomainsGrowth(View):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
# Get start_date and end_date from the request's GET parameters
|
|
||||||
# #999: not needed if we switch to django forms
|
|
||||||
start_date = request.GET.get("start_date", "")
|
start_date = request.GET.get("start_date", "")
|
||||||
end_date = request.GET.get("end_date", "")
|
end_date = request.GET.get("end_date", "")
|
||||||
|
|
||||||
response = HttpResponse(content_type="text/csv")
|
response = HttpResponse(content_type="text/csv")
|
||||||
response["Content-Disposition"] = f'attachment; filename="domain-growth-report-{start_date}-to-{end_date}.csv"'
|
response["Content-Disposition"] = f'attachment; filename="domain-growth-report-{start_date}-to-{end_date}.csv"'
|
||||||
# For #999: set export_data_domain_growth_to_csv to return the resulting queryset, which we can then use
|
csv_export.DomainGrowth.export_data_to_csv(response, start_date, end_date)
|
||||||
# in context to display this data in the template.
|
|
||||||
csv_export.export_data_domain_growth_to_csv(response, start_date, end_date)
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class ExportDataRequestsGrowth(View):
|
class ExportDataRequestsGrowth(View):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
# Get start_date and end_date from the request's GET parameters
|
|
||||||
# #999: not needed if we switch to django forms
|
|
||||||
start_date = request.GET.get("start_date", "")
|
start_date = request.GET.get("start_date", "")
|
||||||
end_date = request.GET.get("end_date", "")
|
end_date = request.GET.get("end_date", "")
|
||||||
|
|
||||||
response = HttpResponse(content_type="text/csv")
|
response = HttpResponse(content_type="text/csv")
|
||||||
response["Content-Disposition"] = f'attachment; filename="requests-{start_date}-to-{end_date}.csv"'
|
response["Content-Disposition"] = f'attachment; filename="requests-{start_date}-to-{end_date}.csv"'
|
||||||
# For #999: set export_data_domain_growth_to_csv to return the resulting queryset, which we can then use
|
csv_export.DomainRequestGrowth.export_data_to_csv(response, start_date, end_date)
|
||||||
# in context to display this data in the template.
|
|
||||||
csv_export.DomainRequestExport.export_data_requests_growth_to_csv(response, start_date, end_date)
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class ExportDataManagedDomains(View):
|
class ExportDataManagedDomains(View):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
# Get start_date and end_date from the request's GET parameters
|
|
||||||
# #999: not needed if we switch to django forms
|
|
||||||
start_date = request.GET.get("start_date", "")
|
start_date = request.GET.get("start_date", "")
|
||||||
end_date = request.GET.get("end_date", "")
|
end_date = request.GET.get("end_date", "")
|
||||||
response = HttpResponse(content_type="text/csv")
|
response = HttpResponse(content_type="text/csv")
|
||||||
response["Content-Disposition"] = f'attachment; filename="managed-domains-{start_date}-to-{end_date}.csv"'
|
response["Content-Disposition"] = f'attachment; filename="managed-domains-{start_date}-to-{end_date}.csv"'
|
||||||
csv_export.export_data_managed_domains_to_csv(response, start_date, end_date)
|
csv_export.DomainManaged.export_data_to_csv(response, start_date, end_date)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class ExportDataUnmanagedDomains(View):
|
class ExportDataUnmanagedDomains(View):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
# Get start_date and end_date from the request's GET parameters
|
|
||||||
# #999: not needed if we switch to django forms
|
|
||||||
start_date = request.GET.get("start_date", "")
|
start_date = request.GET.get("start_date", "")
|
||||||
end_date = request.GET.get("end_date", "")
|
end_date = request.GET.get("end_date", "")
|
||||||
response = HttpResponse(content_type="text/csv")
|
response = HttpResponse(content_type="text/csv")
|
||||||
response["Content-Disposition"] = f'attachment; filename="unamanaged-domains-{start_date}-to-{end_date}.csv"'
|
response["Content-Disposition"] = f'attachment; filename="unmanaged-domains-{start_date}-to-{end_date}.csv"'
|
||||||
csv_export.export_data_unmanaged_domains_to_csv(response, start_date, end_date)
|
csv_export.DomainUnmanaged.export_data_to_csv(response, start_date, end_date)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
|
@ -40,7 +40,7 @@ from registrar.models.utility.contact_error import ContactError
|
||||||
from registrar.views.utility.permission_views import UserDomainRolePermissionDeleteView
|
from registrar.views.utility.permission_views import UserDomainRolePermissionDeleteView
|
||||||
|
|
||||||
from ..forms import (
|
from ..forms import (
|
||||||
ContactForm,
|
UserForm,
|
||||||
SeniorOfficialContactForm,
|
SeniorOfficialContactForm,
|
||||||
DomainOrgNameAddressForm,
|
DomainOrgNameAddressForm,
|
||||||
DomainAddUserForm,
|
DomainAddUserForm,
|
||||||
|
@ -573,7 +573,7 @@ class DomainYourContactInformationView(DomainFormBaseView):
|
||||||
"""Domain your contact information editing view."""
|
"""Domain your contact information editing view."""
|
||||||
|
|
||||||
template_name = "domain_your_contact_information.html"
|
template_name = "domain_your_contact_information.html"
|
||||||
form_class = ContactForm
|
form_class = UserForm
|
||||||
|
|
||||||
@waffle_flag("!profile_feature") # type: ignore
|
@waffle_flag("!profile_feature") # type: ignore
|
||||||
def dispatch(self, request, *args, **kwargs): # type: ignore
|
def dispatch(self, request, *args, **kwargs): # type: ignore
|
||||||
|
@ -582,7 +582,7 @@ class DomainYourContactInformationView(DomainFormBaseView):
|
||||||
def get_form_kwargs(self, *args, **kwargs):
|
def get_form_kwargs(self, *args, **kwargs):
|
||||||
"""Add domain_info.submitter instance to make a bound form."""
|
"""Add domain_info.submitter instance to make a bound form."""
|
||||||
form_kwargs = super().get_form_kwargs(*args, **kwargs)
|
form_kwargs = super().get_form_kwargs(*args, **kwargs)
|
||||||
form_kwargs["instance"] = self.request.user.contact
|
form_kwargs["instance"] = self.request.user
|
||||||
return form_kwargs
|
return form_kwargs
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
|
|
|
@ -812,15 +812,15 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
self.object.delete()
|
self.object.delete()
|
||||||
|
|
||||||
# Delete orphaned contacts - but only for if they are not associated with a user
|
# Delete orphaned contacts
|
||||||
Contact.objects.filter(id__in=contacts_to_delete, user=None).delete()
|
Contact.objects.filter(id__in=contacts_to_delete).delete()
|
||||||
|
|
||||||
# After a delete occurs, do a second sweep on any returned duplicates.
|
# After a delete occurs, do a second sweep on any returned duplicates.
|
||||||
# This determines if any of these three fields share a contact, which is used for
|
# This determines if any of these three fields share a contact, which is used for
|
||||||
# the edge case where the same user may be an SO, and a submitter, for example.
|
# the edge case where the same user may be an SO, and a submitter, for example.
|
||||||
if len(duplicates) > 0:
|
if len(duplicates) > 0:
|
||||||
duplicates_to_delete, _ = self._get_orphaned_contacts(domain_request, check_db=True)
|
duplicates_to_delete, _ = self._get_orphaned_contacts(domain_request, check_db=True)
|
||||||
Contact.objects.filter(id__in=duplicates_to_delete, user=None).delete()
|
Contact.objects.filter(id__in=duplicates_to_delete).delete()
|
||||||
|
|
||||||
# Return a 200 response with an empty body
|
# Return a 200 response with an empty body
|
||||||
return HttpResponse(status=200)
|
return HttpResponse(status=200)
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
"""Views for a User Profile.
|
"""Views for a User Profile.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
@ -9,9 +8,6 @@ from django.http import QueryDict
|
||||||
from django.views.generic.edit import FormMixin
|
from django.views.generic.edit import FormMixin
|
||||||
from registrar.forms.user_profile import UserProfileForm, FinishSetupProfileForm
|
from registrar.forms.user_profile import UserProfileForm, FinishSetupProfileForm
|
||||||
from django.urls import NoReverseMatch, reverse
|
from django.urls import NoReverseMatch, reverse
|
||||||
from registrar.models import (
|
|
||||||
Contact,
|
|
||||||
)
|
|
||||||
from registrar.models.user import User
|
from registrar.models.user import User
|
||||||
from registrar.models.utility.generic_helper import replace_url_queryparams
|
from registrar.models.utility.generic_helper import replace_url_queryparams
|
||||||
from registrar.views.utility.permission_views import UserProfilePermissionView
|
from registrar.views.utility.permission_views import UserProfilePermissionView
|
||||||
|
@ -25,7 +21,7 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
||||||
Base View for the User Profile. Handles getting and setting the User Profile
|
Base View for the User Profile. Handles getting and setting the User Profile
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = Contact
|
model = User
|
||||||
template_name = "profile.html"
|
template_name = "profile.html"
|
||||||
form_class = UserProfileForm
|
form_class = UserProfileForm
|
||||||
base_view_name = "user-profile"
|
base_view_name = "user-profile"
|
||||||
|
@ -109,6 +105,7 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
||||||
"""If the form is invalid, conditionally display an additional error."""
|
"""If the form is invalid, conditionally display an additional error."""
|
||||||
if hasattr(self.user, "finished_setup") and not self.user.finished_setup:
|
if hasattr(self.user, "finished_setup") and not self.user.finished_setup:
|
||||||
messages.error(self.request, "Before you can manage your domain, we need you to add contact information.")
|
messages.error(self.request, "Before you can manage your domain, we need you to add contact information.")
|
||||||
|
form.initial["redirect"] = form.data.get("redirect")
|
||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
|
@ -123,9 +120,7 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
"""Override get_object to return the logged-in user's contact"""
|
"""Override get_object to return the logged-in user's contact"""
|
||||||
self.user = self.request.user # get the logged in user
|
self.user = self.request.user # get the logged in user
|
||||||
if hasattr(self.user, "contact"): # Check if the user has a contact instance
|
return self.user
|
||||||
return self.user.contact
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class FinishProfileSetupView(UserProfileView):
|
class FinishProfileSetupView(UserProfileView):
|
||||||
|
@ -134,7 +129,7 @@ class FinishProfileSetupView(UserProfileView):
|
||||||
|
|
||||||
template_name = "finish_profile_setup.html"
|
template_name = "finish_profile_setup.html"
|
||||||
form_class = FinishSetupProfileForm
|
form_class = FinishSetupProfileForm
|
||||||
model = Contact
|
model = User
|
||||||
|
|
||||||
base_view_name = "finish-user-profile-setup"
|
base_view_name = "finish-user-profile-setup"
|
||||||
|
|
||||||
|
@ -160,11 +155,11 @@ class FinishProfileSetupView(UserProfileView):
|
||||||
# Get the current form and validate it
|
# Get the current form and validate it
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
self.redirect_page = False
|
self.redirect_page = False
|
||||||
if "contact_setup_save_button" in request.POST:
|
if "user_setup_save_button" in request.POST:
|
||||||
# Logic for when the 'Save' button is clicked, which indicates
|
# Logic for when the 'Save' button is clicked, which indicates
|
||||||
# user should stay on this page
|
# user should stay on this page
|
||||||
self.redirect_page = False
|
self.redirect_page = False
|
||||||
elif "contact_setup_submit_button" in request.POST:
|
elif "user_setup_submit_button" in request.POST:
|
||||||
# Logic for when the other button is clicked, which indicates
|
# Logic for when the other button is clicked, which indicates
|
||||||
# the user should be taken to the redirect page
|
# the user should be taken to the redirect page
|
||||||
self.redirect_page = True
|
self.redirect_page = True
|
||||||
|
|
|
@ -4,7 +4,7 @@ import abc # abstract base class
|
||||||
|
|
||||||
from django.views.generic import DetailView, DeleteView, TemplateView
|
from django.views.generic import DetailView, DeleteView, TemplateView
|
||||||
from registrar.models import Domain, DomainRequest, DomainInvitation
|
from registrar.models import Domain, DomainRequest, DomainInvitation
|
||||||
from registrar.models.contact import Contact
|
from registrar.models.user import User
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
|
|
||||||
from .mixins import (
|
from .mixins import (
|
||||||
|
@ -154,9 +154,9 @@ class UserProfilePermissionView(UserProfilePermission, DetailView, abc.ABC):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# DetailView property for what model this is viewing
|
# DetailView property for what model this is viewing
|
||||||
model = Contact
|
model = User
|
||||||
# variable name in template context for the model object
|
# variable name in template context for the model object
|
||||||
context_object_name = "contact"
|
context_object_name = "user"
|
||||||
|
|
||||||
# Abstract property enforces NotImplementedError on an attribute.
|
# Abstract property enforces NotImplementedError on an attribute.
|
||||||
@property
|
@property
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue