Skip to content

Commit

Permalink
View for associating locations to contacts and bulk creation (nautobo…
Browse files Browse the repository at this point in the history
…t#5445) (nautobot#5618)

* save changes

* New Views are up and functional

* changelog

* save changes

* location.html changes

* update filters

* update contact/team filter email match, add test for filter

* address PR feedback

* Location to Contact/Team migration management command (nautobot#5474)

* Apply suggestions from code review



* Fixup after applying review comments

* Fix rendering of empty-string phone and email values

* Simplify template logic

* refactored Create contact from location view

* added unitttests

* address feedback in standup

* ruff fix

* address PR feedback

* address feedback from standup

* address Pr feedback

* fix the name form field required validation

* replaced hardcoded action string with LocationDataToContactActionChoices

* replaced hardcoded action string with LocationDataToContactActionChoices

* address feedback from sprint review

* address PR feedback

* Update nautobot/dcim/choices.py



---------

Co-authored-by: Hanlin Miao <[email protected]>
Co-authored-by: Gary Snider <[email protected]>
  • Loading branch information
3 people authored Apr 24, 2024
1 parent 2e6a144 commit dfbaf60
Show file tree
Hide file tree
Showing 9 changed files with 516 additions and 91 deletions.
1 change: 1 addition & 0 deletions changes/5034.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added a view to convert location contact information to contacts or teams.
14 changes: 14 additions & 0 deletions nautobot/dcim/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ class LocationStatusChoices(ChoiceSet):
)


class LocationDataToContactActionChoices(ChoiceSet):
ASSOCIATE_EXISTING_CONTACT = "associate existing contact"
ASSOCIATE_EXISTING_TEAM = "associate existing team"
CREATE_AND_ASSIGN_NEW_CONTACT = "create and assign new contact"
CREATE_AND_ASSIGN_NEW_TEAM = "create and assign new team"

CHOICES = (
(ASSOCIATE_EXISTING_CONTACT, "Associate to existing contact"),
(ASSOCIATE_EXISTING_TEAM, "Associate to existing team"),
(CREATE_AND_ASSIGN_NEW_CONTACT, "Create and assign new contact"),
(CREATE_AND_ASSIGN_NEW_TEAM, "Create and assign new team"),
)


#
# Racks
#
Expand Down
52 changes: 51 additions & 1 deletion nautobot/dcim/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
StatusModelFilterFormMixin,
TagsBulkEditFormMixin,
)
from nautobot.extras.models import ExternalIntegration, SecretsGroup, Status
from nautobot.extras.models import Contact, ContactAssociation, ExternalIntegration, Role, SecretsGroup, Status, Team
from nautobot.ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
from nautobot.ipam.models import IPAddress, IPAddressToInterface, VLAN, VLANLocationAssignment, VRF
from nautobot.tenancy.forms import TenancyFilterForm, TenancyForm
Expand All @@ -69,6 +69,7 @@
InterfaceModeChoices,
InterfaceRedundancyGroupProtocolChoices,
InterfaceTypeChoices,
LocationDataToContactActionChoices,
PortTypeChoices,
PowerFeedPhaseChoices,
PowerFeedSupplyChoices,
Expand Down Expand Up @@ -371,6 +372,55 @@ class LocationFilterForm(NautobotFilterForm, StatusModelFilterFormMixin, Tenancy
tags = TagFilterField(model)


class LocationMigrateDataToContactForm(NautobotModelForm):
# Assign tab form fields
action = forms.ChoiceField(
choices=LocationDataToContactActionChoices,
required=True,
widget=StaticSelect2(),
)
location = DynamicModelChoiceField(queryset=Location.objects.all(), required=False, label="Source Location")
contact = DynamicModelChoiceField(
queryset=Contact.objects.all(),
required=False,
label="Available Contacts",
query_params={"similar_to_location_data": "$location"},
)
team = DynamicModelChoiceField(
queryset=Team.objects.all(),
required=False,
label="Available Teams",
query_params={"similar_to_location_data": "$location"},
)
role = DynamicModelChoiceField(
queryset=Role.objects.all(),
required=True,
query_params={"content_types": ContactAssociation._meta.label_lower},
)
status = DynamicModelChoiceField(
queryset=Status.objects.all(),
required=True,
query_params={"content_types": ContactAssociation._meta.label_lower},
)
name = forms.CharField(required=False, label="Name")
phone = forms.CharField(required=False, label="Phone")
email = forms.CharField(required=False, label="Email")

class Meta:
model = ContactAssociation
fields = [
"action",
"location",
"contact",
"team",
"role",
"status",
"name",
"phone",
"email",
]


#
# Rack groups
#
Expand Down
45 changes: 32 additions & 13 deletions nautobot/dcim/templates/dcim/location.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
</div>
<div class="panel panel-default">
<div class="panel-heading">
<strong>Contact Info</strong>
<strong>Geographical Info</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
Expand Down Expand Up @@ -115,20 +115,39 @@
{% endif %}
</td>
</tr>
<tr>
<td>Contact Name</td>
<td>{{ object.contact_name|placeholder }}</td>
</tr>
<tr>
<td>Contact Phone</td>
<td>{{ object.contact_phone|hyperlinked_phone_number }}</td>
</tr>
<tr>
<td>Contact E-Mail</td>
<td>{{ object.contact_email|hyperlinked_email }}</td>
</tr>
</table>
</div>
{% if show_convert_to_contact_button %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Contact Info</strong>
</div>
<table class="table table-hover panel-body attr-table">
<tr>
<td>Contact Name</td>
<td>{{ object.contact_name|placeholder }}</td>
</tr>
<tr>
<td>Contact Phone</td>
<td>{{ object.contact_phone|hyperlinked_phone_number }}</td>
</tr>
<tr>
<td>Contact E-Mail</td>
<td>{{ object.contact_email|hyperlinked_email }}</td>
</tr>
</table>
{% if request.user|has_perms:contact_association_permission %}
{% with request.path|add:"?tab=contacts"|urlencode as return_url %}
<div class="panel-footer text-right noprint">
<a href="{% url 'dcim:location_migrate_data_to_contact' pk=object.pk %}?return_url={{return_url}}" class="btn btn-primary btn-xs">
<span class="mdi mdi-account-edit" aria-hidden="true"></span>
Convert to contact/team record
</a>
</div>
{% endwith %}
{% endif %}
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>Comments</strong>
Expand Down
102 changes: 102 additions & 0 deletions nautobot/dcim/templates/dcim/location_migrate_data_to_contact.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
{% extends 'generic/object_create.html' %}
{% load form_helpers %}
{% load helpers %}

{% block title %}Migrating contact data from {{ obj_type }} {{ obj }}{% endblock %}
{% block form %}
<div id="assign" class="tabcontent">
<div class="panel panel-default">
<div class="panel-heading"><strong>Contact Data</strong></div>
<div class="panel-body">
{% render_field form.action %}
{% render_field form.location %}
<div class="form-group">
<label class="col-md-3 control-label required">Source Location "Contact Name"</label>
<div class="col-md-9">
<p class="form-control-static">{{ obj.contact_name | placeholder}}</p>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label required">Source Location "Contact Phone"</label>
<div class="col-md-9">
<p class="form-control-static">{{ obj.contact_phone | placeholder}}</p>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label required">Source Location "Contact Email"</label>
<div class="col-md-9">
<p class="form-control-static">{{ obj.contact_email | placeholder}}</p>
</div>
</div>
{% render_field form.name %}
{% render_field form.phone %}
{% render_field form.email %}
{% render_field form.contact %}
{% render_field form.team %}
{% render_field form.role %}
{% render_field form.status %}
</div>
</div>
</div>
{% endblock %}

{% block buttons %}
<button type="submit" name="_create" class="btn btn-primary">Assign</button>
<a href="{% url 'dcim:location' pk=obj.pk %}" class="btn btn-default">Cancel</a>
{% endblock %}

{% block javascript %}
<script>

// action drop down toggle
const action = document.getElementById("id_action");
function toggle_form_fields() {
const action_option = action.value;
document.getElementById("id_location").disabled = true;
const name = document.getElementById("id_name");
const name_label = document.querySelector("label[for=id_name]");
const phone = document.getElementById("id_phone");
const email = document.getElementById("id_email");
const similar_contacts_label = document.querySelector("label[for=id_contact]");
const similar_contacts = document.getElementById("id_contact");
const similar_teams_label = document.querySelector("label[for=id_team]");
const similar_teams = document.getElementById("id_team");

// Toggle these form fields when the action matches
similar_contacts.toggleAttribute("required", action_option=="associate existing contact");
similar_contacts_label.classList.toggle("required", action_option=="associate existing contact")
similar_teams.toggleAttribute("required", action_option=="associate existing team");
similar_teams_label.classList.toggle("required", action_option=="associate existing team")
name.toggleAttribute("disabled", action_option.match(/associate existing/));
name.toggleAttribute("required", action_option.match(/create and assign/));
name_label.classList.toggle("required", action_option.match(/create and assign/));
phone.toggleAttribute("disabled", action_option.match(/associate existing/));
email.toggleAttribute("disabled", action_option.match(/associate existing/));

// Show and hide form fields and toggle form field labels
if (action_option === "associate existing contact"){
similar_contacts.parentElement.parentElement.style.display = "block";
similar_teams.parentElement.parentElement.style.display = "none";
name.parentElement.parentElement.style.display = "none";
phone.parentElement.parentElement.style.display = "none";
email.parentElement.parentElement.style.display = "none";
} else if (action_option === "associate existing team"){
similar_teams.parentElement.parentElement.style.display = "block";
similar_contacts.parentElement.parentElement.style.display = "none";
name.parentElement.parentElement.style.display = "none";
phone.parentElement.parentElement.style.display = "none";
email.parentElement.parentElement.style.display = "none";
} else {
similar_contacts.parentElement.parentElement.style.display = "none";
similar_contacts.removeAttribute("required")
similar_teams.parentElement.parentElement.style.display = "none";
name.parentElement.parentElement.style.display = "block";
phone.parentElement.parentElement.style.display = "block";
email.parentElement.parentElement.style.display = "block";
}

}
window.onload = toggle_form_fields
action.onchange = toggle_form_fields
</script>
{% endblock %}
Loading

0 comments on commit dfbaf60

Please sign in to comment.