Compare commits
22 Commits
dc8524362b
...
546ad3e36c
Author | SHA1 | Date |
---|---|---|
Arthur Hanson | 546ad3e36c | |
Markku Leiniö | 0cc2963e6f | |
Arthur Hanson | 56ea7b8714 | |
Jeremy Stretch | 6e658d43dc | |
Arnold | 6ff349dbac | |
Daniel Sheppard | d7d97b1b52 | |
Markku Leiniö | d7f652bcc7 | |
Jeremy Stretch | 313b6e624c | |
Arthur | 0df3787796 | |
Jeremy Stretch | 5c68fc9202 | |
Jeremy Stretch | ff8dabe8d9 | |
Markku Leiniö | 5c5c0e1e43 | |
Jeremy Stretch | b87d1eca98 | |
Arthur | f809733f4c | |
Arthur | d868c395e9 | |
Arthur | 3bdff476b6 | |
Arthur | 8c8d39084e | |
Arthur | 7415a3b157 | |
Arthur | e9cd805c1a | |
Arthur | 5a91a0f543 | |
Arthur | b21d3e735c | |
Arthur | 933dc6a34a |
|
@ -11,8 +11,21 @@ master = true
|
|||
; clear environment on exit
|
||||
vacuum = true
|
||||
|
||||
; make SIGTERM stop the app (instead of reload)
|
||||
die-on-term = true
|
||||
|
||||
; exit if no app can be loaded
|
||||
need-app = true
|
||||
|
||||
; do not use multiple interpreters
|
||||
single-interpreter = true
|
||||
|
||||
; change to the project directory
|
||||
chdir = netbox
|
||||
|
||||
; specify the WSGI module to load
|
||||
module = netbox.wsgi
|
||||
|
||||
; only log internal messages and errors (reverse proxy already logs the requests)
|
||||
disable-logging = true
|
||||
log-5xx = true
|
||||
|
|
|
@ -77,7 +77,7 @@ Create the following for each model:
|
|||
|
||||
## 13. GraphQL API components
|
||||
|
||||
Create a Graphene object type for the model in `graphql/types.py` by subclassing the appropriate class from `netbox.graphql.types`.
|
||||
Create a GraphQL object type for the model in `graphql/types.py` by subclassing the appropriate class from `netbox.graphql.types`.
|
||||
|
||||
Also extend the schema class defined in `graphql/schema.py` with the individual object and object list fields per the established convention.
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ pip3 install pyuwsgi
|
|||
Once installed, add the package to `local_requirements.txt` to ensure it is re-installed during future rebuilds of the virtual environment:
|
||||
|
||||
```no-highlight
|
||||
sudo sh -c "echo 'pyuwgsi' >> /opt/netbox/local_requirements.txt"
|
||||
sudo sh -c "echo 'pyuwsgi' >> /opt/netbox/local_requirements.txt"
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# GraphQL API Overview
|
||||
|
||||
NetBox provides a read-only [GraphQL](https://graphql.org/) API to complement its REST API. This API is powered by the [Graphene](https://graphene-python.org/) library and [Graphene-Django](https://docs.graphene-python.org/projects/django/en/latest/).
|
||||
NetBox provides a read-only [GraphQL](https://graphql.org/) API to complement its REST API. This API is powered by [Strawberry Django](https://strawberry-graphql.github.io/strawberry-django/).
|
||||
|
||||
## Queries
|
||||
|
||||
|
@ -47,7 +47,7 @@ NetBox provides both a singular and plural query field for each object type:
|
|||
|
||||
For example, query `device(id:123)` to fetch a specific device (identified by its unique ID), and query `device_list` (with an optional set of filters) to fetch all devices.
|
||||
|
||||
For more detail on constructing GraphQL queries, see the [Graphene documentation](https://docs.graphene-python.org/en/latest/) as well as the [GraphQL queries documentation](https://graphql.org/learn/queries/).
|
||||
For more detail on constructing GraphQL queries, see the [GraphQL queries documentation](https://graphql.org/learn/queries/). For filtering and lookup syntax, please refer to the [Strawberry Django documentation](https://strawberry-graphql.github.io/strawberry-django/guide/filters/).
|
||||
|
||||
## Filtering
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Defining the Schema Class
|
||||
|
||||
A plugin can extend NetBox's GraphQL API by registering its own schema class. By default, NetBox will attempt to import `graphql.schema` from the plugin, if it exists. This path can be overridden by defining `graphql_schema` on the PluginConfig instance as the dotted path to the desired Python class. This class must be a subclass of `graphene.ObjectType`.
|
||||
A plugin can extend NetBox's GraphQL API by registering its own schema class. By default, NetBox will attempt to import `graphql.schema` from the plugin, if it exists. This path can be overridden by defining `graphql_schema` on the PluginConfig instance as the dotted path to the desired Python class.
|
||||
|
||||
### Example
|
||||
|
||||
|
|
|
@ -3,16 +3,18 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from ipam.models import ASN
|
||||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import add_blank_choice
|
||||
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
from utilities.forms.rendering import FieldSet, TabbedGroups
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, NumberWithOptions
|
||||
|
||||
__all__ = (
|
||||
'CircuitBulkEditForm',
|
||||
'CircuitTerminationBulkEditForm',
|
||||
'CircuitTypeBulkEditForm',
|
||||
'ProviderBulkEditForm',
|
||||
'ProviderAccountBulkEditForm',
|
||||
|
@ -172,3 +174,48 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
|||
nullable_fields = (
|
||||
'tenant', 'commit_rate', 'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
class CircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
description = forms.CharField(
|
||||
label=_('Description'),
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
label=_('Site'),
|
||||
queryset=Site.objects.all(),
|
||||
required=False
|
||||
)
|
||||
provider_network = DynamicModelChoiceField(
|
||||
label=_('Provider Network'),
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False
|
||||
)
|
||||
port_speed = forms.IntegerField(
|
||||
required=False,
|
||||
label=_('Port speed (Kbps)'),
|
||||
)
|
||||
upstream_speed = forms.IntegerField(
|
||||
required=False,
|
||||
label=_('upstream speed (Kbps)'),
|
||||
)
|
||||
mark_connected = forms.NullBooleanField(
|
||||
label=_('Mark connected'),
|
||||
required=False,
|
||||
widget=BulkEditNullBooleanSelect
|
||||
)
|
||||
|
||||
model = CircuitTermination
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'description',
|
||||
TabbedGroups(
|
||||
FieldSet('site', name=_('Site')),
|
||||
FieldSet('provider_network', name=_('Provider Network')),
|
||||
),
|
||||
'mark_connected', name=_('Circuit Termination')
|
||||
),
|
||||
FieldSet('port_speed', 'upstream_speed', name=_('Termination Details')),
|
||||
)
|
||||
nullable_fields = ('description')
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
from django import forms
|
||||
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.choices import CircuitStatusChoices, CircuitTerminationSideChoices
|
||||
from circuits.models import *
|
||||
from circuits.choices import *
|
||||
from dcim.models import Site
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
@ -111,7 +112,16 @@ class CircuitImportForm(NetBoxModelImportForm):
|
|||
]
|
||||
|
||||
|
||||
class CircuitTerminationImportForm(forms.ModelForm):
|
||||
class CircuitTerminationImportForm(NetBoxModelImportForm):
|
||||
circuit = CSVModelChoiceField(
|
||||
label=_('Circuit'),
|
||||
queryset=Circuit.objects.all(),
|
||||
to_field_name='cid',
|
||||
)
|
||||
term_side = CSVChoiceField(
|
||||
label=_('Termination'),
|
||||
choices=CircuitTerminationSideChoices,
|
||||
)
|
||||
site = CSVModelChoiceField(
|
||||
label=_('Site'),
|
||||
queryset=Site.objects.all(),
|
||||
|
@ -129,5 +139,5 @@ class CircuitTerminationImportForm(forms.ModelForm):
|
|||
model = CircuitTermination
|
||||
fields = [
|
||||
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'pp_info', 'description',
|
||||
'pp_info', 'description', 'tags'
|
||||
]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices, CircuitTerminationSideChoices
|
||||
from circuits.models import *
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from ipam.models import ASN
|
||||
|
@ -13,6 +13,7 @@ from utilities.forms.widgets import DatePicker, NumberWithOptions
|
|||
|
||||
__all__ = (
|
||||
'CircuitFilterForm',
|
||||
'CircuitTerminationFilterForm',
|
||||
'CircuitTypeFilterForm',
|
||||
'ProviderFilterForm',
|
||||
'ProviderAccountFilterForm',
|
||||
|
@ -186,3 +187,42 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
|||
)
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class CircuitTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||
model = CircuitTermination
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('circuit_id', 'term_side', name=_('Circuit')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site')
|
||||
)
|
||||
circuit_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
required=False,
|
||||
label=_('Circuit')
|
||||
)
|
||||
term_side = forms.MultipleChoiceField(
|
||||
label=_('Term Side'),
|
||||
choices=CircuitTerminationSideChoices,
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
|
|
@ -227,7 +227,7 @@ class CircuitTermination(
|
|||
return f'{self.circuit}: Termination {self.term_side}'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.circuit.get_absolute_url()
|
||||
return reverse('circuits:circuittermination', args=[self.pk])
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
|
|
@ -10,6 +10,7 @@ from .columns import CommitRateColumn
|
|||
|
||||
__all__ = (
|
||||
'CircuitTable',
|
||||
'CircuitTerminationTable',
|
||||
'CircuitTypeTable',
|
||||
)
|
||||
|
||||
|
@ -88,3 +89,26 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
|||
default_columns = (
|
||||
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
|
||||
)
|
||||
|
||||
|
||||
class CircuitTerminationTable(NetBoxTable):
|
||||
circuit = tables.Column(
|
||||
verbose_name=_('Circuit'),
|
||||
linkify=True
|
||||
)
|
||||
site = tables.Column(
|
||||
verbose_name=_('Site'),
|
||||
linkify=True
|
||||
)
|
||||
provider_network = tables.Column(
|
||||
verbose_name=_('Provider Network'),
|
||||
linkify=True
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CircuitTermination
|
||||
fields = (
|
||||
'pk', 'id', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info', 'description', 'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = ('pk', 'id', 'circuit', 'term_side', 'description')
|
||||
|
|
|
@ -287,10 +287,7 @@ class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||
}
|
||||
|
||||
|
||||
class CircuitTerminationTestCase(
|
||||
ViewTestCases.EditObjectViewTestCase,
|
||||
ViewTestCases.DeleteObjectViewTestCase,
|
||||
):
|
||||
class CircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = CircuitTermination
|
||||
|
||||
@classmethod
|
||||
|
@ -327,6 +324,24 @@ class CircuitTerminationTestCase(
|
|||
'description': 'New description',
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"circuit,term_side,site,description",
|
||||
"Circuit 3,A,Site 1,Foo",
|
||||
"Circuit 3,Z,Site 1,Bar",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,port_speed,description",
|
||||
f"{circuit_terminations[0].pk},100,New description7",
|
||||
f"{circuit_terminations[1].pk},200,New description8",
|
||||
f"{circuit_terminations[2].pk},300,New description9",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'port_speed': 400,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_trace(self):
|
||||
device = create_test_device('Device 1')
|
||||
|
|
|
@ -48,7 +48,11 @@ urlpatterns = [
|
|||
path('circuits/<int:pk>/', include(get_model_urls('circuits', 'circuit'))),
|
||||
|
||||
# Circuit terminations
|
||||
path('circuit-terminations/', views.CircuitTerminationListView.as_view(), name='circuittermination_list'),
|
||||
path('circuit-terminations/add/', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
|
||||
path('circuit-terminations/import/', views.CircuitTerminationBulkImportView.as_view(), name='circuittermination_import'),
|
||||
path('circuit-terminations/edit/', views.CircuitTerminationBulkEditView.as_view(), name='circuittermination_bulk_edit'),
|
||||
path('circuit-terminations/delete/', views.CircuitTerminationBulkDeleteView.as_view(), name='circuittermination_bulk_delete'),
|
||||
path('circuit-terminations/<int:pk>/', include(get_model_urls('circuits', 'circuittermination'))),
|
||||
|
||||
]
|
||||
|
|
|
@ -408,6 +408,18 @@ class CircuitContactsView(ObjectContactsView):
|
|||
# Circuit terminations
|
||||
#
|
||||
|
||||
class CircuitTerminationListView(generic.ObjectListView):
|
||||
queryset = CircuitTermination.objects.prefetch_related('circuit')
|
||||
filterset = filtersets.CircuitTerminationFilterSet
|
||||
filterset_form = forms.CircuitTerminationFilterForm
|
||||
table = tables.CircuitTerminationTable
|
||||
|
||||
|
||||
@register_model_view(CircuitTermination)
|
||||
class CircuitTerminationView(generic.ObjectView):
|
||||
queryset = CircuitTermination.objects.prefetch_related('circuit')
|
||||
|
||||
|
||||
@register_model_view(CircuitTermination, 'edit')
|
||||
class CircuitTerminationEditView(generic.ObjectEditView):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
|
@ -419,5 +431,23 @@ class CircuitTerminationDeleteView(generic.ObjectDeleteView):
|
|||
queryset = CircuitTermination.objects.all()
|
||||
|
||||
|
||||
class CircuitTerminationBulkImportView(generic.BulkImportView):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
model_form = forms.CircuitTerminationImportForm
|
||||
|
||||
|
||||
class CircuitTerminationBulkEditView(generic.BulkEditView):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
filterset = filtersets.CircuitTerminationFilterSet
|
||||
table = tables.CircuitTerminationTable
|
||||
form = forms.CircuitTerminationBulkEditForm
|
||||
|
||||
|
||||
class CircuitTerminationBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
filterset = filtersets.CircuitTerminationFilterSet
|
||||
table = tables.CircuitTerminationTable
|
||||
|
||||
|
||||
# Trace view
|
||||
register_model_view(CircuitTermination, 'trace', kwargs={'model': CircuitTermination})(PathTraceView)
|
||||
|
|
|
@ -85,6 +85,7 @@ class Command(BaseCommand):
|
|||
|
||||
module_name, script_name = script.split('.', 1)
|
||||
module, script = get_module_and_script(module_name, script_name)
|
||||
script = script.python_class
|
||||
|
||||
# Take user from command line if provided and exists, other
|
||||
if options['user']:
|
||||
|
|
|
@ -60,7 +60,10 @@ def get_module_scripts(scriptmodule):
|
|||
return cls.full_name.split(".", maxsplit=1)[1]
|
||||
|
||||
loader = SourceFileLoader(get_python_name(scriptmodule), get_full_path(scriptmodule))
|
||||
module = loader.load_module()
|
||||
try:
|
||||
module = loader.load_module()
|
||||
except FileNotFoundError:
|
||||
return {}
|
||||
|
||||
scripts = {}
|
||||
ordered = getattr(module, 'script_order', [])
|
||||
|
|
|
@ -545,7 +545,7 @@ class ScriptResultsTable(BaseTable):
|
|||
template_code="""{% load log_levels %}{% log_level record.status %}""",
|
||||
verbose_name=_('Level')
|
||||
)
|
||||
message = tables.Column(
|
||||
message = columns.MarkdownColumn(
|
||||
verbose_name=_('Message')
|
||||
)
|
||||
|
||||
|
@ -566,22 +566,17 @@ class ReportResultsTable(BaseTable):
|
|||
time = tables.Column(
|
||||
verbose_name=_('Time')
|
||||
)
|
||||
status = tables.Column(
|
||||
empty_values=(),
|
||||
verbose_name=_('Level')
|
||||
)
|
||||
status = tables.TemplateColumn(
|
||||
template_code="""{% load log_levels %}{% log_level record.status %}""",
|
||||
verbose_name=_('Level')
|
||||
)
|
||||
|
||||
object = tables.Column(
|
||||
verbose_name=_('Object')
|
||||
)
|
||||
url = tables.Column(
|
||||
verbose_name=_('URL')
|
||||
)
|
||||
message = tables.Column(
|
||||
message = columns.MarkdownColumn(
|
||||
verbose_name=_('Message')
|
||||
)
|
||||
|
||||
|
|
|
@ -258,6 +258,7 @@ CIRCUITS_MENU = Menu(
|
|||
items=(
|
||||
get_model_item('circuits', 'circuit', _('Circuits')),
|
||||
get_model_item('circuits', 'circuittype', _('Circuit Types')),
|
||||
get_model_item('circuits', 'circuittermination', _('Circuit Terminations')),
|
||||
),
|
||||
),
|
||||
MenuGroup(
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li class="breadcrumb-item"><a href="{% url 'circuits:circuit_list' %}?provider_id={{ object.provider.pk }}">{{ object.provider }}</a></li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-6">
|
||||
|
||||
<div class="card">
|
||||
{% if object %}
|
||||
<table class="table table-hover attr-table">
|
||||
{% if object.site %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Site" %}</th>
|
||||
<td>
|
||||
{% if object.site.region %}
|
||||
{{ object.site.region|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.site|linkify }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Termination" %}</th>
|
||||
<td>
|
||||
{% if object.mark_connected %}
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
|
||||
<span class="text-muted">{% trans "Marked as connected" %}</span>
|
||||
{% elif object.cable %}
|
||||
<a class="d-block d-md-inline" href="{{ object.cable.get_absolute_url }}">{{ object.cable }}</a> {% trans "to" %}
|
||||
{% for peer in object.link_peers %}
|
||||
{% if peer.device %}
|
||||
{{ peer.device|linkify }}<br/>
|
||||
{% elif peer.circuit %}
|
||||
{{ peer.circuit|linkify }}<br/>
|
||||
{% endif %}
|
||||
{{ peer|linkify }}{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
<div class="mt-1">
|
||||
<a href="{% url 'circuits:circuittermination_trace' pk=object.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
|
||||
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i> {% trans "Trace" %}
|
||||
</a>
|
||||
{% if perms.dcim.change_cable %}
|
||||
<a href="{% url 'dcim:cable_edit' pk=object.cable.pk %}?return_url={{ object.circuit.get_absolute_url }}" title="{% trans "Edit cable" %}" class="btn btn-warning lh-1">
|
||||
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> {% trans "Edit" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.delete_cable %}
|
||||
<a href="{% url 'dcim:cable_delete' pk=object.cable.pk %}?return_url={{ object.circuit.get_absolute_url }}" title="{% trans "Remove cable" %}" class="btn btn-danger lh-1">
|
||||
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i> {% trans "Disconnect" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif perms.dcim.add_cable %}
|
||||
<div class="dropdown">
|
||||
<button type="button" class="btn btn-success dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> {% trans "Connect" %}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&return_url={{ object.get_absolute_url }}">{% trans "Interface" %}</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}">{% trans "Front Port" %}</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}">{% trans "Rear Port" %}</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&return_url={{ object.get_absolute_url }}">{% trans "Circuit Termination" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider Network" %}</th>
|
||||
<td>{{ object.provider_network.provider|linkify }} / {{ object.provider_network|linkify }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Speed" %}</th>
|
||||
<td>
|
||||
{% if object.port_speed and object.upstream_speed %}
|
||||
<i class="mdi mdi-arrow-down-bold" title="{% trans "Downstream" %}"></i> {{ object.port_speed|humanize_speed }}
|
||||
<i class="mdi mdi-arrow-up-bold" title="{% trans "Upstream" %}"></i> {{ object.upstream_speed|humanize_speed }}
|
||||
{% elif object.port_speed %}
|
||||
{{ object.port_speed|humanize_speed }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Cross-Connect" %}</th>
|
||||
<td>{{ object.xconnect_id|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Patch Panel/Port" %}</th>
|
||||
<td>{{ object.pp_info|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="card-body">
|
||||
<span class="text-muted">{% trans "None" %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -31,6 +31,10 @@
|
|||
<th scope="row">{% trans "NetBox version" %}</th>
|
||||
<td>{{ stats.netbox_version }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Python version" %}</th>
|
||||
<td>{{ stats.python_version }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Django version" %}</th>
|
||||
<td>{{ stats.django_version }}</td>
|
||||
|
|
|
@ -46,7 +46,21 @@
|
|||
<input type="hidden" name="next" value="{{ request.POST.next }}" />
|
||||
{% endif %}
|
||||
|
||||
{% render_form form %}
|
||||
<div class="form-group mb-3">
|
||||
<label for="id_username" class="form-label">{{ form.username.label }}</label>
|
||||
{{ form.username }}
|
||||
{% for error in form.username.errors %}
|
||||
<div class="alert alert-danger">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="id_password" class="form-label">{{ form.password.label }}</label>
|
||||
{{ form.password }}
|
||||
{% for error in form.password.errors %}
|
||||
<div class="alert alert-danger">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="form-footer">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
|
|
Loading…
Reference in New Issue