diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index b87d627ed..3d2038b22 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -26,7 +26,7 @@ body:
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
- placeholder: v4.0.2
+ placeholder: v4.0.3
validations:
required: true
- type: dropdown
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
index 6c245c7ef..bd9a17ff9 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yaml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
- placeholder: v4.0.2
+ placeholder: v4.0.3
validations:
required: true
- type: dropdown
diff --git a/.github/workflows/close-incomplete-issues.yml b/.github/workflows/close-incomplete-issues.yml
new file mode 100644
index 000000000..4d31d735e
--- /dev/null
+++ b/.github/workflows/close-incomplete-issues.yml
@@ -0,0 +1,32 @@
+# close-stale-issues (https://github.com/marketplace/actions/close-stale-issues)
+name: Close incomplete issues
+
+on:
+ schedule:
+ - cron: '15 4 * * *'
+ workflow_dispatch:
+
+permissions:
+ actions: write
+ issues: write
+
+jobs:
+ stale:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/stale@v9
+ with:
+ close-issue-message: >
+ This issue is being closed as no further information has been provided. If
+ you would like to revisit this topic, please first modify your original post
+ to include all the requested detail, and then ask that the issue be reopened.
+ days-before-stale: 7
+ days-before-close: 7
+ only-issue-labels: 'status: revisions needed'
+ operations-per-run: 100
+ remove-stale-when-updated: false
+ stale-issue-label: 'pending closure'
+ stale-issue-message: >
+ This is a reminder that additional information is needed in order to further
+ triage this issue. If the requested details are not provided, the issue will
+ soon be closed automatically.
diff --git a/.github/workflows/close-stale-issues.yml b/.github/workflows/close-stale-issues.yml
index 7a29c8f08..1ac1ea687 100644
--- a/.github/workflows/close-stale-issues.yml
+++ b/.github/workflows/close-stale-issues.yml
@@ -17,18 +17,19 @@ jobs:
steps:
- uses: actions/stale@v9
with:
+ # General parameters
+ operations-per-run: 100
+ remove-stale-when-updated: false
+
+ # Issue parameters
close-issue-message: >
This issue has been automatically closed due to lack of activity. In an
effort to reduce noise, please do not comment any further. Note that the
core maintainers may elect to reopen this issue at a later date if deemed
necessary.
- close-pr-message: >
- This PR has been automatically closed due to lack of activity.
- days-before-stale: 90
- days-before-close: 30
+ days-before-issue-stale: 90
+ days-before-issue-close: 30
exempt-issue-labels: 'status: accepted,status: blocked,status: needs milestone'
- operations-per-run: 100
- remove-stale-when-updated: false
stale-issue-label: 'pending closure'
stale-issue-message: >
This issue has been automatically marked as stale because it has not had
@@ -38,6 +39,12 @@ jobs:
process by "bumping" the issue; doing so will result in its immediate closure
and you may be barred from participating in any future discussions. Please see
our [contributing guide](https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md).
+
+ # Pull request parameters
+ close-pr-message: >
+ This PR has been automatically closed due to lack of activity.
+ days-before-pr-stale: 15
+ days-before-pr-close: 15
stale-pr-label: 'pending closure'
stale-pr-message: >
This PR has been automatically marked as stale because it has not had
diff --git a/.gitignore b/.gitignore
index 93954fd41..ac5f420b4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@ yarn-error.log*
/venv/
/*.sh
local_requirements.txt
+local_settings.py
!upgrade.sh
fabfile.py
gunicorn.py
diff --git a/README.md b/README.md
index 8d2efed23..4d21003b5 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
-
-
-
-
-
Stats via Repography
-
diff --git a/contrib/generated_schema.json b/contrib/generated_schema.json
index fe9d56b34..5cfdfd9d0 100644
--- a/contrib/generated_schema.json
+++ b/contrib/generated_schema.json
@@ -179,6 +179,9 @@
"usb-micro-ab",
"usb-3-b",
"usb-3-micro-b",
+ "molex-micro-fit-1x2",
+ "molex-micro-fit-2x2",
+ "molex-micro-fit-2x4",
"dc-terminal",
"saf-d-grid",
"neutrik-powercon-20",
@@ -281,6 +284,9 @@
"usb-a",
"usb-micro-b",
"usb-c",
+ "molex-micro-fit-1x2",
+ "molex-micro-fit-2x2",
+ "molex-micro-fit-2x4",
"dc-terminal",
"hdot-cx",
"saf-d-grid",
@@ -375,6 +381,8 @@
"gsm",
"cdma",
"lte",
+ "4g",
+ "5g",
"sonet-oc3",
"sonet-oc12",
"sonet-oc48",
@@ -408,12 +416,15 @@
"e3",
"xdsl",
"docsis",
+ "bpon",
+ "epon",
+ "10g-epon",
"gpon",
"xg-pon",
"xgs-pon",
"ng-pon2",
- "epon",
- "10g-epon",
+ "25g-pon",
+ "50g-pon",
"cisco-stackwise",
"cisco-stackwise-plus",
"cisco-flexstack",
diff --git a/docs/_theme/main.html b/docs/_theme/main.html
index 4dfc4e14e..99907bf42 100644
--- a/docs/_theme/main.html
+++ b/docs/_theme/main.html
@@ -2,8 +2,8 @@
{% block site_meta %}
{{ super() }}
- {# Disable search indexing unless we're building for ReadTheDocs #}
- {% if not config.extra.readthedocs %}
+ {# Disable search indexing unless we're building for public consumption #}
+ {% if not config.extra.build_public %}
{% endif %}
{% endblock %}
diff --git a/docs/configuration/required-parameters.md b/docs/configuration/required-parameters.md
index bda365995..90eb8c0cf 100644
--- a/docs/configuration/required-parameters.md
+++ b/docs/configuration/required-parameters.md
@@ -94,15 +94,25 @@ REDIS = {
}
```
-!!! note
- If you are upgrading from a NetBox release older than v2.7.0, please note that the Redis connection configuration
- settings have changed. Manual modification to bring the `REDIS` section inline with the above specification is
- necessary
-
!!! warning
It is highly recommended to keep the task and cache databases separate. Using the same database number on the
same Redis instance for both may result in queued background tasks being lost during cache flushing events.
+### UNIX Socket Support
+
+Redis may alternatively be configured by specifying a complete URL instead of individual components. This approach supports the use of a UNIX socket connection. For example:
+
+```python
+REDIS = {
+ 'tasks': {
+ 'URL': 'unix:///run/redis-netbox/redis.sock?db=0'
+ },
+ 'caching': {
+ 'URL': 'unix:///run/redis-netbox/redis.sock?db=1'
+ },
+}
+```
+
### Using Redis Sentinel
If you are using [Redis Sentinel](https://redis.io/topics/sentinel) for high-availability purposes, there is minimal
diff --git a/docs/release-notes/version-4.0.md b/docs/release-notes/version-4.0.md
index 0970b0357..c644f7cc4 100644
--- a/docs/release-notes/version-4.0.md
+++ b/docs/release-notes/version-4.0.md
@@ -1,5 +1,37 @@
# NetBox v4.0
+## v4.0.3 (2024-05-22)
+
+### Enhancements
+
+* [#12984](https://github.com/netbox-community/netbox/issues/12984) - Add Molex Micro-Fit power port & outlet types
+* [#13764](https://github.com/netbox-community/netbox/issues/13764) - Enable contact assignments for aggregates, prefixes, IP ranges, and IP addresses
+* [#14639](https://github.com/netbox-community/netbox/issues/14639) - Add Ukrainian translation support
+* [#14653](https://github.com/netbox-community/netbox/issues/14653) - Add an inventory items table column for all device components
+* [#14686](https://github.com/netbox-community/netbox/issues/14686) - Add German translation support
+* [#14855](https://github.com/netbox-community/netbox/issues/14855) - Add Chinese translation support
+* [#14948](https://github.com/netbox-community/netbox/issues/14948) - Introduce the `has_virtual_device_context` filter for devices
+* [#15353](https://github.com/netbox-community/netbox/issues/15353) - Improve error reporting when custom scripts fail to load
+* [#15496](https://github.com/netbox-community/netbox/issues/15496) - Implement dedicated views for management of circuit terminations
+* [#15603](https://github.com/netbox-community/netbox/issues/15603) - Add 4G & 5G cellular interface types
+* [#15962](https://github.com/netbox-community/netbox/issues/15962) - Enable UNIX socket connections for Redis
+
+### Bug Fixes
+
+* [#13293](https://github.com/netbox-community/netbox/issues/13293) - Limit interface selector for IP address to current device/VM
+* [#14953](https://github.com/netbox-community/netbox/issues/14953) - Ensure annotated count fields are present in REST API response data when creating new objects
+* [#14982](https://github.com/netbox-community/netbox/issues/14982) - Fix OpenAPI schema definition for SerializedPKRelatedFields
+* [#15082](https://github.com/netbox-community/netbox/issues/15082) - Strip whitespace from choice values & labels when creating a custom field choice set
+* [#16138](https://github.com/netbox-community/netbox/issues/16138) - Fix support for referencing users & groups in object permissions
+* [#16145](https://github.com/netbox-community/netbox/issues/16145) - Restore ability to reference custom scripts via module & name in REST API
+* [#16164](https://github.com/netbox-community/netbox/issues/16164) - Correct display of selected values in UI when filtering object list by a null value
+* [#16173](https://github.com/netbox-community/netbox/issues/16173) - Fix TypeError exception when viewing object list with no pagination preference defined
+* [#16228](https://github.com/netbox-community/netbox/issues/16228) - Fix permissions enforcement for GraphQL queries of users & groups
+* [#16232](https://github.com/netbox-community/netbox/issues/16232) - Preserve bulk action checkboxes on dynamic tables when using pagination
+* [#16240](https://github.com/netbox-community/netbox/issues/16240) - Fixed NoReverseMatch exception when adding circuit terminations to an object counts dashboard widget
+
+---
+
## v4.0.2 (2024-05-14)
!!! warning "Important"
diff --git a/mkdocs.yml b/mkdocs.yml
index 6f7ea7045..cf1e66cea 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -42,7 +42,7 @@ plugins:
show_root_toc_entry: false
show_source: false
extra:
- readthedocs: !ENV READTHEDOCS
+ build_public: !ENV BUILD_PUBLIC
social:
- icon: fontawesome/brands/github
link: https://github.com/netbox-community/netbox
diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py
index a1fc8661a..e52673874 100644
--- a/netbox/circuits/filtersets.py
+++ b/netbox/circuits/filtersets.py
@@ -275,6 +275,17 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
queryset=ProviderNetwork.objects.all(),
label=_('ProviderNetwork (ID)'),
)
+ provider_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='circuit__provider_id',
+ queryset=Provider.objects.all(),
+ label=_('Provider (ID)'),
+ )
+ provider = django_filters.ModelMultipleChoiceFilter(
+ field_name='circuit__provider__slug',
+ queryset=Provider.objects.all(),
+ to_field_name='slug',
+ label=_('Provider (slug)'),
+ )
class Meta:
model = CircuitTermination
diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py
index 3ac311c56..ea15c3010 100644
--- a/netbox/circuits/forms/bulk_edit.py
+++ b/netbox/circuits/forms/bulk_edit.py
@@ -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')
diff --git a/netbox/circuits/forms/bulk_import.py b/netbox/circuits/forms/bulk_import.py
index 8127d5bcb..1ceb44b60 100644
--- a/netbox/circuits/forms/bulk_import.py
+++ b/netbox/circuits/forms/bulk_import.py
@@ -1,10 +1,10 @@
from django import forms
-
-from circuits.choices import CircuitStatusChoices
-from circuits.models import *
-from dcim.models import Site
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
+
+from circuits.choices import *
+from circuits.models import *
+from dcim.models import Site
from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
@@ -12,6 +12,7 @@ from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugFiel
__all__ = (
'CircuitImportForm',
'CircuitTerminationImportForm',
+ 'CircuitTerminationImportRelatedForm',
'CircuitTypeImportForm',
'ProviderImportForm',
'ProviderAccountImportForm',
@@ -111,7 +112,16 @@ class CircuitImportForm(NetBoxModelImportForm):
]
-class CircuitTerminationImportForm(forms.ModelForm):
+class BaseCircuitTerminationImportForm(forms.ModelForm):
+ 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(),
@@ -125,9 +135,21 @@ class CircuitTerminationImportForm(forms.ModelForm):
required=False
)
+
+class CircuitTerminationImportRelatedForm(BaseCircuitTerminationImportForm):
class Meta:
model = CircuitTermination
fields = [
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
- 'pp_info', 'description',
+ 'pp_info', 'description'
+ ]
+
+
+class CircuitTerminationImportForm(NetBoxModelImportForm, BaseCircuitTerminationImportForm):
+
+ class Meta:
+ model = CircuitTermination
+ fields = [
+ 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
+ 'pp_info', 'description', 'tags'
]
diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py
index b2426e928..6f6473c3d 100644
--- a/netbox/circuits/forms/filtersets.py
+++ b/netbox/circuits/forms/filtersets.py
@@ -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,46 @@ 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('provider_id', 'provider_network_id', name=_('Provider')),
+ FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
+ )
+ 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
+ )
+ provider_network_id = DynamicModelMultipleChoiceField(
+ queryset=ProviderNetwork.objects.all(),
+ required=False,
+ query_params={
+ 'provider_id': '$provider_id'
+ },
+ label=_('Provider network')
+ )
+ provider_id = DynamicModelMultipleChoiceField(
+ queryset=Provider.objects.all(),
+ required=False,
+ label=_('Provider')
+ )
+ tag = TagFilterField(model)
diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py
index 7b65d52ad..fa21d7cd3 100644
--- a/netbox/circuits/models/circuits.py
+++ b/netbox/circuits/models/circuits.py
@@ -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()
diff --git a/netbox/circuits/tables/circuits.py b/netbox/circuits/tables/circuits.py
index 6ae727eca..5d650df61 100644
--- a/netbox/circuits/tables/circuits.py
+++ b/netbox/circuits/tables/circuits.py
@@ -10,6 +10,7 @@ from .columns import CommitRateColumn
__all__ = (
'CircuitTable',
+ 'CircuitTerminationTable',
'CircuitTypeTable',
)
@@ -88,3 +89,31 @@ 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
+ )
+ provider = tables.Column(
+ verbose_name=_('Provider'),
+ linkify=True,
+ accessor='circuit.provider'
+ )
+ 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', 'provider', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
+ 'xconnect_id', 'pp_info', 'description', 'created', 'last_updated', 'actions',
+ )
+ default_columns = ('pk', 'id', 'circuit', 'provider', 'term_side', 'description')
diff --git a/netbox/circuits/tests/test_filtersets.py b/netbox/circuits/tests/test_filtersets.py
index 0480439eb..df10c3929 100644
--- a/netbox/circuits/tests/test_filtersets.py
+++ b/netbox/circuits/tests/test_filtersets.py
@@ -351,24 +351,26 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
providers = (
Provider(name='Provider 1', slug='provider-1'),
+ Provider(name='Provider 2', slug='provider-2'),
+ Provider(name='Provider 3', slug='provider-3'),
)
Provider.objects.bulk_create(providers)
provider_networks = (
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
- ProviderNetwork(name='Provider Network 2', provider=providers[0]),
- ProviderNetwork(name='Provider Network 3', provider=providers[0]),
+ ProviderNetwork(name='Provider Network 2', provider=providers[1]),
+ ProviderNetwork(name='Provider Network 3', provider=providers[2]),
)
ProviderNetwork.objects.bulk_create(provider_networks)
circuits = (
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 1'),
- Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 2'),
- Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 3'),
+ Circuit(provider=providers[1], type=circuit_types[0], cid='Circuit 2'),
+ Circuit(provider=providers[2], type=circuit_types[0], cid='Circuit 3'),
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 4'),
- Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 5'),
- Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 6'),
- Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 7'),
+ Circuit(provider=providers[1], type=circuit_types[0], cid='Circuit 5'),
+ Circuit(provider=providers[2], type=circuit_types[0], cid='Circuit 6'),
+ Circuit(provider=providers[2], type=circuit_types[0], cid='Circuit 7'),
)
Circuit.objects.bulk_create(circuits)
@@ -413,10 +415,17 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_circuit_id(self):
- circuits = Circuit.objects.all()[:2]
+ circuits = Circuit.objects.filter(cid__in=['Circuit 1', 'Circuit 2'])
params = {'circuit_id': [circuits[0].pk, circuits[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+ def test_provider(self):
+ providers = Provider.objects.all()[:2]
+ params = {'provider_id': [providers[0].pk, providers[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+ params = {'provider': [providers[0].slug, providers[1].slug]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+
def test_site(self):
sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]}
diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py
index 85e2304cf..577548703 100644
--- a/netbox/circuits/tests/test_views.py
+++ b/netbox/circuits/tests/test_views.py
@@ -5,8 +5,11 @@ from django.urls import reverse
from circuits.choices import *
from circuits.models import *
+from core.models import ObjectType
from dcim.models import Cable, Interface, Site
from ipam.models import ASN, RIR
+from netbox.choices import ImportFormatChoices
+from users.models import ObjectPermission
from utilities.testing import ViewTestCases, create_tags, create_test_device
@@ -115,6 +118,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@classmethod
def setUpTestData(cls):
+ Site.objects.create(name='Site 1', slug='site-1')
providers = (
Provider(name='Provider 1', slug='provider-1'),
@@ -184,6 +188,51 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'comments': 'New comments',
}
+ @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
+ def test_bulk_import_objects_with_terminations(self):
+ json_data = """
+ [
+ {
+ "cid": "Circuit 7",
+ "provider": "Provider 1",
+ "type": "Circuit Type 1",
+ "status": "active",
+ "description": "Testing Import",
+ "terminations": [
+ {
+ "term_side": "A",
+ "site": "Site 1"
+ },
+ {
+ "term_side": "Z",
+ "site": "Site 1"
+ }
+ ]
+ }
+ ]
+ """
+ initial_count = self._get_queryset().count()
+ data = {
+ 'data': json_data,
+ 'format': ImportFormatChoices.JSON,
+ }
+
+ # Assign model-level permission
+ obj_perm = ObjectPermission(
+ name='Test permission',
+ actions=['add']
+ )
+ obj_perm.save()
+ obj_perm.users.add(self.user)
+ obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
+
+ # Try GET with model-level permission
+ self.assertHttpStatus(self.client.get(self._get_url('import')), 200)
+
+ # Test POST with permission
+ self.assertHttpStatus(self.client.post(self._get_url('import'), data), 302)
+ self.assertEqual(self._get_queryset().count(), initial_count + 1)
+
class ProviderAccountTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = ProviderAccount
@@ -287,10 +336,7 @@ class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
}
-class CircuitTerminationTestCase(
- ViewTestCases.EditObjectViewTestCase,
- ViewTestCases.DeleteObjectViewTestCase,
-):
+class CircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = CircuitTermination
@classmethod
@@ -327,6 +373,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')
diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py
index 55a192c64..5c0ab99ee 100644
--- a/netbox/circuits/urls.py
+++ b/netbox/circuits/urls.py
@@ -48,7 +48,11 @@ urlpatterns = [
path('circuits/
+
+ {% else %}
+
+
+ {% trans "Circuit" %}
+
+ {{ object.circuit|linkify }}
+
+
+
+ {% include 'circuits/inc/circuit_termination_fields.html' with termination=object %}
+ {% trans "Provider" %}
+
+ {{ object.circuit.provider|linkify }}
+
+
- {% if termination.site %}
-
-
- {% trans "Site" %}
-
- {% if termination.site.region %}
- {{ termination.site.region|linkify }} /
- {% endif %}
- {{ termination.site|linkify }}
-
-
-
- {% else %}
- {% trans "Termination" %}
-
- {% if termination.mark_connected %}
-
- {% trans "Marked as connected" %}
- {% elif termination.cable %}
- {{ termination.cable }} {% trans "to" %}
- {% for peer in termination.link_peers %}
- {% if peer.device %}
- {{ peer.device|linkify }}
-
- {% elif peer.circuit %}
- {{ peer.circuit|linkify }}
- {% endif %}
- {{ peer|linkify }}{% if not forloop.last %},{% endif %}
- {% endfor %}
-
-
- {% endif %}
- {% trans "Provider Network" %}
- {{ termination.provider_network.provider|linkify }} / {{ termination.provider_network|linkify }}
-
-
- {% trans "Speed" %}
-
- {% if termination.port_speed and termination.upstream_speed %}
- {{ termination.port_speed|humanize_speed }}
- {{ termination.upstream_speed|humanize_speed }}
- {% elif termination.port_speed %}
- {{ termination.port_speed|humanize_speed }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
-
-
-
- {% trans "Cross-Connect" %}
- {{ termination.xconnect_id|placeholder }}
-
-
- {% trans "Patch Panel/Port" %}
- {{ termination.pp_info|placeholder }}
-
-
+ {% include 'circuits/inc/circuit_termination_fields.html' with termination=termination %}
{% trans "Description" %}
- {{ termination.description|placeholder }}
-
{% trans "Tags" %}
diff --git a/netbox/templates/circuits/inc/circuit_termination_fields.html b/netbox/templates/circuits/inc/circuit_termination_fields.html
new file mode 100644
index 000000000..97d194f24
--- /dev/null
+++ b/netbox/templates/circuits/inc/circuit_termination_fields.html
@@ -0,0 +1,90 @@
+{% load helpers %}
+{% load i18n %}
+
+{% if termination.site %}
+
+
+ {% trans "Site" %}
+
+ {% if termination.site.region %}
+ {{ termination.site.region|linkify }} /
+ {% endif %}
+ {{ termination.site|linkify }}
+
+
+
+{% else %}
+ {% trans "Termination" %}
+
+ {% if termination.mark_connected %}
+
+ {% trans "Marked as connected" %}
+ {% elif termination.cable %}
+ {{ termination.cable }} {% trans "to" %}
+ {% for peer in termination.link_peers %}
+ {% if peer.device %}
+ {{ peer.device|linkify }}
+
+ {% elif peer.circuit %}
+ {{ peer.circuit|linkify }}
+ {% endif %}
+ {{ peer|linkify }}{% if not forloop.last %},{% endif %}
+ {% endfor %}
+
+
+{% endif %}
+ {% trans "Provider Network" %}
+ {{ termination.provider_network.provider|linkify }} / {{ termination.provider_network|linkify }}
+
+
+ {% trans "Speed" %}
+
+ {% if termination.port_speed and termination.upstream_speed %}
+ {{ termination.port_speed|humanize_speed }}
+ {{ termination.upstream_speed|humanize_speed }}
+ {% elif termination.port_speed %}
+ {{ termination.port_speed|humanize_speed }}
+ {% else %}
+ {{ ''|placeholder }}
+ {% endif %}
+
+
+
+ {% trans "Cross-Connect" %}
+ {{ termination.xconnect_id|placeholder }}
+
+
+ {% trans "Patch Panel/Port" %}
+ {{ termination.pp_info|placeholder }}
+
+
diff --git a/netbox/templates/extras/script.html b/netbox/templates/extras/script.html
index 8a4f86579..fe616dc8a 100644
--- a/netbox/templates/extras/script.html
+++ b/netbox/templates/extras/script.html
@@ -14,38 +14,43 @@
{% trans "You do not have permission to run scripts" %}.
{% endif %}
- {% trans "Description" %}
+ {{ termination.description|placeholder }}
+