diff --git a/.github/ISSUE_TEMPLATE/designer-onboarding.md b/.github/ISSUE_TEMPLATE/designer-onboarding.md index 0296b653d..461850b60 100644 --- a/.github/ISSUE_TEMPLATE/designer-onboarding.md +++ b/.github/ISSUE_TEMPLATE/designer-onboarding.md @@ -56,6 +56,7 @@ By following the steps, you should have access / been added to the following: - [ ] Add onboardee to dotgov Slack channels - [ ] Add onboardee to Project Folder as a "Contributor" (or if you do not have permissions, confirm with Cameron) - If onboardee if a federal employee, add them as a "Content Manager" instead -- [ ] Once onboardee has been granted a Figma license, add them as an editor to the Figma team workspace, Otherwise, you can add them as a viewer in the meantime. +- [ ] Coordinate with Cameron to grant onboardee a Figma license +- [ ] Add onboardee as an editor to the Figma team workspace, If they do not have a license (yet), you can add them as a viewer. - [ ] Add onboardee to our team meetings - [ ] If applicable, invite onboardee to Google Analytics, Google Search Console, and Search.gov console diff --git a/docs/developer/README.md b/docs/developer/README.md index f894955e5..9421d5856 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -73,14 +73,23 @@ cp ./.env-example .env Get the secrets from Cloud.gov by running `cf env getgov-YOURSANDBOX`. More information is available in [rotate_application_secrets.md](../operations/runbooks/rotate_application_secrets.md). -## Adding user to /admin +## Getting access to /admin on all development sandboxes (also referred to as "adding to fixtures") -The endpoint /admin can be used to view and manage site content, including but not limited to user information and the list of current applications in the database. To be able to view and use /admin locally: +The endpoint /admin can be used to view and manage site content, including but not limited to user information and the list of current applications in the database. However, access to this is limited to analysts and full-access users with regular domain requestors and domain managers not being able to see this page. + +While on production (the sandbox referred to as `stable`), an existing analyst or full-access user typically grants access /admin as part of onboarding ([see these instructions](../django-admin/roles.md)), doing this for all development sandboxes is very time consuming. Instead, to get access to /admin on all development sandboxes and when developing code locally, refer to the following sections depending on what level of user access you desire. + + +### Adding full-access user to /admin + + To get access to /admin on every non-production sandbox and to use /admin in local development, do the following: 1. Login via login.gov 2. Go to the home page and make sure you can see the part where you can submit a domain request -3. Go to /admin and it will tell you that UUID is not authorized, copy that UUID for use in 4 -4. in src/registrar/fixtures_users.py add to the `ADMINS` list in that file by adding your UUID as your username along with your first and last name. See below: +3. Go to /admin and it will tell you that your UUID is not authorized (it shows a very long string, this is your UUID). Copy that UUID for use in 4. +4. (Designers) Message in #getgov-dev that you need access to admin as a `superuser` and send them this UUID along with your desired email address. Please see the "Adding an Analyst to /admin" section below to complete similiar steps if you also desire an `analyst` user account. Engineers will handle the remaining steps for designers, stop here. + +(Engineers) In src/registrar/fixtures_users.py add to the `ADMINS` list in that file by adding your UUID as your username along with your first and last name. See below: ``` ADMINS = [ @@ -93,16 +102,18 @@ The endpoint /admin can be used to view and manage site content, including but n ] ``` -5. In the browser, navigate to /admin. To verify that all is working correctly, under "domain requests" you should see fake domains with various fake statuses. -6. Add an optional email key/value pair +5. (Engineers) In the browser, navigate to /admin. To verify that all is working correctly, under "domain requests" you should see fake domains with various fake statuses. +6. (Engineers) Add an optional email key/value pair -### Adding an Analyst to /admin +### Adding an analyst-level user to /admin Analysts are a variant of the admin role with limited permissions. The process for adding an Analyst is much the same as adding an admin: 1. Login via login.gov (if you already exist as an admin, you will need to create a separate login.gov account for this: i.e. first.last+1@email.com) 2. Go to the home page and make sure you can see the part where you can submit a domain request 3. Go to /admin and it will tell you that UUID is not authorized, copy that UUID for use in 4 (this will be a different UUID than the one obtained from creating an admin) -4. in src/registrar/fixtures_users.py add to the `STAFF` list in that file by adding your UUID as your username along with your first and last name. See below: +4. (Designers) Message in #getgov-dev that you need access to admin as a `superuser` and send them this UUID along with your desired email address. Engineers will handle the remaining steps for designers, stop here. + +5. (Engineers) In src/registrar/fixtures_users.py add to the `STAFF` list in that file by adding your UUID as your username along with your first and last name. See below: ``` STAFF = [ @@ -115,10 +126,11 @@ Analysts are a variant of the admin role with limited permissions. The process f ] ``` -5. In the browser, navigate to /admin. To verify that all is working correctly, verify that you can only see a sub-section of the modules and some are set to view-only. -6. Add an optional email key/value pair +5. (Engineers) In the browser, navigate to /admin. To verify that all is working correctly, verify that you can only see a sub-section of the modules and some are set to view-only. +6. (Engineers) Add an optional email key/value pair Do note that if you wish to have both an analyst and admin account, append `-Analyst` to your first and last name, or use a completely different first/last name to avoid confusion. Example: `Bob-Analyst` + ## Adding to CODEOWNERS (optional) The CODEOWNERS file sets the tagged individuals as default reviewers on any Pull Request that changes files that they are marked as owners of. diff --git a/docs/django-admin/roles.md b/docs/django-admin/roles.md index c527bbfa5..4e74a857f 100644 --- a/docs/django-admin/roles.md +++ b/docs/django-admin/roles.md @@ -9,7 +9,7 @@ our `user_group` model and run in a migration. For more details, refer to the [user group model](../../src/registrar/models/user_group.py). -## Adding a user as analyst or granting full access via django-admin +## Adding a user as analyst or granting full access via django-admin (/admin) If a new team member has joined, then they will need to be granted analyst (`cisa_analysts_group`) or full access (`full_access_group`) permissions in order to view the admin pages. These admin pages are the ones found at manage.get.gov/admin. To do this, do the following: @@ -21,7 +21,7 @@ To do this, do the following: 5. (Optional) If the user needs access to django admin (such as an analyst), then you will also need to make sure "Staff Status" is checked. This can be found in the same `User Permissions` section right below the checkbox for `Active`. 6. Click `Save` to apply all changes. -## Removing a user group permission via django-admin +## Removing a user group permission via django-admin (/admin) If an employee was given the wrong permissions or has had a change in roles that subsequently requires a permission change, then their permissions should be updated in django-admin. Much like in the previous section you can accomplish this by doing the following: diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 264257b35..25ac8a9d6 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -779,6 +779,46 @@ class WebsiteAdmin(ListHeaderAdmin): ] search_help_text = "Search by website." + def get_model_perms(self, request): + """ + Return empty perms dict thus hiding the model from admin index. + """ + superuser_perm = request.user.has_perm("registrar.full_access_permission") + analyst_perm = request.user.has_perm("registrar.analyst_access_permission") + if analyst_perm and not superuser_perm: + return {} + return super().get_model_perms(request) + + def has_change_permission(self, request, obj=None): + """ + Allow analysts to access the change form directly via URL. + """ + superuser_perm = request.user.has_perm("registrar.full_access_permission") + analyst_perm = request.user.has_perm("registrar.analyst_access_permission") + if analyst_perm and not superuser_perm: + return True + return super().has_change_permission(request, obj) + + def response_change(self, request, obj): + """ + Override to redirect users back to the previous page after saving. + """ + superuser_perm = request.user.has_perm("registrar.full_access_permission") + analyst_perm = request.user.has_perm("registrar.analyst_access_permission") + return_path = request.GET.get("return_path") + + # First, call the super method to perform the standard operations and capture the response + response = super().response_change(request, obj) + + # Don't redirect to the website page on save if the user is an analyst. + # Rather, just redirect back to the originating page. + if (analyst_perm and not superuser_perm) and return_path: + # Redirect to the return path if it exists + return HttpResponseRedirect(return_path) + + # If no redirection is needed, return the original response + return response + class UserDomainRoleAdmin(ListHeaderAdmin): """Custom user domain role admin class.""" @@ -1468,7 +1508,10 @@ class DomainInformationInline(admin.StackedInline): def has_change_permission(self, request, obj=None): """Custom has_change_permission override so that we can specify that analysts can edit this through this inline, but not through the model normally""" - if request.user.has_perm("registrar.analyst_access_permission"): + + superuser_perm = request.user.has_perm("registrar.full_access_permission") + analyst_perm = request.user.has_perm("registrar.analyst_access_permission") + if analyst_perm and not superuser_perm: return True return super().has_change_permission(request, obj) @@ -1897,6 +1940,46 @@ class DraftDomainAdmin(ListHeaderAdmin): # in autocomplete_fields for user ordering = ["name"] + def get_model_perms(self, request): + """ + Return empty perms dict thus hiding the model from admin index. + """ + superuser_perm = request.user.has_perm("registrar.full_access_permission") + analyst_perm = request.user.has_perm("registrar.analyst_access_permission") + if analyst_perm and not superuser_perm: + return {} + return super().get_model_perms(request) + + def has_change_permission(self, request, obj=None): + """ + Allow analysts to access the change form directly via URL. + """ + superuser_perm = request.user.has_perm("registrar.full_access_permission") + analyst_perm = request.user.has_perm("registrar.analyst_access_permission") + if analyst_perm and not superuser_perm: + return True + return super().has_change_permission(request, obj) + + def response_change(self, request, obj): + """ + Override to redirect users back to the previous page after saving. + """ + superuser_perm = request.user.has_perm("registrar.full_access_permission") + analyst_perm = request.user.has_perm("registrar.analyst_access_permission") + return_path = request.GET.get("return_path") + + # First, call the super method to perform the standard operations and capture the response + response = super().response_change(request, obj) + + # Don't redirect to the website page on save if the user is an analyst. + # Rather, just redirect back to the originating page. + if (analyst_perm and not superuser_perm) and return_path: + # Redirect to the return path if it exists + return HttpResponseRedirect(return_path) + + # If no redirection is needed, return the original response + return response + class PublicContactAdmin(ListHeaderAdmin): """Custom PublicContact admin class.""" diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index fd66754bc..4b69dc8e3 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -536,6 +536,18 @@ address.dja-address-contact-list { } } +.dja-status-list { + border-top: solid 1px var(--border-color); + margin-left: 0 !important; + padding-left: 0 !important; + padding-top: 10px; + li { + line-height: 1.5; + font-family: "Source Sans Pro Web", "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif !important; + padding-top: 0; + padding-bottom: 0; + } +} // Make the clipboard button "float" inside of the input box .admin-icon-group { diff --git a/src/registrar/migrations/0084_create_groups_v11.py b/src/registrar/migrations/0084_create_groups_v11.py new file mode 100644 index 000000000..efb587520 --- /dev/null +++ b/src/registrar/migrations/0084_create_groups_v11.py @@ -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", "0083_alter_contact_email_alter_publiccontact_email"), + ] + + operations = [ + migrations.RunPython( + create_groups, + reverse_code=migrations.RunPython.noop, + atomic=True, + ), + ] diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index 6bc20ebeb..bf35b0143 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -94,6 +94,9 @@ class Contact(TimeStampedModel): 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): + 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) diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index bf904a044..2688ef57f 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -9,6 +9,7 @@ from .domain_invitation import DomainInvitation from .transition_domain import TransitionDomain from .verified_by_staff import VerifiedByStaff from .domain import Domain +from .domain_request import DomainRequest from phonenumber_field.modelfields import PhoneNumberField # type: ignore @@ -67,6 +68,33 @@ class User(AbstractUser): def is_restricted(self): return self.status == self.RESTRICTED + def get_approved_domains_count(self): + """Return count of approved domains""" + allowed_states = [Domain.State.UNKNOWN, Domain.State.DNS_NEEDED, Domain.State.READY, Domain.State.ON_HOLD] + approved_domains_count = self.domains.filter(state__in=allowed_states).count() + return approved_domains_count + + def get_active_requests_count(self): + """Return count of active requests""" + allowed_states = [ + DomainRequest.DomainRequestStatus.SUBMITTED, + DomainRequest.DomainRequestStatus.IN_REVIEW, + DomainRequest.DomainRequestStatus.ACTION_NEEDED, + ] + active_requests_count = self.domain_requests_created.filter(status__in=allowed_states).count() + return active_requests_count + + def get_rejected_requests_count(self): + """Return count of rejected requests""" + return self.domain_requests_created.filter(status=DomainRequest.DomainRequestStatus.REJECTED).count() + + def get_ineligible_requests_count(self): + """Return count of ineligible requests""" + return self.domain_requests_created.filter(status=DomainRequest.DomainRequestStatus.INELIGIBLE).count() + + def has_contact_info(self): + return bool(self.contact.title or self.contact.email or self.contact.phone) + @classmethod def needs_identity_verification(cls, email, uuid): """A method used by our oidc classes to test whether a user needs email/uuid verification diff --git a/src/registrar/models/user_group.py b/src/registrar/models/user_group.py index 82179f8dc..76657fe29 100644 --- a/src/registrar/models/user_group.py +++ b/src/registrar/models/user_group.py @@ -41,11 +41,6 @@ class UserGroup(Group): "model": "domain", "permissions": ["view_domain"], }, - { - "app_label": "registrar", - "model": "draftdomain", - "permissions": ["change_draftdomain"], - }, { "app_label": "registrar", "model": "user", @@ -56,11 +51,6 @@ class UserGroup(Group): "model": "domaininvitation", "permissions": ["add_domaininvitation", "view_domaininvitation"], }, - { - "app_label": "registrar", - "model": "website", - "permissions": ["change_website"], - }, { "app_label": "registrar", "model": "userdomainrole", diff --git a/src/registrar/templates/django/admin/includes/contact_detail_list.html b/src/registrar/templates/django/admin/includes/contact_detail_list.html index 8ad6fb96d..0ac9c4c49 100644 --- a/src/registrar/templates/django/admin/includes/contact_detail_list.html +++ b/src/registrar/templates/django/admin/includes/contact_detail_list.html @@ -1,6 +1,6 @@ {% load i18n static %} -
+ {% if show_formatted_name %} {% if contact.get_formatted_name %} @@ -10,7 +10,7 @@ {% endif %} {% endif %} - {% if user.title or user.contact.title or user.email or user.contact.email or user.phone or user.contact.phone %} + {% if user.has_contact_info %} {# Title #} {% if user.title or user.contact.title %} {% if user.contact.title %} diff --git a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html index 4b0b36391..eff73f828 100644 --- a/src/registrar/templates/django/admin/includes/detail_table_fieldset.html +++ b/src/registrar/templates/django/admin/includes/detail_table_fieldset.html @@ -27,6 +27,10 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html) {% endif %} + {% elif field.field.name == "requested_domain" %} + {% with current_path=request.get_full_path %} + {{ original.requested_domain }} + {% endwith%} {% elif field.field.name == "current_websites" %} {% comment %} The "website" model is essentially just a text field. @@ -49,9 +53,11 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html) {% elif field.field.name == "alternative_domains" %}We received your .gov domain request. Our next step is to review your request. This usually takes 20 business days. We’ll email you if we have questions and when we complete our review. Contact us with any questions.
+We received your .gov domain request. Our next step is to review your request. This usually takes 30 business days. We’ll email you if we have questions and when we complete our review. Contact us with any questions.