Merge branch 'develop' into feature

This commit is contained in:
Arthur Hanson 2024-10-28 14:29:48 -07:00
commit 99904c1518
39 changed files with 76608 additions and 87922 deletions

View File

@ -14,7 +14,7 @@ body:
attributes: attributes:
label: NetBox version label: NetBox version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v4.1.4 placeholder: v4.1.5
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
@ -36,9 +36,8 @@ body:
options: options:
- I volunteer to perform this work (if approved) - I volunteer to perform this work (if approved)
- I'm a NetBox Labs customer - I'm a NetBox Labs customer
- This is a very minor change
- N/A - N/A
default: 3 default: 2
validations: validations:
required: true required: true
- type: textarea - type: textarea

View File

@ -31,16 +31,15 @@ body:
options: options:
- I volunteer to perform this work (if approved) - I volunteer to perform this work (if approved)
- I'm a NetBox Labs customer - I'm a NetBox Labs customer
- This is preventing me from using NetBox
- N/A - N/A
default: 3 default: 2
validations: validations:
required: true required: true
- type: input - type: input
attributes: attributes:
label: NetBox Version label: NetBox Version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v4.1.4 placeholder: v4.1.5
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -1,5 +1,5 @@
<div align="center"> <div align="center">
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" /> <img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo_light.svg" width="400" alt="NetBox logo" />
<p><strong>The cornerstone of every automated network</strong></p> <p><strong>The cornerstone of every automated network</strong></p>
<a href="https://github.com/netbox-community/netbox/releases"><img src="https://img.shields.io/github/v/release/netbox-community/netbox" alt="Latest release" /></a> <a href="https://github.com/netbox-community/netbox/releases"><img src="https://img.shields.io/github/v/release/netbox-community/netbox" alt="Latest release" /></a>
<a href="https://github.com/netbox-community/netbox/blob/master/LICENSE.txt"><img src="https://img.shields.io/badge/license-Apache_2.0-blue.svg" alt="License" /></a> <a href="https://github.com/netbox-community/netbox/blob/master/LICENSE.txt"><img src="https://img.shields.io/badge/license-Apache_2.0-blue.svg" alt="License" /></a>

View File

@ -42,7 +42,7 @@ django-rich
# Django integration for RQ (Reqis queuing) # Django integration for RQ (Reqis queuing)
# https://github.com/rq/django-rq/blob/master/CHANGELOG.md # https://github.com/rq/django-rq/blob/master/CHANGELOG.md
django-rq django-rq<3.0
# Abstraction models for rendering and paginating HTML tables # Abstraction models for rendering and paginating HTML tables
# https://github.com/jieter/django-tables2/blob/master/CHANGELOG.md # https://github.com/jieter/django-tables2/blob/master/CHANGELOG.md
@ -116,6 +116,10 @@ PyYAML
# https://github.com/psf/requests/blob/main/HISTORY.md # https://github.com/psf/requests/blob/main/HISTORY.md
requests requests
# rq
# https://github.com/rq/rq/blob/master/CHANGES.md
rq<2.0
# Social authentication framework # Social authentication framework
# https://github.com/python-social-auth/social-core/blob/master/CHANGELOG.md # https://github.com/python-social-auth/social-core/blob/master/CHANGELOG.md
social-auth-core social-auth-core

View File

