Skip to content

Commit 1fd39e7

Browse files
authored
Merge pull request #242 from netboxlabs/bulk-warning
Disable updates to custom objects if in a branch
2 parents 17b5433 + e4a94ba commit 1fd39e7

File tree

7 files changed

+370
-18
lines changed

7 files changed

+370
-18
lines changed

.github/workflows/lint-tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ jobs:
6969
with:
7070
repository: "netbox-community/netbox"
7171
path: netbox
72-
ref: feature
72+
ref: main
7373
- name: Install netbox-custom-objects
7474
working-directory: netbox-custom-objects
7575
run: |

netbox_custom_objects/api/views.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
from django.http import Http404
2+
from django.utils.translation import gettext_lazy as _
23
from drf_spectacular.utils import extend_schema_view, extend_schema
34
from rest_framework.routers import APIRootView
45
from rest_framework.viewsets import ModelViewSet
6+
from rest_framework.exceptions import ValidationError
57

68
from netbox_custom_objects.filtersets import get_filterset_class
79
from netbox_custom_objects.models import CustomObjectType, CustomObjectTypeField
10+
from netbox_custom_objects.utilities import is_in_branch
811

912
from . import serializers
1013

14+
# Constants
15+
BRANCH_ACTIVE_ERROR_MESSAGE = _("Please switch to the main branch to perform this operation.")
16+
1117

1218
class RootView(APIRootView):
1319
def get_view_name(self):
@@ -60,6 +66,21 @@ def filterset_class(self):
6066
def list(self, request, *args, **kwargs):
6167
return super().list(request, *args, **kwargs)
6268

69+
def create(self, request, *args, **kwargs):
70+
if is_in_branch():
71+
raise ValidationError(BRANCH_ACTIVE_ERROR_MESSAGE)
72+
return super().create(request, *args, **kwargs)
73+
74+
def update(self, request, *args, **kwargs):
75+
if is_in_branch():
76+
raise ValidationError(BRANCH_ACTIVE_ERROR_MESSAGE)
77+
return super().update(request, *args, **kwargs)
78+
79+
def partial_update(self, request, *args, **kwargs):
80+
if is_in_branch():
81+
raise ValidationError(BRANCH_ACTIVE_ERROR_MESSAGE)
82+
return super().partial_update(request, *args, **kwargs)
83+
6384

6485
class CustomObjectTypeFieldViewSet(ModelViewSet):
6586
queryset = CustomObjectTypeField.objects.all()
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"{% if branch_warning %} disabled{% endif %}>{% 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"{% if branch_warning %} disabled{% endif %}>{% 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"{% if branch_warning %} disabled{% endif %}>{% 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"{% if branch_warning %} disabled{% endif %}>{% 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/templates/netbox_custom_objects/customobject_edit.html

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,22 @@ <h3 class="col-9 offset-3 mb-3 h4">{{ group }}</h3>
4545
{% endif %}
4646
{% endfor %}
4747
</div>
48-
{% endblock form %}
48+
{% endblock form %}
49+
50+
{% block buttons %}
51+
<a href="{{ return_url }}" class="btn btn-outline-secondary btn-float">{% trans "Cancel" %}</a>
52+
{% if object.pk %}
53+
<button type="submit" name="_update" class="btn btn-primary"{% if branch_warning %} disabled{% endif %}>
54+
{% trans "Save" %}
55+
</button>
56+
{% else %}
57+
<div class="btn-group" role="group" aria-label="{% trans "Actions" %}">
58+
<button type="submit" name="_create" class="btn btn-primary"{% if branch_warning %} disabled{% endif %}>
59+
{% trans "Create" %}
60+
</button>
61+
<button type="submit" name="_addanother" class="btn btn-outline-primary btn-float"{% if branch_warning %} disabled{% endif %}>
62+
{% trans "Create & Add Another" %}
63+
</button>
64+
</div>
65+
{% endif %}
66+
{% endblock buttons %}

netbox_custom_objects/templates/netbox_custom_objects/inc/branch_warning.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
</span>
77
<span class="flex-fill">
88
{% blocktrans trimmed %}
9-
This object has fields that reference objects in other apps and you have a branch active. Care must be taken to not reference an object that only exists in another branch.
9+
Please switch to the main branch to perform this operation.
1010
{% endblocktrans %}
1111
</span>
1212
</div>

0 commit comments

Comments
 (0)