Skip to content

Commit 82f2118

Browse files
committed
Add branching warning to bulk edit / import
1 parent 17b5433 commit 82f2118

File tree

3 files changed

+351
-2
lines changed

3 files changed

+351
-2
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
{% extends 'generic/bulk_edit.html' %}
2+
{% load form_helpers %}
3+
{% load helpers %}
4+
{% load i18n %}
5+
{% load render_table from django_tables2 %}
6+
7+
8+
{% block content %}
9+
10+
{# Edit form #}
11+
<div class="tab-pane show active" id="edit-form" role="tabpanel" aria-labelledby="edit-form-tab">
12+
{% if branch_warning %}
13+
{% include 'netbox_custom_objects/inc/branch_warning.html' %}
14+
{% endif %}
15+
16+
<form action="" method="post" class="form form-horizontal mt-5">
17+
<div id="form_fields" hx-disinherit="hx-select hx-swap">
18+
{% csrf_token %}
19+
{% if request.POST.return_url %}
20+
<input type="hidden" name="return_url" value="{{ request.POST.return_url }}" />
21+
{% endif %}
22+
{% for field in form.hidden_fields %}
23+
{{ field }}
24+
{% endfor %}
25+
26+
{% if form.fieldsets %}
27+
28+
{# Render grouped fields according to declared fieldsets #}
29+
{% for fieldset in form.fieldsets %}
30+
{% render_fieldset form fieldset %}
31+
{% endfor %}
32+
33+
{# Render tag add/remove fields #}
34+
{% if form.add_tags and form.remove_tags %}
35+
<div class="field-group mb-5">
36+
<div class="row">
37+
<h2 class="col-9 offset-3">{% trans "Tags" %}</h2>
38+
</div>
39+
{% render_field form.add_tags %}
40+
{% render_field form.remove_tags %}
41+
</div>
42+
{% endif %}
43+
44+
{# Render custom fields #}
45+
{% if form.custom_fields %}
46+
<div class="field-group mb-5">
47+
<div class="row">
48+
<h2 class="col-9 offset-3">{% trans "Custom Fields" %}</h2>
49+
</div>
50+
{% render_custom_fields form %}
51+
</div>
52+
{% endif %}
53+
54+
{# Render comments #}
55+
{% if form.comments %}
56+
<div class="field-group mb-5">
57+
<div class="row">
58+
<h2 class="col-9 offset-3">{% trans "Comments" %}</h2>
59+
</div>
60+
{% render_field form.comments bulk_nullable=True %}
61+
</div>
62+
{% endif %}
63+
64+
{% else %}
65+
66+
{# Render all fields #}
67+
{% for field in form.visible_fields %}
68+
{% if field.name in form.meta_fields %}
69+
{% elif field.name in form.nullable_fields %}
70+
{% render_field field bulk_nullable=True %}
71+
{% else %}
72+
{% render_field field %}
73+
{% endif %}
74+
{% endfor %}
75+
76+
{% endif %}
77+
78+
{# Meta fields #}
79+
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 px-3 mb-3">
80+
{% if form.changelog_message %}
81+
{% render_field form.changelog_message %}
82+
{% endif %}
83+
{% render_field form.background_job %}
84+
</div>
85+
86+
<div class="btn-float-group-right">
87+
<a href="{{ return_url }}" class="btn btn-outline-secondary btn-float">{% trans "Cancel" %}</a>
88+
<button type="submit" name="_apply" class="btn btn-primary">{% trans "Apply" %}</button>
89+
</div>
90+
</div>
91+
</form>
92+
</div>
93+
94+
{# Selected objects list #}
95+
<div class="tab-pane" id="object-list" role="tabpanel" aria-labelledby="object-list-tab">
96+
<div class="card">
97+
<div class="card-body table-responsive">
98+
{% render_table table 'inc/table.html' %}
99+
</div>
100+
</div>
101+
</div>
102+
103+
{% endblock content %}
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
{% extends 'generic/bulk_import.html' %}
2+
{% load form_helpers %}
3+
{% load helpers %}
4+
{% load i18n %}
5+
6+
7+
{% block content %}
8+
9+
{# Data Import Form #}
10+
<div class="tab-pane show active" id="import-form" role="tabpanel" aria-labelledby="import-form-tab">
11+
<div class="col col-md-12 col-lg-10 offset-lg-1">
12+
13+
{% if branch_warning %}
14+
{% include 'netbox_custom_objects/inc/branch_warning.html' %}
15+
{% endif %}
16+
17+
<form action="" method="post" enctype="multipart/form-data" class="form">
18+
{% csrf_token %}
19+
<input type="hidden" name="import_method" value="direct" />
20+
21+
{# Form fields #}
22+
{% render_field form.data %}
23+
{% render_field form.format %}
24+
{% render_field form.csv_delimiter %}
25+
26+
{# Meta fields #}
27+
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 px-3 mb-3">
28+
{% if form.changelog_message %}
29+
{% render_field form.changelog_message %}
30+
{% endif %}
31+
{% render_field form.background_job %}
32+
</div>
33+
34+
<div class="form-group">
35+
<div class="col col-md-12 text-end">
36+
{% if return_url %}
37+
<a href="{{ return_url }}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
38+
{% endif %}
39+
<button type="submit" name="data_submit" class="btn btn-primary">{% trans "Submit" %}</button>
40+
</div>
41+
</div>
42+
</form>
43+
</div>
44+
</div>
45+
46+
{# File Upload Form #}
47+
<div class="tab-pane show" id="upload-form" role="tabpanel" aria-labelledby="upload-form-tab">
48+
<div class="col col-md-12 col-lg-10 offset-lg-1">
49+
<form action="" method="post" enctype="multipart/form-data" class="form">
50+
{% csrf_token %}
51+
<input type="hidden" name="import_method" value="upload" />
52+
53+
{# Form fields #}
54+
{% render_field form.upload_file %}
55+
{% render_field form.format %}
56+
{% render_field form.csv_delimiter %}
57+
58+
{# Meta fields #}
59+
{# Background jobs not supported with file uploads #}
60+
{% if form.changelog_message %}
61+
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 px-3 mb-3">
62+
{% render_field form.changelog_message %}
63+
</div>
64+
{% endif %}
65+
66+
<div class="form-group">
67+
<div class="col col-md-12 text-end">
68+
{% if return_url %}
69+
<a href="{{ return_url }}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
70+
{% endif %}
71+
<button type="submit" name="file_submit" class="btn btn-primary">{% trans "Submit" %}</button>
72+
</div>
73+
</div>
74+
</form>
75+
</div>
76+
</div>
77+
78+
{# DataFile Form #}
79+
<div class="tab-pane show" id="datafile-form" role="tabpanel" aria-labelledby="datafile-form-tab">
80+
<div class="col col-md-12 col-lg-10 offset-lg-1">
81+
<form action="" method="post" enctype="multipart/form-data" class="form">
82+
{% csrf_token %}
83+
<input type="hidden" name="import_method" value="datafile" />
84+
85+
{# Form fields #}
86+
{% render_field form.data_source %}
87+
{% render_field form.data_file %}
88+
{% render_field form.format %}
89+
{% render_field form.csv_delimiter %}
90+
91+
{# Meta fields #}
92+
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 px-3 mb-3">
93+
{% if form.changelog_message %}
94+
{% render_field form.changelog_message %}
95+
{% endif %}
96+
{% render_field form.background_job %}
97+
</div>
98+
99+
<div class="form-group">
100+
<div class="col col-md-12 text-end">
101+
{% if return_url %}
102+
<a href="{{ return_url }}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
103+
{% endif %}
104+
<button type="submit" name="file_submit" class="btn btn-primary">{% trans "Submit" %}</button>
105+
</div>
106+
</div>
107+
</form>
108+
</div>
109+
</div>
110+
111+
{% if fields %}
112+
<div class="row my-3">
113+
<div class="col col-md-12">
114+
<div class="card">
115+
<h2 class="card-header">{% trans "Field Options" %}</h2>
116+
<table class="table">
117+
<thead>
118+
<tr>
119+
<th>{% trans "Field" %}</th>
120+
<th>{% trans "Required" %}</th>
121+
<th>{% trans "Accessor" %}</th>
122+
<th>{% trans "Description" %}</th>
123+
</tr>
124+
</thead>
125+
<tbody>
126+
{% for name, field in fields.items %}
127+
<tr>
128+
<td class="font-monospace{% if field.required %} fw-bold{% endif %}">
129+
{{ name }}
130+
</td>
131+
<td>
132+
{% if field.required %}
133+
{% checkmark True true="Required" %}
134+
{% else %}
135+
{{ ''|placeholder }}
136+
{% endif %}
137+
</td>
138+
{% if field.to_field_name %}
139+
<td class="font-monospace">{{ field.to_field_name }}</td>
140+
{% else %}
141+
<td>{{ ''|placeholder }}</td>
142+
{% endif %}
143+
<td>
144+
{% if field.help_text %}
145+
{{ field.help_text }}
146+
{% elif field.label %}
147+
{{ field.label }}
148+
{% endif %}
149+
{% if field.STATIC_CHOICES %}
150+
<a href="#" data-bs-toggle="modal" data-bs-target="#{{ name }}_choices" aria-label="{{ name }} {% trans "choices" %}"><i class="mdi mdi-help-circle"></i></a>
151+
<div class="modal fade" id="{{ name }}_choices" tabindex="-1" role="dialog">
152+
<div class="modal-dialog" role="document">
153+
<div class="modal-content">
154+
<div class="modal-header">
155+
<h5 class="modal-title">
156+
<span class="font-monospace">{{ name }}</span> {% trans "Choices" %}
157+
</h5>
158+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
159+
</div>
160+
<table class="table table-striped modal-body">
161+
<thead>
162+
<tr>
163+
<th>{% trans "Import Value" %}</th>
164+
<th>{% trans "Label" %}</th>
165+
</tr>
166+
</thead>
167+
<tbody>
168+
{% for value, label in field.choices %}
169+
{% if value %}
170+
<tr>
171+
<td><samp>{{ value }}</samp></td>
172+
<td>{{ label }}</td>
173+
</tr>
174+
{% endif %}
175+
{% endfor %}
176+
</tbody>
177+
</table>
178+
</div>
179+
</div>
180+
</div>
181+
{% endif %}
182+
{% if field|widget_type == 'dateinput' %}
183+
<br /><small class="text-muted">{% trans "Format: YYYY-MM-DD" %}</small>
184+
{% elif field|widget_type == 'checkboxinput' %}
185+
<br /><small class="text-muted">{% trans "Specify true or false" %}</small>
186+
{% endif %}
187+
</td>
188+
</tr>
189+
{% endfor %}
190+
</tbody>
191+
</table>
192+
</div>
193+
</div>
194+
</div>
195+
<p class="small text-muted">
196+
<i class="mdi mdi-check-bold text-success"></i>
197+
{% blocktrans trimmed %}
198+
Required fields <strong>must</strong> be specified for all objects.
199+
{% endblocktrans %}
200+
</p>
201+
<p class="small text-muted">
202+
<i class="mdi mdi-information-outline"></i>
203+
{% blocktrans trimmed with example="vrf.rd" %}
204+
Related objects may be referenced by any unique attribute. For example, <code>{{ example }}</code> would identify a VRF by its route distinguisher.
205+
{% endblocktrans %}
206+
</p>
207+
{% endif %}
208+
209+
{% endblock content %}

netbox_custom_objects/views.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,7 @@ def get_return_url(self, request, obj=None):
634634

635635
@register_model_view(CustomObject, "bulk_edit", path="edit", detail=False)
636636
class CustomObjectBulkEditView(CustomObjectTableMixin, generic.BulkEditView):
637+
template_name = "netbox_custom_objects/custom_object_bulk_edit.html"
637638
queryset = None
638639
custom_object_type = None
639640
table = None
@@ -690,6 +691,23 @@ def get_form(self, queryset):
690691

691692
return form
692693

694+
def get_extra_context(self, request):
695+
696+
# Check if we're in a branch and if there are external object pointers
697+
has_external_object_pointers = False
698+
if is_in_branch():
699+
# Check all fields in the custom object type
700+
for field in self.custom_object_type.fields.all():
701+
if field.type in [CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT]:
702+
# Check if the related object type is not from the current app
703+
if field.related_object_type.app_label != APP_LABEL:
704+
has_external_object_pointers = True
705+
break
706+
707+
return {
708+
'branch_warning': has_external_object_pointers,
709+
}
710+
693711

694712
@register_model_view(CustomObject, "bulk_delete", path="delete", detail=False)
695713
class CustomObjectBulkDeleteView(CustomObjectTableMixin, generic.BulkDeleteView):
@@ -706,18 +724,20 @@ def setup(self, request, *args, **kwargs):
706724
def get_queryset(self, request):
707725
if self.queryset:
708726
return self.queryset
709-
custom_object_type = self.kwargs.pop("custom_object_type", None)
727+
self.custom_object_type = self.kwargs.pop("custom_object_type", None)
710728
self.custom_object_type = CustomObjectType.objects.get(
711-
slug=custom_object_type
729+
slug=self.custom_object_type
712730
)
713731
model = self.custom_object_type.get_model_with_serializer()
714732
return model.objects.all()
715733

716734

717735
@register_model_view(CustomObject, "bulk_import", path="import", detail=False)
718736
class CustomObjectBulkImportView(generic.BulkImportView):
737+
template_name = "netbox_custom_objects/custom_object_bulk_import.html"
719738
queryset = None
720739
model_form = None
740+
custom_object_type = None
721741

722742
def get(self, request, custom_object_type):
723743
# Necessary because get() in BulkImportView only takes request and no **kwargs
@@ -774,6 +794,23 @@ def get_model_form(self, queryset):
774794

775795
return form
776796

797+
def get_extra_context(self, request):
798+
799+
# Check if we're in a branch and if there are external object pointers
800+
has_external_object_pointers = False
801+
if is_in_branch():
802+
# Check all fields in the custom object type
803+
for field in self.custom_object_type.fields.all():
804+
if field.type in [CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT]:
805+
# Check if the related object type is not from the current app
806+
if field.related_object_type.app_label != APP_LABEL:
807+
has_external_object_pointers = True
808+
break
809+
810+
return {
811+
'branch_warning': has_external_object_pointers,
812+
}
813+
777814

778815
class CustomObjectJournalView(ConditionalLoginRequiredMixin, View):
779816
"""

0 commit comments

Comments
 (0)