mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-05 17:28:31 +02:00
new view, transfer and delete logic, modal, combobox
This commit is contained in:
parent
43097ce69d
commit
a7c801739d
9 changed files with 492 additions and 55 deletions
|
@ -41,6 +41,8 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.urls import path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -172,40 +172,39 @@ function addOrRemoveSessionBoolean(name, add){
|
|||
** To perform data operations on this - we need to use jQuery rather than vanilla js.
|
||||
*/
|
||||
(function (){
|
||||
let selector = django.jQuery("#id_investigator")
|
||||
let assignSelfButton = document.querySelector("#investigator__assign_self");
|
||||
if (!selector || !assignSelfButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
let currentUserId = assignSelfButton.getAttribute("data-user-id");
|
||||
let currentUserName = assignSelfButton.getAttribute("data-user-name");
|
||||
if (!currentUserId || !currentUserName){
|
||||
console.error("Could not assign current user: no values found.")
|
||||
return;
|
||||
}
|
||||
|
||||
// Hook a click listener to the "Assign to me" button.
|
||||
// Logic borrowed from here: https://select2.org/programmatic-control/add-select-clear-items#create-if-not-exists
|
||||
assignSelfButton.addEventListener("click", function() {
|
||||
if (selector.find(`option[value='${currentUserId}']`).length) {
|
||||
// Select the value that is associated with the current user.
|
||||
selector.val(currentUserId).trigger("change");
|
||||
} else {
|
||||
// Create a DOM Option that matches the desired user. Then append it and select it.
|
||||
let userOption = new Option(currentUserName, currentUserId, true, true);
|
||||
selector.append(userOption).trigger("change");
|
||||
if (document.getElementById("id_investigator") && django && django.jQuery) {
|
||||
let selector = django.jQuery("#id_investigator")
|
||||
let assignSelfButton = document.querySelector("#investigator__assign_self");
|
||||
if (!selector || !assignSelfButton) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Listen to any change events, and hide the parent container if investigator has a value.
|
||||
selector.on('change', function() {
|
||||
// The parent container has display type flex.
|
||||
assignSelfButton.parentElement.style.display = this.value === currentUserId ? "none" : "flex";
|
||||
});
|
||||
|
||||
|
||||
let currentUserId = assignSelfButton.getAttribute("data-user-id");
|
||||
let currentUserName = assignSelfButton.getAttribute("data-user-name");
|
||||
if (!currentUserId || !currentUserName){
|
||||
console.error("Could not assign current user: no values found.")
|
||||
return;
|
||||
}
|
||||
|
||||
// Hook a click listener to the "Assign to me" button.
|
||||
// Logic borrowed from here: https://select2.org/programmatic-control/add-select-clear-items#create-if-not-exists
|
||||
assignSelfButton.addEventListener("click", function() {
|
||||
if (selector.find(`option[value='${currentUserId}']`).length) {
|
||||
// Select the value that is associated with the current user.
|
||||
selector.val(currentUserId).trigger("change");
|
||||
} else {
|
||||
// Create a DOM Option that matches the desired user. Then append it and select it.
|
||||
let userOption = new Option(currentUserName, currentUserId, true, true);
|
||||
selector.append(userOption).trigger("change");
|
||||
}
|
||||
});
|
||||
|
||||
// Listen to any change events, and hide the parent container if investigator has a value.
|
||||
selector.on('change', function() {
|
||||
// The parent container has display type flex.
|
||||
assignSelfButton.parentElement.style.display = this.value === currentUserId ? "none" : "flex";
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
/** An IIFE for pages in DjangoAdmin that use a clipboard button
|
||||
|
@ -215,7 +214,6 @@ function addOrRemoveSessionBoolean(name, add){
|
|||
function copyToClipboardAndChangeIcon(button) {
|
||||
// Assuming the input is the previous sibling of the button
|
||||
let input = button.previousElementSibling;
|
||||
let userId = input.getAttribute("user-id")
|
||||
// Copy input value to clipboard
|
||||
if (input) {
|
||||
navigator.clipboard.writeText(input.value).then(function() {
|
||||
|
|
|
@ -120,7 +120,7 @@ html[data-theme="light"] {
|
|||
body.dashboard,
|
||||
body.change-list,
|
||||
body.change-form,
|
||||
.analytics {
|
||||
.custom-admin-template, dt {
|
||||
color: var(--body-fg);
|
||||
}
|
||||
.usa-table td {
|
||||
|
@ -149,7 +149,7 @@ html[data-theme="dark"] {
|
|||
body.dashboard,
|
||||
body.change-list,
|
||||
body.change-form,
|
||||
.analytics {
|
||||
.custom-admin-template, dt {
|
||||
color: var(--body-fg);
|
||||
}
|
||||
.usa-table td {
|
||||
|
@ -160,7 +160,7 @@ html[data-theme="dark"] {
|
|||
// Remove when dark mode successfully applies to Django delete page.
|
||||
.delete-confirmation .content a:not(.button) {
|
||||
color: color('primary');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -364,14 +364,40 @@ input.admin-confirm-button {
|
|||
list-style-type: none;
|
||||
line-height: normal;
|
||||
}
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 10px 8px;
|
||||
line-height: normal;
|
||||
}
|
||||
a.button:active, a.button:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
// This block resolves some of the issues we're seeing on buttons due to css
|
||||
// conflicts between DJ and USWDS
|
||||
a.button,
|
||||
.usa-button {
|
||||
display: inline-block;
|
||||
padding: 10px 15px;
|
||||
font-size: 14px;
|
||||
line-height: 16.1px;
|
||||
font-kerning: auto;
|
||||
font-family: inherit;
|
||||
font-weight: normal;
|
||||
}
|
||||
.button svg,
|
||||
.button span,
|
||||
.usa-button svg,
|
||||
.usa-button span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.usa-button:not(.usa-button--unstyled, .usa-button--outline, .usa-modal__close, .usa-button--secondary) {
|
||||
background: var(--button-bg);
|
||||
}
|
||||
.usa-button span {
|
||||
font-size: 14px;
|
||||
}
|
||||
.usa-button:not(.usa-button--unstyled, .usa-button--outline, .usa-modal__close, .usa-button--secondary):hover {
|
||||
background: var(--button-hover-bg);
|
||||
}
|
||||
a.button:active, a.button:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
.usa-modal {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.module--custom {
|
||||
|
@ -732,7 +758,7 @@ div.dja__model-description{
|
|||
|
||||
li {
|
||||
list-style-type: disc;
|
||||
font-family: Source Sans Pro Web,Helvetica Neue,Helvetica,Roboto,Arial,sans-serif;
|
||||
font-family: family('sans');
|
||||
}
|
||||
|
||||
a, a:link, a:visited {
|
||||
|
@ -852,3 +878,9 @@ ul.add-list-reset {
|
|||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
// Fix the combobox when deployed outside admin (eg user transfer)
|
||||
.submit-row .select2,
|
||||
.submit-row .select2 span {
|
||||
margin-top: 0;
|
||||
}
|
|
@ -357,13 +357,14 @@ CSP_FORM_ACTION = allowed_sources
|
|||
# and inline with a nonce, as well as allowing connections back to their domain.
|
||||
# Note: If needed, we can embed chart.js instead of using the CDN
|
||||
CSP_DEFAULT_SRC = ("'self'",)
|
||||
CSP_STYLE_SRC = ["'self'", "https://www.ssa.gov/accessibility/andi/andi.css"]
|
||||
CSP_STYLE_SRC = ["'self'", "https://www.ssa.gov/accessibility/andi/andi.css", "https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css"]
|
||||
CSP_SCRIPT_SRC_ELEM = [
|
||||
"'self'",
|
||||
"https://www.googletagmanager.com/",
|
||||
"https://cdn.jsdelivr.net/npm/chart.js",
|
||||
"https://www.ssa.gov",
|
||||
"https://ajax.googleapis.com",
|
||||
"https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"
|
||||
]
|
||||
CSP_CONNECT_SRC = ["'self'", "https://www.google-analytics.com/", "https://www.ssa.gov/accessibility/andi/andi.js"]
|
||||
CSP_INCLUDE_NONCE_IN = ["script-src-elem", "style-src"]
|
||||
|
|
|
@ -24,6 +24,7 @@ from registrar.views.report_views import (
|
|||
|
||||
from registrar.views.domain_request import Step
|
||||
from registrar.views.domain_requests_json import get_domain_requests_json
|
||||
from registrar.views.transfer_user import TransferUserView
|
||||
from registrar.views.utility.api_views import get_senior_official_from_federal_agency_json
|
||||
from registrar.views.domains_json import get_domains_json
|
||||
from registrar.views.utility import always_404
|
||||
|
@ -129,6 +130,7 @@ urlpatterns = [
|
|||
AnalyticsView.as_view(),
|
||||
name="analytics",
|
||||
),
|
||||
path('admin/registrar/user/<int:user_id>/transfer/', TransferUserView.as_view(), name='transfer_user'),
|
||||
path(
|
||||
"admin/api/get-senior-official-from-federal-agency-json/",
|
||||
get_senior_official_from_federal_agency_json,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
{% block content %}
|
||||
|
||||
<div id="content-main" class="analytics">
|
||||
<div id="content-main" class="custom-admin-template">
|
||||
|
||||
<div class="grid-row grid-gap-2">
|
||||
<div class="tablet:grid-col-6 margin-top-2">
|
||||
|
@ -29,28 +29,28 @@
|
|||
<div class="padding-top-2 padding-x-2">
|
||||
<ul class="usa-button-group wrapped-button-group">
|
||||
<li class="usa-button-group__item">
|
||||
<a href="{% url 'export_data_type' %}" class="button text-no-wrap" role="button">
|
||||
<a href="{% url 'export_data_type' %}" class="usa-button text-no-wrap" role="button">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||
</svg><span class="margin-left-05">All domain metadata</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<a href="{% url 'export_data_full' %}" class="button text-no-wrap" role="button">
|
||||
<a href="{% url 'export_data_full' %}" class="usa-button text-no-wrap" role="button">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||
</svg><span class="margin-left-05">Current full</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<a href="{% url 'export_data_federal' %}" class="button text-no-wrap" role="button">
|
||||
<a href="{% url 'export_data_federal' %}" class="usa-button text-no-wrap" role="button">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||
</svg><span class="margin-left-05">Current federal</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<a href="{% url 'export_data_domain_requests_full' %}" class="button text-no-wrap" role="button">
|
||||
<a href="{% url 'export_data_domain_requests_full' %}" class="usa-button text-no-wrap" role="button">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||
</svg><span class="margin-left-05">All domain requests metadata</span>
|
||||
|
@ -84,35 +84,35 @@
|
|||
</div>
|
||||
<ul class="usa-button-group">
|
||||
<li class="usa-button-group__item">
|
||||
<button class="button exportLink" data-export-url="{% url 'export_domains_growth' %}" type="button">
|
||||
<button class="usa-button exportLink" data-export-url="{% url 'export_domains_growth' %}" type="button">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||
</svg><span class="margin-left-05">Domain growth</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<button class="button exportLink" data-export-url="{% url 'export_requests_growth' %}" type="button">
|
||||
<button class="usa-button exportLink" data-export-url="{% url 'export_requests_growth' %}" type="button">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||
</svg><span class="margin-left-05">Request growth</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<button class="button exportLink" data-export-url="{% url 'export_managed_domains' %}" type="button">
|
||||
<button class="usa-button exportLink" data-export-url="{% url 'export_managed_domains' %}" type="button">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||
</svg><span class="margin-left-05">Managed domains</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<button class="button exportLink" data-export-url="{% url 'export_unmanaged_domains' %}" type="button">
|
||||
<button class="usa-button exportLink" data-export-url="{% url 'export_unmanaged_domains' %}" type="button">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||
</svg><span class="margin-left-05">Unmanaged domains</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<button class="button exportLink usa-button--secondary" data-export-url="{% url 'analytics' %}" type="button">
|
||||
<button class="usa-button exportLink usa-button--secondary" data-export-url="{% url 'analytics' %}" type="button">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#assessment"></use>
|
||||
</svg><span class="margin-left-05">Update charts</span>
|
||||
|
|
229
src/registrar/templates/admin/transfer_user.html
Normal file
229
src/registrar/templates/admin/transfer_user.html
Normal file
|
@ -0,0 +1,229 @@
|
|||
{% extends 'admin/base_site.html' %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block content_title %}<h1>Transfer user</h1>{% endblock %}
|
||||
|
||||
{% block extrastyle %}
|
||||
|
||||
{{ block.super }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extrahead %}
|
||||
{{ block.super }}
|
||||
<!-- Making the user select a combobox: -->
|
||||
<!-- Load Django Admin's base JavaScript. This is NEEDED because select2 relies on it. -->
|
||||
<script src="{% static 'admin/js/vendor/jquery/jquery.min.js' %}"></script>
|
||||
|
||||
<!-- Include Select2 JavaScript. Since this view technically falls outside of admin, this is needed. -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<script type="application/javascript" src="{% static 'js/get-gov-admin-extra.js' %}" defer></script>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<div class="breadcrumbs">
|
||||
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
|
||||
› <a href="{% url 'admin:app_list' 'registrar' %}">{% trans 'Registrar' %}</a>
|
||||
› <a href="{% url 'admin:registrar_user_changelist' %}">{% trans 'Users' %}</a>
|
||||
› <a href="{% url 'admin:registrar_user_change' current_user.pk %}">{{ current_user.first_name }} {{ current_user.last_name }}</a>
|
||||
› {% trans 'Transfer User' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content-main" class="custom-admin-template">
|
||||
|
||||
<div class="module padding-4 display-flex flex-row flex-justify submit-row">
|
||||
|
||||
<div class="desktop:flex-align-center">
|
||||
<form method="GET" action="{% url 'transfer_user' current_user.pk %}">
|
||||
<label for="selected_user" class="text-middle">Select user to transfer data from:</label>
|
||||
<select name="selected_user" id="selected_user" class="admin-combobox margin-top-0" onchange="this.form.submit()">
|
||||
<option value="">Select a user</option>
|
||||
{% for user in other_users %}
|
||||
<option value="{{ user.pk }}" {% if selected_user and user.pk == selected_user.pk %}selected{% endif %}>
|
||||
{{ user.first_name }} {{ user.last_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="submit" value="Preview" class="usa-button">
|
||||
</form>
|
||||
</div>
|
||||
<div class="desktop:flex-align-center">
|
||||
{% if selected_user %}
|
||||
<a class="usa-button" href="#transfer-and-delete" aria-controls="transfer-and-delete" data-open-modal>
|
||||
Transfer and delete old user
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-row grid-gap-2">
|
||||
<div class="tablet:grid-col-6 margin-top-2">
|
||||
<div class="module height-full">
|
||||
<h2>User to receive data</h2>
|
||||
<div class="padding-top-2 padding-x-2">
|
||||
<dl>
|
||||
<dt>Username:</dt>
|
||||
<dd>{{ current_user.username }}</dd>
|
||||
<dt>Created at:</dt>
|
||||
<dd>{{ current_user.created_at }}</dd>
|
||||
<dt>Last login:</dt>
|
||||
<dd>{{ current_user.last_login }}</dd>
|
||||
<dt>First name:</dt>
|
||||
<dd>{{ current_user.first_name }}</dd>
|
||||
<dt>Middle name:</dt>
|
||||
<dd>{{ current_user.middle_name }}</dd>
|
||||
<dt>Last name:</dt>
|
||||
<dd>{{ current_user.last_name }}</dd>
|
||||
<dt>Title:</dt>
|
||||
<dd>{{ current_user.title }}</dd>
|
||||
<dt>Email:</dt>
|
||||
<dd>{{ current_user.email }}</dd>
|
||||
<dt>Phone:</dt>
|
||||
<dd>{{ current_user.phone }}</dd>
|
||||
<dt>Domains:</dt>
|
||||
<dd>
|
||||
{% if current_user_domains %}
|
||||
<ul>
|
||||
{% for domain in current_user_domains %}
|
||||
<li>{{ domain }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>Domain requests:</dt>
|
||||
<dd>
|
||||
{% if current_user_domain_requests %}
|
||||
<ul>
|
||||
{% for request in current_user_domain_requests %}
|
||||
<li>{{ request }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tablet:grid-col-6 margin-top-2">
|
||||
<div class="module height-full">
|
||||
<h2>User to lose data and be deleted</h2>
|
||||
<div class="padding-top-2 padding-x-2">
|
||||
{% if selected_user %}
|
||||
<dl>
|
||||
<dt>Username:</dt>
|
||||
<dd>{{ selected_user.username }}</dd>
|
||||
<dt>Created at:</dt>
|
||||
<dd>{{ selected_user.created_at }}</dd>
|
||||
<dt>Last login:</dt>
|
||||
<dd>{{ selected_user.last_login }}</dd>
|
||||
<dt>First name:</dt>
|
||||
<dd>{{ selected_user.first_name }}</dd>
|
||||
<dt>Middle name:</dt>
|
||||
<dd>{{ selected_user.middle_name }}</dd>
|
||||
<dt>Last name:</dt>
|
||||
<dd>{{ selected_user.last_name }}</dd>
|
||||
<dt>Title:</dt>
|
||||
<dd>{{ selected_user.title }}</dd>
|
||||
<dt>Email:</dt>
|
||||
<dd>{{ selected_user.email }}</dd>
|
||||
<dt>Phone:</dt>
|
||||
<dd>{{ selected_user.phone }}</dd>
|
||||
<dt>Domains:</dt>
|
||||
<dd>
|
||||
{% if selected_user_domains %}
|
||||
<ul>
|
||||
{% for domain in selected_user_domains %}
|
||||
<li>{{ domain }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>Domain requests:</dt>
|
||||
<dd>
|
||||
{% if selected_user_domain_requests %}
|
||||
<ul>
|
||||
{% for request in selected_user_domain_requests %}
|
||||
<li>{{ request }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
{% else %}
|
||||
<p>No user selected yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="usa-modal"
|
||||
id="transfer-and-delete"
|
||||
aria-labelledby="This action will delete {{ selected_user }}"
|
||||
aria-describedby="This action will delete {{ selected_user }}"
|
||||
>
|
||||
<div class="usa-modal__content">
|
||||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading" id="transfer-and-delete-heading">
|
||||
This action will delete {{ selected_user }}
|
||||
</h2>
|
||||
<div class="usa-prose">
|
||||
{% if selected_user != logged_in_user %}
|
||||
<p>But you know what you're doing, right?</p>
|
||||
{% else %}
|
||||
<p>Don't do it!</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="usa-modal__footer">
|
||||
<ul class="usa-button-group">
|
||||
{% if selected_user != logged_in_user %}
|
||||
<li class="usa-button-group__item">
|
||||
<form method="POST" action="{% url 'transfer_user' current_user.pk %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="selected_user" value="{{ selected_user.pk }}">
|
||||
<input type="submit" class="usa-button" value="Transfer and delete old user">
|
||||
</form>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="usa-button-group__item">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||
name="_cancel_domain_request_ineligible"
|
||||
data-close-modal
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-modal__close"
|
||||
aria-label="Close this window"
|
||||
data-close-modal
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,6 +1,21 @@
|
|||
{% extends 'django/admin/email_clipboard_change_form.html' %}
|
||||
{% load i18n static %}
|
||||
|
||||
|
||||
{% block field_sets %}
|
||||
<div class="display-flex flex-row flex-justify submit-row">
|
||||
<div class="desktop:flex-align-self-end">
|
||||
<a href="{% url 'transfer_user' original.pk %}" class="button">
|
||||
Transfer data from old account
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for fieldset in adminform %}
|
||||
{% include "django/admin/includes/domain_fieldset.html" with state_help_message=state_help_message %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block after_related_objects %}
|
||||
<div class="module aligned padding-3">
|
||||
<h2>Associated requests and domains</h2>
|
||||
|
|
158
src/registrar/views/transfer_user.py
Normal file
158
src/registrar/views/transfer_user.py
Normal file
|
@ -0,0 +1,158 @@
|
|||
import logging
|
||||
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.views import View
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.models.domain_information import DomainInformation
|
||||
from registrar.models.domain_request import DomainRequest
|
||||
from registrar.models.portfolio import Portfolio
|
||||
from registrar.models.user import User
|
||||
from django.contrib.admin import site
|
||||
from django.contrib import messages
|
||||
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
from registrar.models.verified_by_staff import VerifiedByStaff
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TransferUserView(View):
|
||||
"""Transfer user methods that set up the transfer_user template and handle the forms on it."""
|
||||
|
||||
JOINS = [
|
||||
(DomainRequest, 'creator'),
|
||||
(DomainInformation, 'creator'),
|
||||
(Portfolio, 'creator'),
|
||||
(DomainRequest, 'investigator'),
|
||||
(UserDomainRole, 'user'),
|
||||
(VerifiedByStaff, 'requestor'),
|
||||
]
|
||||
|
||||
USER_FIELDS = ['portfolio', 'portfolio_roles', 'portfolio_additional_permissions']
|
||||
|
||||
def get(self, request, user_id):
|
||||
"""current_user referes to the 'source' user where the button that redirects to this view was clicked.
|
||||
other_users exclude current_user and populate a dropdown, selected_user is the selection in the dropdown.
|
||||
|
||||
This also querries the relevant domains and domain requests, and the admin context needed for the sidenav."""
|
||||
|
||||
current_user = get_object_or_404(User, pk=user_id)
|
||||
other_users = User.objects.exclude(pk=user_id).order_by('first_name', 'last_name') # Exclude the current user from the dropdown
|
||||
|
||||
# Get the default admin site context, needed for the sidenav
|
||||
admin_context = site.each_context(request)
|
||||
|
||||
context = {
|
||||
'current_user': current_user,
|
||||
'other_users': other_users,
|
||||
'logged_in_user': request.user,
|
||||
**admin_context, # Include the admin context
|
||||
'current_user_domains': self.get_domains(current_user),
|
||||
'current_user_domain_requests': self.get_domain_requests(current_user)
|
||||
}
|
||||
|
||||
selected_user_id = request.GET.get('selected_user')
|
||||
if selected_user_id:
|
||||
selected_user = get_object_or_404(User, pk=selected_user_id)
|
||||
context['selected_user'] = selected_user
|
||||
context['selected_user_domains'] = self.get_domains(selected_user)
|
||||
context['selected_user_domain_requests'] = self.get_domain_requests(selected_user)
|
||||
|
||||
return render(request, 'admin/transfer_user.html', context)
|
||||
|
||||
def post(self, request, user_id):
|
||||
"""This handles the transfer from selected_user to current_user then deletes selected_user.
|
||||
|
||||
NOTE: We have a ticket to refactor this into a more solid lookup for related fields in #2645"""
|
||||
|
||||
current_user = get_object_or_404(User, pk=user_id)
|
||||
selected_user_id = request.POST.get('selected_user')
|
||||
selected_user = get_object_or_404(User, pk=selected_user_id)
|
||||
|
||||
try:
|
||||
change_logs = []
|
||||
|
||||
# Transfer specific fields
|
||||
self.transfer_user_fields_and_log(selected_user, current_user, change_logs)
|
||||
|
||||
# Perform the updates and log the changes
|
||||
for model_class, field_name in self.JOINS:
|
||||
self.update_joins_and_log(model_class, field_name, selected_user, current_user, change_logs)
|
||||
|
||||
# Success message if any related objects were updated
|
||||
if change_logs:
|
||||
success_message = f"Data transferred successfully for the following objects: {change_logs}"
|
||||
messages.success(request, success_message)
|
||||
|
||||
selected_user.delete()
|
||||
messages.success(request, f"Deleted {selected_user} {selected_user.username}")
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f"An error occurred during the transfer: {e}")
|
||||
|
||||
return redirect('admin:registrar_user_change', object_id=user_id)
|
||||
|
||||
@classmethod
|
||||
def update_joins_and_log(cls, model_class, field_name, old_user, new_user, change_logs):
|
||||
"""
|
||||
Helper function to update the user join fields for a given model and log the changes.
|
||||
"""
|
||||
|
||||
filter_kwargs = {field_name: old_user}
|
||||
updated_objects = model_class.objects.filter(**filter_kwargs)
|
||||
|
||||
for obj in updated_objects:
|
||||
# Check for duplicate UserDomainRole before updating
|
||||
if model_class == UserDomainRole:
|
||||
if model_class.objects.filter(user=new_user, domain=obj.domain).exists():
|
||||
continue # Skip the update to avoid a duplicate
|
||||
|
||||
# Update the field on the object and save it
|
||||
setattr(obj, field_name, new_user)
|
||||
obj.save()
|
||||
|
||||
# Log the change
|
||||
cls.log_change(obj, field_name, old_user, new_user, change_logs)
|
||||
|
||||
@classmethod
|
||||
def transfer_user_fields_and_log(cls, old_user, new_user, change_logs):
|
||||
"""
|
||||
Transfers portfolio fields from the old_user to the new_user.
|
||||
Logs the changes for each transferred field.
|
||||
|
||||
NOTE: This will be refactored in #2644
|
||||
"""
|
||||
|
||||
for field in cls.USER_FIELDS:
|
||||
old_value = getattr(old_user, field, None)
|
||||
|
||||
if old_value:
|
||||
setattr(new_user, field, old_value)
|
||||
cls.log_change(new_user, field, old_value, old_value, change_logs)
|
||||
|
||||
new_user.save()
|
||||
|
||||
@classmethod
|
||||
def log_change(cls, obj, field_name, old_value, new_value, change_logs):
|
||||
"""Logs the change for a specific field on an object"""
|
||||
log_entry = f'Changed {field_name} from "{old_value}" to "{new_value}" on {obj}'
|
||||
|
||||
logger.info(log_entry)
|
||||
|
||||
# Collect the related object for the success message
|
||||
change_logs.append(log_entry)
|
||||
|
||||
@classmethod
|
||||
def get_domains(cls, user):
|
||||
"""A simplified version of domains_json"""
|
||||
user_domain_roles = UserDomainRole.objects.filter(user=user)
|
||||
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
|
||||
domains = Domain.objects.filter(id__in=domain_ids)
|
||||
|
||||
return domains
|
||||
|
||||
@classmethod
|
||||
def get_domain_requests(cls, user):
|
||||
"""A simplified version of domain_requests_json"""
|
||||
domain_requests = DomainRequest.objects.filter(creator=user)
|
||||
|
||||
return domain_requests
|
Loading…
Add table
Add a link
Reference in a new issue