Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/lint-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
with:
repository: "netbox-community/netbox"
path: netbox
ref: feature
ref: main
- name: Install netbox-custom-objects
working-directory: netbox-custom-objects
run: |
Expand Down
21 changes: 21 additions & 0 deletions netbox_custom_objects/api/views.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
from django.http import Http404
from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema_view, extend_schema
from rest_framework.routers import APIRootView
from rest_framework.viewsets import ModelViewSet
from rest_framework.exceptions import ValidationError

from netbox_custom_objects.filtersets import get_filterset_class
from netbox_custom_objects.models import CustomObjectType, CustomObjectTypeField
from netbox_custom_objects.utilities import is_in_branch

from . import serializers

# Constants
BRANCH_ACTIVE_ERROR_MESSAGE = _("Please switch to the main branch to perform this operation.")


class RootView(APIRootView):
def get_view_name(self):
Expand Down Expand Up @@ -60,6 +66,21 @@ def filterset_class(self):
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)

def create(self, request, *args, **kwargs):
if is_in_branch():
raise ValidationError(BRANCH_ACTIVE_ERROR_MESSAGE)
return super().create(request, *args, **kwargs)

def update(self, request, *args, **kwargs):
if is_in_branch():
raise ValidationError(BRANCH_ACTIVE_ERROR_MESSAGE)
return super().update(request, *args, **kwargs)

def partial_update(self, request, *args, **kwargs):
if is_in_branch():
raise ValidationError(BRANCH_ACTIVE_ERROR_MESSAGE)
return super().partial_update(request, *args, **kwargs)


class CustomObjectTypeFieldViewSet(ModelViewSet):
queryset = CustomObjectTypeField.objects.all()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
{% extends 'generic/bulk_edit.html' %}
{% load form_helpers %}
{% load helpers %}
{% load i18n %}
{% load render_table from django_tables2 %}


{% block content %}

{# Edit form #}
<div class="tab-pane show active" id="edit-form" role="tabpanel" aria-labelledby="edit-form-tab">
{% if branch_warning %}
{% include 'netbox_custom_objects/inc/branch_warning.html' %}
{% endif %}

<form action="" method="post" class="form form-horizontal mt-5">
<div id="form_fields" hx-disinherit="hx-select hx-swap">
{% csrf_token %}
{% if request.POST.return_url %}
<input type="hidden" name="return_url" value="{{ request.POST.return_url }}" />
{% endif %}
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}

{% if form.fieldsets %}