@ -1,14 +1,26 @@
# NetBox v4.1 # NetBox v4.1
## v4.1.5 (FUTURE) ## v4.1.5 (2024-10-28)
### Enhancements
* [#17789](https://github.com/netbox-community/netbox/issues/17789) - Provide a single "scope" field for bulk editing VLAN group scope assignments
### Bug Fixes ### Bug Fixes
* [#17358](https://github.com/netbox-community/netbox/issues/17358) - Fix validation of overlapping IP ranges
* [#17374](https://github.com/netbox-community/netbox/issues/17374) - Fix styling of highlighted table rows in dark mode
* [#17460](https://github.com/netbox-community/netbox/issues/17460) - Ensure bulk action buttons are consistent for device type components
* [#17635](https://github.com/netbox-community/netbox/issues/17635) - Ensure AbortTransaction is caught when running a custom script with `commit=False`
* [#17685](https://github.com/netbox-community/netbox/issues/17685) - Ensure background jobs are validated before being scheduled
* [#17710](https://github.com/netbox-community/netbox/issues/17710) - Remove cached fields on CableTermination model from GraphQL API * [#17710](https://github.com/netbox-community/netbox/issues/17710) - Remove cached fields on CableTermination model from GraphQL API
* [#17740](https://github.com/netbox-community/netbox/issues/17740) - Ensure support for image attachments with a `.webp` file extension * [#17740](https://github.com/netbox-community/netbox/issues/17740) - Ensure support for image attachments with a `.webp` file extension
* [#17749](https://github.com/netbox-community/netbox/issues/17749) - Restore missing `devicetypes` and `children` fields for several objects in GraphQL API * [#17749](https://github.com/netbox-community/netbox/issues/17749) - Restore missing `devicetypes` and `children` fields for several objects in GraphQL API
* [#17754](https://github.com/netbox-community/netbox/issues/17754) - Remove paginator from version history table under plugin view * [#17754](https://github.com/netbox-community/netbox/issues/17754) - Remove paginator from version history table under plugin view
* [#17759](https://github.com/netbox-community/netbox/issues/17759) - Retain `job_timeout` value when scheduling a recurring custom script * [#17759](https://github.com/netbox-community/netbox/issues/17759) - Retain `job_timeout` value when scheduling a recurring custom script
* [#17774](https://github.com/netbox-community/netbox/issues/17774) - Fix SSO login support for Entra ID (formerly Azure AD)
* [#17802](https://github.com/netbox-community/netbox/issues/17802) - Fix background color for bulk rename buttons in list views
* [#17838](https://github.com/netbox-community/netbox/issues/17838) - Adjust `manage.py` to reference `python3` executable
--- ---

View File

@ -130,7 +130,7 @@ class Job(models.Model):
super().clean() super().clean()
# Validate the assigned object type # Validate the assigned object type
if self.object_type not in ObjectType.objects.with_feature('jobs'): if self.object_type and self.object_type not in ObjectType.objects.with_feature('jobs'):
raise ValidationError( raise ValidationError(
_("Jobs cannot be assigned to this object type ({type}).").format(type=self.object_type) _("Jobs cannot be assigned to this object type ({type}).").format(type=self.object_type)
) )
@ -223,7 +223,7 @@ class Job(models.Model):
rq_queue_name = get_queue_for_model(object_type.model if object_type else None) rq_queue_name = get_queue_for_model(object_type.model if object_type else None)
queue = django_rq.get_queue(rq_queue_name) queue = django_rq.get_queue(rq_queue_name)
status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING
job = Job.objects.create( job = Job(
object_type=object_type, object_type=object_type,
object_id=object_id, object_id=object_id,
name=name, name=name,
@ -233,6 +233,8 @@ class Job(models.Model):
user=user, user=user,
job_id=uuid.uuid4() job_id=uuid.uuid4()
) )
job.full_clean()
job.save()
# Run the job immediately, rather than enqueuing it as a background task. Note that this is a synchronous # Run the job immediately, rather than enqueuing it as a background task. Note that this is a synchronous
# (blocking) operation, and execution will pause until the job completes. # (blocking) operation, and execution will pause until the job completes.

View File

@ -162,7 +162,7 @@ class Cable(PrimaryModel):
if self.length is not None and not self.length_unit: if self.length is not None and not self.length_unit:
raise ValidationError(_("Must specify a unit when setting a cable length")) raise ValidationError(_("Must specify a unit when setting a cable length"))
if self._state.adding and (not self.a_terminations or not self.b_terminations): if self._state.adding and self.pk is None and (not self.a_terminations or not self.b_terminations):
raise ValidationError(_("Must define A and B terminations when creating a new cable.")) raise ValidationError(_("Must define A and B terminations when creating a new cable."))
if self._terminations_modified: if self._terminations_modified:

View File

@ -105,6 +105,8 @@ IPAddressField.register_lookup(lookups.NetIn)
IPAddressField.register_lookup(lookups.NetHostContained) IPAddressField.register_lookup(lookups.NetHostContained)
IPAddressField.register_lookup(lookups.NetFamily) IPAddressField.register_lookup(lookups.NetFamily)
IPAddressField.register_lookup(lookups.NetMaskLength) IPAddressField.register_lookup(lookups.NetMaskLength)
IPAddressField.register_lookup(lookups.Host)
IPAddressField.register_lookup(lookups.Inet)
class ASNField(models.BigIntegerField): class ASNField(models.BigIntegerField):

View File

@ -626,15 +626,15 @@ class IPRange(ContactsMixin, PrimaryModel):
}) })
# Check for overlapping ranges # Check for overlapping ranges
overlapping_range = IPRange.objects.exclude(pk=self.pk).filter(vrf=self.vrf).filter( overlapping_ranges = IPRange.objects.exclude(pk=self.pk).filter(vrf=self.vrf).filter(
Q(start_address__gte=self.start_address, start_address__lte=self.end_address) | # Starts inside Q(start_address__host__inet__gte=self.start_address.ip, start_address__host__inet__lte=self.end_address.ip) | # Starts inside
Q(end_address__gte=self.start_address, end_address__lte=self.end_address) | # Ends inside Q(end_address__host__inet__gte=self.start_address.ip, end_address__host__inet__lte=self.end_address.ip) | # Ends inside
Q(start_address__lte=self.start_address, end_address__gte=self.end_address) # Starts & ends outside Q(start_address__host__inet__lte=self.start_address.ip, end_address__host__inet__gte=self.end_address.ip) # Starts & ends outside
).first() )
if overlapping_range: if overlapping_ranges.exists():
raise ValidationError( raise ValidationError(
_("Defined addresses overlap with range {overlapping_range} in VRF {vrf}").format( _("Defined addresses overlap with range {overlapping_range} in VRF {vrf}").format(
overlapping_range=overlapping_range, overlapping_range=overlapping_ranges.first(),
vrf=self.vrf vrf=self.vrf
)) ))

View File

@ -36,6 +36,35 @@ class TestAggregate(TestCase):
self.assertEqual(aggregate.get_utilization(), 100) self.assertEqual(aggregate.get_utilization(), 100)
class TestIPRange(TestCase):
def test_overlapping_range(self):
iprange_192_168 = IPRange.objects.create(start_address=IPNetwork('192.168.0.1/22'), end_address=IPNetwork('192.168.0.49/22'))
iprange_192_168.clean()
iprange_3_1_99 = IPRange.objects.create(start_address=IPNetwork('1.2.3.1/24'), end_address=IPNetwork('1.2.3.99/24'))
iprange_3_1_99.clean()
iprange_3_100_199 = IPRange.objects.create(start_address=IPNetwork('1.2.3.100/24'), end_address=IPNetwork('1.2.3.199/24'))
iprange_3_100_199.clean()
iprange_3_200_255 = IPRange.objects.create(start_address=IPNetwork('1.2.3.200/24'), end_address=IPNetwork('1.2.3.255/24'))
iprange_3_200_255.clean()
iprange_4_1_99 = IPRange.objects.create(start_address=IPNetwork('1.2.4.1/24'), end_address=IPNetwork('1.2.4.99/24'))
iprange_4_1_99.clean()
iprange_4_200 = IPRange.objects.create(start_address=IPNetwork('1.2.4.200/24'), end_address=IPNetwork('1.2.4.255/24'))
iprange_4_200.clean()
# Overlapping range entirely within existing
with self.assertRaises(ValidationError):
iprange_3_123_124 = IPRange.objects.create(start_address=IPNetwork('1.2.3.123/26'), end_address=IPNetwork('1.2.3.124/26'))
iprange_3_123_124.clean()
# Overlapping range starting within existing
with self.assertRaises(ValidationError):
iprange_4_98_101 = IPRange.objects.create(start_address=IPNetwork('1.2.4.98/24'), end_address=IPNetwork('1.2.4.101/24'))
iprange_4_98_101.clean()
# Overlapping range ending within existing
with self.assertRaises(ValidationError):
iprange_4_198_201 = IPRange.objects.create(start_address=IPNetwork('1.2.4.198/24'), end_address=IPNetwork('1.2.4.201/24'))
iprange_4_198_201.clean()
class TestPrefix(TestCase): class TestPrefix(TestCase):
def test_get_duplicates(self): def test_get_duplicates(self):

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
import os import os
import sys import sys

View File

@ -5,7 +5,7 @@ from django.utils import timezone
from django_rq import get_queue from django_rq import get_queue
from ..jobs import * from ..jobs import *
from core.models import Job from core.models import DataSource, Job
from core.choices import JobStatusChoices from core.choices import JobStatusChoices
@ -68,7 +68,7 @@ class EnqueueTest(JobRunnerTestCase):
""" """
def test_enqueue(self): def test_enqueue(self):
instance = Job() instance = DataSource()
for i in range(1, 3): for i in range(1, 3):
job = TestJobRunner.enqueue(instance, schedule_at=self.get_schedule_at()) job = TestJobRunner.enqueue(instance, schedule_at=self.get_schedule_at())
@ -76,13 +76,13 @@ class EnqueueTest(JobRunnerTestCase):
self.assertEqual(TestJobRunner.get_jobs(instance).count(), i) self.assertEqual(TestJobRunner.get_jobs(instance).count(), i)
def test_enqueue_once(self): def test_enqueue_once(self):
job = TestJobRunner.enqueue_once(instance=Job(), schedule_at=self.get_schedule_at()) job = TestJobRunner.enqueue_once(instance=DataSource(), schedule_at=self.get_schedule_at())
self.assertIsInstance(job, Job) self.assertIsInstance(job, Job)
self.assertEqual(job.name, TestJobRunner.__name__) self.assertEqual(job.name, TestJobRunner.__name__)
def test_enqueue_once_twice_same(self): def test_enqueue_once_twice_same(self):
instance = Job() instance = DataSource()
schedule_at = self.get_schedule_at() schedule_at = self.get_schedule_at()
job1 = TestJobRunner.enqueue_once(instance, schedule_at=schedule_at) job1 = TestJobRunner.enqueue_once(instance, schedule_at=schedule_at)
job2 = TestJobRunner.enqueue_once(instance, schedule_at=schedule_at) job2 = TestJobRunner.enqueue_once(instance, schedule_at=schedule_at)
@ -91,7 +91,7 @@ class EnqueueTest(JobRunnerTestCase):
self.assertEqual(TestJobRunner.get_jobs(instance).count(), 1) self.assertEqual(TestJobRunner.get_jobs(instance).count(), 1)
def test_enqueue_once_twice_different_schedule_at(self): def test_enqueue_once_twice_different_schedule_at(self):
instance = Job() instance = DataSource()
job1 = TestJobRunner.enqueue_once(instance, schedule_at=self.get_schedule_at()) job1 = TestJobRunner.enqueue_once(instance, schedule_at=self.get_schedule_at())
job2 = TestJobRunner.enqueue_once(instance, schedule_at=self.get_schedule_at(2)) job2 = TestJobRunner.enqueue_once(instance, schedule_at=self.get_schedule_at(2))
@ -100,7 +100,7 @@ class EnqueueTest(JobRunnerTestCase):
self.assertEqual(TestJobRunner.get_jobs(instance).count(), 1) self.assertEqual(TestJobRunner.get_jobs(instance).count(), 1)
def test_enqueue_once_twice_different_interval(self): def test_enqueue_once_twice_different_interval(self):
instance = Job() instance = DataSource()
schedule_at = self.get_schedule_at() schedule_at = self.get_schedule_at()
job1 = TestJobRunner.enqueue_once(instance, schedule_at=schedule_at) job1 = TestJobRunner.enqueue_once(instance, schedule_at=schedule_at)
job2 = TestJobRunner.enqueue_once(instance, schedule_at=schedule_at, interval=60) job2 = TestJobRunner.enqueue_once(instance, schedule_at=schedule_at, interval=60)
@ -112,7 +112,7 @@ class EnqueueTest(JobRunnerTestCase):
self.assertEqual(TestJobRunner.get_jobs(instance).count(), 1) self.assertEqual(TestJobRunner.get_jobs(instance).count(), 1)
def test_enqueue_once_with_enqueue(self): def test_enqueue_once_with_enqueue(self):
instance = Job() instance = DataSource()
job1 = TestJobRunner.enqueue_once(instance, schedule_at=self.get_schedule_at(2)) job1 = TestJobRunner.enqueue_once(instance, schedule_at=self.get_schedule_at(2))
job2 = TestJobRunner.enqueue(instance, schedule_at=self.get_schedule_at()) job2 = TestJobRunner.enqueue(instance, schedule_at=self.get_schedule_at())
@ -120,7 +120,7 @@ class EnqueueTest(JobRunnerTestCase):
self.assertEqual(TestJobRunner.get_jobs(instance).count(), 2) self.assertEqual(TestJobRunner.get_jobs(instance).count(), 2)
def test_enqueue_once_after_enqueue(self): def test_enqueue_once_after_enqueue(self):
instance = Job() instance = DataSource()
job1 = TestJobRunner.enqueue(instance, schedule_at=self.get_schedule_at()) job1 = TestJobRunner.enqueue(instance, schedule_at=self.get_schedule_at())
job2 = TestJobRunner.enqueue_once(instance, schedule_at=self.get_schedule_at(2)) job2 = TestJobRunner.enqueue_once(instance, schedule_at=self.get_schedule_at(2))

View File

@ -30,7 +30,7 @@
"gridstack": "10.3.1", "gridstack": "10.3.1",
"htmx.org": "1.9.12", "htmx.org": "1.9.12",
"query-string": "9.1.1", "query-string": "9.1.1",
"sass": "1.79.5", "sass": "1.80.4",
"tom-select": "2.3.1", "tom-select": "2.3.1",
"typeface-inter": "3.18.1", "typeface-inter": "3.18.1",
"typeface-roboto-mono": "1.1.13" "typeface-roboto-mono": "1.1.13"

View File

@ -2656,10 +2656,10 @@ safe-regex-test@^1.0.3:
es-errors "^1.3.0" es-errors "^1.3.0"
is-regex "^1.1.4" is-regex "^1.1.4"
sass@1.79.5: sass@1.80.4:
version "1.79.5" version "1.80.4"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.79.5.tgz#646c627601cd5f84c64f7b1485b9292a313efae4" resolved "https://registry.yarnpkg.com/sass/-/sass-1.80.4.tgz#bc0418fd796cad2f1a1309d8b4d7fe44b7027de0"
integrity sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g== integrity sha512-rhMQ2tSF5CsuuspvC94nPM9rToiAFw2h3JTrLlgmNw1MH79v8Cr3DH6KF6o6r+8oofY3iYVPUf66KzC8yuVN1w==
dependencies: dependencies:
"@parcel/watcher" "^2.4.1" "@parcel/watcher" "^2.4.1"
chokidar "^4.0.0" chokidar "^4.0.0"

View File

@ -1,3 +1,3 @@
version: "4.1.4" version: "4.1.5"
edition: "Community" edition: "Community"
published: "2024-10-15" published: "2024-10-28"

View File

@ -18,21 +18,8 @@
<button type="submit" name="_rename" <button type="submit" name="_rename"
{% formaction %}="{% url bulk_rename_view %}?return_url={{ return_url }}" {% formaction %}="{% url bulk_rename_view %}?return_url={{ return_url }}"
class="btn btn-outline-warning"> class="btn btn-outline-warning">
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename Selected
</button> </button>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% endblock bulk_edit_controls %} {% endblock bulk_edit_controls %}
{% block bulk_extra_controls %}
{{ block.super }}
{% if request.user|can_add:child_model %}
<div class="bulk-button-group">
<a href="{% url table.Meta.model|viewname:"add" %}?device_type={{ object.pk }}&return_url={{ return_url }}" class="btn btn-primary">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i>
{% trans "Add" %} {{ title }}
</a>
</div>
{% endif %}
{% endblock bulk_extra_controls %}

View File

@ -0,0 +1,2 @@
<li class="breadcrumb-item"><a href="{% url 'dcim:devicetype_list' %}?manufacturer_id={{ object.manufacturer.pk }}">{{ object.manufacturer }}</a></li>

View File

@ -0,0 +1,38 @@
{% load buttons %}
{% load helpers %}
{% load i18n %}
{% if perms.dcim.change_devicetype %}
<div class="dropdown">
<button type="button" class="btn btn-primary dropdown-toggle"data-bs-toggle="dropdown" aria-expanded="false">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Components" %}
</button>
<ul class="dropdown-menu">
{% if perms.dcim.add_consoleporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:consoleporttemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_consoleports' pk=object.pk %}">{% trans "Console Ports" %}</a></li>
{% endif %}
{% if perms.dcim.add_consoleserverporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:consoleserverporttemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_consoleserverports' pk=object.pk %}">{% trans "Console Server Ports" %}</a></li>
{% endif %}
{% if perms.dcim.add_powerporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:powerporttemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_powerports' pk=object.pk %}">{% trans "Power Ports" %}</a></li>
{% endif %}
{% if perms.dcim.add_poweroutlettemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:poweroutlettemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_poweroutlets' pk=object.pk %}">{% trans "Power Outlets" %}</a></li>
{% endif %}
{% if perms.dcim.add_interfacetemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:interfacetemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_interfaces' pk=object.pk %}">{% trans "Interfaces" %}</a></li>
{% endif %}
{% if perms.dcim.add_frontporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:frontporttemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_frontports' pk=object.pk %}">{% trans "Front Ports" %}</a></li>
{% endif %}
{% if perms.dcim.add_rearporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:rearporttemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_rearports' pk=object.pk %}">{% trans "Rear Ports" %}</a></li>
{% endif %}
{% if perms.dcim.add_modulebaytemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:modulebaytemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_modulebays' pk=object.pk %}">{% trans "Module Bays" %}</a></li>
{% endif %}
</ul>
</div>
{% endif %}

View File

@ -1,9 +1,20 @@
{% extends 'dcim/moduletype/base.html' %} {% extends 'generic/object.html' %}
{% load buttons %} {% load buttons %}
{% load helpers %} {% load helpers %}
{% load plugins %} {% load plugins %}
{% load i18n %} {% load i18n %}
{% block title %}{{ object.manufacturer }} {{ object.model }}{% endblock %}
{% block breadcrumbs %}
{{ block.super }}
{% include 'dcim/inc/devicetype_breadcrumbs.html' %}
{% endblock %}
{% block extra_controls %}
{% include 'dcim/inc/moduletype_buttons.html' %}
{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col col-md-6"> <div class="col col-md-6">

View File

@ -1,48 +0,0 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block title %}{{ object.manufacturer }} {{ object.model }}{% endblock %}
{% block breadcrumbs %}
{{ block.super }}
<li class="breadcrumb-item"><a href="{% url 'dcim:moduletype_list' %}?manufacturer_id={{ object.manufacturer.pk }}">{{ object.manufacturer }}</a></li>
{% endblock %}
{% block extra_controls %}
{% if perms.dcim.change_devicetype %}
<div class="dropdown">
<button type="button" class="btn btn-primary dropdown-toggle"data-bs-toggle="dropdown" aria-expanded="false">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Components" %}
</button>
<ul class="dropdown-menu">
{% if perms.dcim.add_consoleporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:consoleporttemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_consoleports' pk=object.pk %}">{% trans "Console Ports" %}</a></li>
{% endif %}
{% if perms.dcim.add_consoleserverporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:consoleserverporttemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_consoleserverports' pk=object.pk %}">{% trans "Console Server Ports" %}</a></li>
{% endif %}
{% if perms.dcim.add_powerporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:powerporttemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_powerports' pk=object.pk %}">{% trans "Power Ports" %}</a></li>
{% endif %}
{% if perms.dcim.add_poweroutlettemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:poweroutlettemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_poweroutlets' pk=object.pk %}">{% trans "Power Outlets" %}</a></li>
{% endif %}
{% if perms.dcim.add_interfacetemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:interfacetemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_interfaces' pk=object.pk %}">{% trans "Interfaces" %}</a></li>
{% endif %}
{% if perms.dcim.add_frontporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:frontporttemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_frontports' pk=object.pk %}">{% trans "Front Ports" %}</a></li>
{% endif %}
{% if perms.dcim.add_rearporttemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:rearporttemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_rearports' pk=object.pk %}">{% trans "Rear Ports" %}</a></li>
{% endif %}
{% if perms.dcim.add_modulebaytemplate %}
<li><a class="dropdown-item" href="{% url 'dcim:modulebaytemplate_add' %}?module_type={{ object.pk }}&return_url={% url 'dcim:moduletype_modulebays' pk=object.pk %}">{% trans "Module Bays" %}</a></li>
{% endif %}
</ul>
</div>
{% endif %}
{% endblock %}

View File

@ -1,44 +1,37 @@
{% extends 'dcim/moduletype/base.html' %} {% extends 'generic/object_children.html' %}
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% load helpers %} {% load helpers %}
{% load i18n %} {% load i18n %}
{% block content %} {% block title %}{{ object.manufacturer }} {{ object.model }}{% endblock %}
{% if perms.dcim.change_moduletype %}
<form method="post"> {% block breadcrumbs %}
{% csrf_token %} {{ block.super }}
<div class="card"> {% include 'dcim/inc/devicetype_breadcrumbs.html' %}
<div class="htmx-container table-responsive" id="object_list"> {% endblock %}
{% include 'htmx/table.html' %}
</div> {% block extra_controls %}
<div class="card-footer d-print-none"> {% include 'dcim/inc/moduletype_buttons.html' %}
{% if table.rows %} {% endblock %}
<button type="submit" name="_edit" {% formaction %}="{% url table.Meta.model|viewname:"bulk_rename" %}?return_url={{ return_url }}" class="btn btn-warning">
<span class="mdi mdi-pencil-outline" aria-hidden="true"></span> {% trans "Rename" %} {% block bulk_edit_controls %}
</button> {% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
<button type="submit" name="_edit" {% formaction %}="{% url table.Meta.model|viewname:"bulk_edit" %}?return_url={{ return_url }}" class="btn btn-warning"> {% if 'bulk_edit' in actions and bulk_edit_view %}
<span class="mdi mdi-pencil" aria-hidden="true"></span> {% trans "Edit" %} <button type="submit" name="_edit"
</button> {% formaction %}="{% url bulk_edit_view %}?device={{ object.pk }}&return_url={{ return_url }}"
<button type="submit" name="_delete" {% formaction %}="{% url table.Meta.model|viewname:"bulk_delete" %}?return_url={{ return_url }}" class="btn btn-danger"> class="btn btn-warning">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %} <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
</button> </button>
{% endif %} {% endif %}
<div class="float-end"> {% endwith %}
<a href="{% url table.Meta.model|viewname:"add" %}?module_type={{ object.pk }}&return_url={{ return_url }}" class="btn btn-primary"> {% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% if 'bulk_rename' in actions and bulk_rename_view %}
{% trans "Add" %} {{ title }} <button type="submit" name="_rename"
</a> {% formaction %}="{% url bulk_rename_view %}?return_url={{ return_url }}"
</div> class="btn btn-outline-warning">
<div class="clearfix"></div> <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename Selected
</div> </button>
</div>
</form>
{% else %}
<div class="card">
<h2 class="card-header">{{ title }}</h2>
<div class="htmx-container table-responsive" id="object_list">
{% include 'htmx/table.html' %}
</div>
</div>
{% endif %} {% endif %}
{% endblock content %} {% endwith %}
{% endblock bulk_edit_controls %}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -2,13 +2,13 @@ Django==5.1.2
django-cors-headers==4.5.0 django-cors-headers==4.5.0
django-debug-toolbar==4.4.6 django-debug-toolbar==4.4.6
django-filter==24.3 django-filter==24.3
django-htmx==1.19.0 django-htmx==1.21.0
django-graphiql-debug-toolbar==0.2.0 django-graphiql-debug-toolbar==0.2.0
django-mptt==0.16.0 django-mptt==0.16.0
django-pglocks==1.0.4 django-pglocks==1.0.4
django-prometheus==2.3.1 django-prometheus==2.3.1
django-redis==5.4.0 django-redis==5.4.0
django-rich==1.11.0 django-rich==1.12.0
django-rq==2.10.2 django-rq==2.10.2
django-taggit==6.1.0 django-taggit==6.1.0
django-tables2==2.7.0 django-tables2==2.7.0
@ -20,18 +20,19 @@ feedparser==6.0.11
gunicorn==23.0.0 gunicorn==23.0.0
Jinja2==3.1.4 Jinja2==3.1.4
Markdown==3.7 Markdown==3.7
mkdocs-material==9.5.41 mkdocs-material==9.5.42
mkdocstrings[python-legacy]==0.26.2 mkdocstrings[python-legacy]==0.26.2
netaddr==1.3.0 netaddr==1.3.0
nh3==0.2.18 nh3==0.2.18
Pillow==10.4.0 Pillow==11.0.0
psycopg[c,pool]==3.2.3 psycopg[c,pool]==3.2.3
PyYAML==6.0.2 PyYAML==6.0.2
requests==2.32.3 requests==2.32.3
rq==1.16.2
social-auth-app-django==5.4.2 social-auth-app-django==5.4.2
social-auth-core==4.5.4 social-auth-core==4.5.4
strawberry-graphql==0.246.2 strawberry-graphql==0.247.0
strawberry-graphql-django==0.48.0 strawberry-graphql-django==0.49.1
svgwrite==1.4.3 svgwrite==1.4.3
tablib==3.7.0 tablib==3.7.0
tzdata==2024.2 tzdata==2024.2