{# Render grouped fields according to declared fieldsets #}
{% for fieldset in form.fieldsets %}
{% render_fieldset form fieldset %}
{% endfor %}

{# Render tag add/remove fields #}
{% if form.add_tags and form.remove_tags %}
<div class="field-group mb-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "Tags" %}</h2>
</div>
{% render_field form.add_tags %}
{% render_field form.remove_tags %}
</div>
{% endif %}

{# Render custom fields #}
{% if form.custom_fields %}
<div class="field-group mb-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "Custom Fields" %}</h2>
</div>
{% render_custom_fields form %}
</div>
{% endif %}

{# Render comments #}
{% if form.comments %}
<div class="field-group mb-5">
<div class="row">
<h2 class="col-9 offset-3">{% trans "Comments" %}</h2>
</div>
{% render_field form.comments bulk_nullable=True %}
</div>
{% endif %}

{% else %}

{# Render all fields #}
{% for field in form.visible_fields %}
{% if field.name in form.meta_fields %}
{% elif field.name in form.nullable_fields %}
{% render_field field bulk_nullable=True %}
{% else %}
{% render_field field %}
{% endif %}
{% endfor %}

{% endif %}

{# Meta fields #}
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 px-3 mb-3">
{% if form.changelog_message %}
{% render_field form.changelog_message %}
{% endif %}
{% render_field form.background_job %}
</div>

<div class="btn-float-group-right">
<a href="{{ return_url }}" class="btn btn-outline-secondary btn-float">{% trans "Cancel" %}</a>
<button type="submit" name="_apply" class="btn btn-primary"{% if branch_warning %} disabled{% endif %}>{% trans "Apply" %}</button>
</div>
</div>
</form>
</div>

{# Selected objects list #}
<div class="tab-pane" id="object-list" role="tabpanel" aria-labelledby="object-list-tab">
<div class="card">
<div class="card-body table-responsive">
{% render_table table 'inc/table.html' %}
</div>
</div>
</div>

{% endblock content %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
{% extends 'generic/bulk_import.html' %}
{% load form_helpers %}
{% load helpers %}
{% load i18n %}


{% block content %}

{# Data Import Form #}
<div class="tab-pane show active" id="import-form" role="tabpanel" aria-labelledby="import-form-tab">
<div class="col col-md-12 col-lg-10 offset-lg-1">

{% if branch_warning %}
{% include 'netbox_custom_objects/inc/branch_warning.html' %}
{% endif %}

<form action="" method="post" enctype="multipart/form-data" class="form">
{% csrf_token %}
<input type="hidden" name="import_method" value="direct" />

{# Form fields #}
{% render_field form.data %}
{% render_field form.format %}
{% render_field form.csv_delimiter %}

{# Meta fields #}
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 px-3 mb-3">
{% if form.changelog_message %}
{% render_field form.changelog_message %}
{% endif %}
{% render_field form.background_job %}
</div>

<div class="form-group">
<div class="col col-md-12 text-end">
{% if return_url %}
<a href="{{ return_url }}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
{% endif %}
<button type="submit" name="data_submit" class="btn btn-primary"{% if branch_warning %} disabled{% endif %}>{% trans "Submit" %}</button>
</div>
</div>
</form>
</div>
</div>

{# File Upload Form #}
<div class="tab-pane show" id="upload-form" role="tabpanel" aria-labelledby="upload-form-tab">
<div class="col col-md-12 col-lg-10 offset-lg-1">
<form action="" method="post" enctype="multipart/form-data" class="form">
{% csrf_token %}
<input type="hidden" name="import_method" value="upload" />

{# Form fields #}
{% render_field form.upload_file %}
{% render_field form.format %}
{% render_field form.csv_delimiter %}

{# Meta fields #}
{# Background jobs not supported with file uploads #}
{% if form.changelog_message %}
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 px-3 mb-3">
{% render_field form.changelog_message %}
</div>
{% endif %}

<div class="form-group">
<div class="col col-md-12 text-end">
{% if return_url %}
<a href="{{ return_url }}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
{% endif %}
<button type="submit" name="file_submit" class="btn btn-primary"{% if branch_warning %} disabled{% endif %}>{% trans "Submit" %}</button>
</div>
</div>
</form>
</div>
</div>

{# DataFile Form #}
<div class="tab-pane show" id="datafile-form" role="tabpanel" aria-labelledby="datafile-form-tab">
<div class="col col-md-12 col-lg-10 offset-lg-1">
<form action="" method="post" enctype="multipart/form-data" class="form">
{% csrf_token %}
<input type="hidden" name="import_method" value="datafile" />

{# Form fields #}
{% render_field form.data_source %}
{% render_field form.data_file %}
{% render_field form.format %}
{% render_field form.csv_delimiter %}

{# Meta fields #}
<div class="bg-primary-subtle border border-primary rounded-1 pt-3 px-3 mb-3">
{% if form.changelog_message %}
{% render_field form.changelog_message %}
{% endif %}
{% render_field form.background_job %}
</div>

<div class="form-group">
<div class="col col-md-12 text-end">
{% if return_url %}
<a href="{{ return_url }}" class="btn btn-outline-secondary">{% trans "Cancel" %}</a>
{% endif %}
<button type="submit" name="file_submit" class="btn btn-primary"{% if branch_warning %} disabled{% endif %}>{% trans "Submit" %}</button>
</div>
</div>
</form>
</div>
</div>

{% if fields %}
<div class="row my-3">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">{% trans "Field Options" %}</h2>
<table class="table">
<thead>
<tr>
<th>{% trans "Field" %}</th>
<th>{% trans "Required" %}</th>
<th>{% trans "Accessor" %}</th>
<th>{% trans "Description" %}</th>
</tr>
</thead>
<tbody>
{% for name, field in fields.items %}
<tr>
<td class="font-monospace{% if field.required %} fw-bold{% endif %}">
{{ name }}
</td>
<td>
{% if field.required %}
{% checkmark True true="Required" %}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
{% if field.to_field_name %}
<td class="font-monospace">{{ field.to_field_name }}</td>
{% else %}
<td>{{ ''|placeholder }}</td>
{% endif %}
<td>
{% if field.help_text %}
{{ field.help_text }}
{% elif field.label %}
{{ field.label }}
{% endif %}
{% if field.STATIC_CHOICES %}
<a href="#" data-bs-toggle="modal" data-bs-target="#{{ name }}_choices" aria-label="{{ name }} {% trans "choices" %}"><i class="mdi mdi-help-circle"></i></a>
<div class="modal fade" id="{{ name }}_choices" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<span class="font-monospace">{{ name }}</span> {% trans "Choices" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<table class="table table-striped modal-body">
<thead>
<tr>
<th>{% trans "Import Value" %}</th>
<th>{% trans "Label" %}</th>
</tr>
</thead>
<tbody>
{% for value, label in field.choices %}
{% if value %}
<tr>
<td><samp>{{ value }}</samp></td>
<td>{{ label }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
{% if field|widget_type == 'dateinput' %}
<br /><small class="text-muted">{% trans "Format: YYYY-MM-DD" %}</small>
{% elif field|widget_type == 'checkboxinput' %}
<br /><small class="text-muted">{% trans "Specify true or false" %}</small>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<p class="small text-muted">
<i class="mdi mdi-check-bold text-success"></i>
{% blocktrans trimmed %}
Required fields <strong>must</strong> be specified for all objects.
{% endblocktrans %}
</p>
<p class="small text-muted">
<i class="mdi mdi-information-outline"></i>
{% blocktrans trimmed with example="vrf.rd" %}
Related objects may be referenced by any unique attribute. For example, <code>{{ example }}</code> would identify a VRF by its route distinguisher.
{% endblocktrans %}
</p>
{% endif %}

{% endblock content %}
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,22 @@ <h3 class="col-9 offset-3 mb-3 h4">{{ group }}</h3>
{% endif %}
{% endfor %}
</div>
{% endblock form %}
{% endblock form %}

{% block buttons %}
<a href="{{ return_url }}" class="btn btn-outline-secondary btn-float">{% trans "Cancel" %}</a>
{% if object.pk %}
<button type="submit" name="_update" class="btn btn-primary"{% if branch_warning %} disabled{% endif %}>
{% trans "Save" %}
</button>
{% else %}
<div class="btn-group" role="group" aria-label="{% trans "Actions" %}">
<button type="submit" name="_create" class="btn btn-primary"{% if branch_warning %} disabled{% endif %}>
{% trans "Create" %}
</button>
<button type="submit" name="_addanother" class="btn btn-outline-primary btn-float"{% if branch_warning %} disabled{% endif %}>
{% trans "Create & Add Another" %}
</button>
</div>
{% endif %}
{% endblock buttons %}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</span>
<span class="flex-fill">
{% blocktrans trimmed %}
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.
Please switch to the main branch to perform this operation.
{% endblocktrans %}
</span>
</div>
Loading