Compare commits

...

34 Commits

Author SHA1 Message Date
Jason Novinger
290e4afaa0 Release v4.3.6 (#20081)
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
2025-08-12 12:15:08 -05:00
github-actions
ca95050b7d Update source translation strings
Some checks are pending
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-08-12 05:02:37 +00:00
m-hau
34e4ccb212 Fixes #20056: Add rf_role to generate_schema.json (#20071)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-08-11 10:40:01 -07:00
Jonathan Ramstedt
fcb49f9881 Closes #19728: add c18 power port type (#20064)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-08-11 08:41:14 -05:00
Jason Novinger
7e40f40248 Closes #20060: Update v4.3.5 release docs to note impact of fixing #18900 2025-08-11 08:31:03 -04:00
github-actions
8e08524fed Update source translation strings
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
2025-08-09 05:03:34 +00:00
Jason Novinger
8bb47dad0f Fixes #20023: Add GiST index on Prefix.prefix for net contains ops (#20059)
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
Resolves performance issue where prefix deletion with 2000+ children
took 5-10 minutes due to sequential scans in hierarchy depth/children
calculations. Adding PostgreSQL GiST index with inet_ops enables efficient
network containment operators (>>, <<, <<=) in annotate_hierarchy() queries.

Performance impact:
- 30-60x speedup: 5-10 minutes → 10 seconds for large prefix deletions
- Real-world validation: 4s migration time on 1.24M prefix dataset
- Storage cost: 47MB index (11% of table storage, 38 bytes per prefix)

Works in conjunction with existing B-tree indexes on vrf_id for optimal
query performance. Benefits all network containment operations including
hierarchy navigation, aggregate views, and available IP/prefix calculations.
2025-08-08 14:14:55 -05:00
Daniel Sheppard
5d7c8318aa Fixes: #19996 - Correct dynamic query parameters for IP Address field in Add/Edit Service form (#20040)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
* Fixes: #19996 - Correct dynamic query parameters for IP Address field in Add/Edit Service form

* Remove debug and do some cleanup
2025-08-08 09:52:03 -04:00
bctiemann
2d495d4f32 Merge pull request #20026 from netbox-community/19998-fixes-missing-changelog-cleared-tags
Fixes: #19998 - Add changelog entry when clearing M2M fields
2025-08-08 06:17:30 -04:00
github-actions
cea83f31b8 Update source translation strings
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-08-08 05:09:12 +00:00
Daniel Sheppard
6c0dc8b630 Correct mistake made on determination of whether it is a tag or not
Some checks failed
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
2025-08-07 21:12:25 -05:00
bctiemann
1c86f81298 Merge pull request #20034 from netbox-community/20033-bookmark-bulk-deletion
Fixes #20033: Fix exception when bulk deleting bookmarks
2025-08-07 20:29:10 -04:00
Daniel Sheppard
630d7aa4c2 Clarify additional branch functionality
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
2025-08-07 08:27:13 -05:00
Daniel Sheppard
043275df19 Clarify label 2025-08-07 08:24:54 -05:00
Jonathan Ramstedt
122f612750 Fixes #19379: allow standalone id in vlan-ids range list (#20024)
Some checks failed
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
* Fixes #19379: allow standalone id in vlan-ids range list

* Misc cleanup

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-08-07 08:56:07 -04:00
github-actions
65b36fd594 Update source translation strings
Some checks are pending
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-08-07 05:08:34 +00:00
Jeremy Stretch
e828ca5cb4 Fixes #20030: Fix height of object list action buttons & others (#20036)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-08-06 13:49:52 -07:00
Jeremy Stretch
fce10c73b7 Closes #17222: Improve visibility of notifications icon (#20035) 2025-08-06 14:28:01 -05:00
bctiemann
0cf76bc5c7 Merge pull request #20021 from netbox-community/19999-script-list-widget-misformatted
Fixes #19999: Script list dashboard widget now displays correctly
2025-08-06 15:08:56 -04:00
Jeremy Stretch
11f228cae9 Fixes #20033: Fix exception when bulk deleting bookmarks 2025-08-06 10:29:17 -04:00
Daniel Sheppard
a86cd9dfc6 Clarify comment 2025-08-05 15:49:01 -05:00
Daniel Sheppard
15541c6440 Fixes: #19998 - Add changelog entry when clearing M2M fields 2025-08-05 15:28:41 -05:00
github-actions
6ce3012f93 Update source translation strings
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
2025-08-05 05:08:54 +00:00
bctiemann
fec6cf705f Merge pull request #20015 from mraerino/graphql-contains-prefix
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled
Implement `contains` filter for IPAM prefixes and IP ranges
2025-08-04 18:16:19 -04:00
Jason Novinger
9c6d0d1ddc Fixes #19999: Script list widget now displays correctly
- Extract script list content into reusable partial template
- Add object-list CSS class for proper embedded table styling
- Hide module headers and management actions in widget context
- Use compact buttons with icon-only labels for widgets
- Add test coverage for embedded parameter handling

The embedded version now renders cleanly in dashboard widgets while
preserving full functionality in the main script list page.
2025-08-04 16:52:11 -05:00
Jason Novinger
47359d9284 Fixes #20017: ensures full ChangeLog change is highlighted (#20018) 2025-08-04 14:27:39 -07:00
Jeremy Stretch
669df62cde Closes #18873: Add a request timeout parameter to the RSS feed dashboard widget (#20004) 2025-08-04 14:23:33 -07:00
Marcus Weiner
9df0bdcfaf Fixes #19622: Allow loading graphql query from URL (#20014)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
2025-08-04 09:28:59 -05:00
Jad Seifeddine
d222913716 Fixes: #19917 - Fix MAC address pagination duplicates by adding 'pk' to model ordering (#19961)
* Fix MAC address pagination duplicates by adding 'pk' to model ordering

Add 'pk' to MACAddress model ordering to ensure deterministic results
when multiple MAC addresses have the same value. This prevents the same
MAC address from appearing on multiple pages during pagination.

The issue occurred because Django's default ordering by 'mac_address'
alone is non-deterministic when multiple records share the same MAC
address value, causing inconsistent pagination results when the same
MAC address is assigned to multiple interfaces on a device.

Added regression test that verifies MAC addresses with identical values
are properly ordered by their primary key, ensuring consistent pagination
behavior across the application.

Fixes netbox-community#19917

* Remove test

* Resolve migration conflict

---------

Co-authored-by: Jad Seifeddine <jseifeddine@macquarietelecom.com>
Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2025-08-04 10:15:05 -04:00
Jason Novinger
2c09973e01 Fixes #20009: Fix DOM-based XSS vulnerability in search export functionality
Replace direct string concatenation with URLSearchParams to properly
encode user input in export link URLs, preventing injection of malicious
parameters or scripts through the search functionality.

Resolves CodeQL Alert #63 (js/xss-through-dom)
2025-08-04 09:12:49 -04:00
Marcus Weiner
4506c809d8 Fix formatting 2025-08-03 15:32:55 +02:00
Marcus Weiner
5d194214aa Implement contains filter for IPAM IP ranges 2025-08-03 15:32:14 +02:00
Marcus Weiner
0827198cad Implement contains filter for IPAM prefixes 2025-08-03 15:19:58 +02:00
github-actions
bb83187505 Update source translation strings
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
2025-08-02 05:05:04 +00:00
79 changed files with 8848 additions and 8294 deletions

View File

@@ -15,7 +15,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v4.3.5
placeholder: v4.3.6
validations:
required: true
- type: dropdown

View File

@@ -27,7 +27,7 @@ body:
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
placeholder: v4.3.5
placeholder: v4.3.6
validations:
required: true
- type: dropdown

View File

@@ -95,6 +95,7 @@
"iec-60320-c8",
"iec-60320-c14",
"iec-60320-c16",
"iec-60320-c18",
"iec-60320-c20",
"iec-60320-c22",
"iec-60309-p-n-e-4h",
@@ -209,6 +210,7 @@
"iec-60320-c7",
"iec-60320-c13",
"iec-60320-c15",
"iec-60320-c17",
"iec-60320-c19",
"iec-60320-c21",
"iec-60309-p-n-e-4h",
@@ -474,6 +476,13 @@
"passive-48v-2pair",
"passive-48v-4pair"
]
},
"rf_role": {
"type": "string",
"enum": [
"ap",
"station"
]
}
}
},

View File

@@ -1,5 +1,34 @@
# NetBox v4.3
## v4.3.6 (2025-08-12)
### Enhancements
* [#17222](https://github.com/netbox-community/netbox/issues/17222) - Made unread notifications more visible with improved styling and positioning
* [#18843](https://github.com/netbox-community/netbox/issues/18843) - Include color name when exporting cables
* [#18873](https://github.com/netbox-community/netbox/issues/18873) - Add a request timeout parameter to the RSS feed dashboard widget
* [#19622](https://github.com/netbox-community/netbox/issues/19622) - Allow sharing GraphQL queries as links
* [#19728](https://github.com/netbox-community/netbox/issues/19728) - Added C18 power port type for audio devices
* [#19968](https://github.com/netbox-community/netbox/issues/19968) - Improve object type selection form field when editing permissions
* [#19977](https://github.com/netbox-community/netbox/issues/19977) - Improve performance when filtering device components by site, location, or rack
### Bug Fixes
* [#19321](https://github.com/netbox-community/netbox/issues/19321) - Reduce redundant database queries when bulk importing devices
* [#19379](https://github.com/netbox-community/netbox/issues/19379) - Support singular VLAN IDs in list when editing a VLAN group
* [#19812](https://github.com/netbox-community/netbox/issues/19812) - Implement `contains` GraphQL filter for IPAM prefixes and IP ranges
* [#19917](https://github.com/netbox-community/netbox/issues/19917) - Ensure deterministic ordering of duplicate MAC addresses
* [#19996](https://github.com/netbox-community/netbox/issues/19996) - Correct dynamic query parameters for IP Address field in Add/Edit Service form
* [#19998](https://github.com/netbox-community/netbox/issues/19998) - Fix missing changelog records for deleted tags
* [#19999](https://github.com/netbox-community/netbox/issues/19999) - Corrected excessive whitespace in script list dashboard widget
* [#20001](https://github.com/netbox-community/netbox/issues/20001) - `is_api_request()` should not evaluate a request's content type
* [#20009](https://github.com/netbox-community/netbox/issues/20009) - Ensure search parameter is escaped for export links under object list views
* [#20017](https://github.com/netbox-community/netbox/issues/20017) - Fix highlighting of changed lines in changelog data
* [#20023](https://github.com/netbox-community/netbox/issues/20023) - Add GiST index on prefixes table to vastly improve bulk deletion time
* [#20030](https://github.com/netbox-community/netbox/issues/20030) - Fix height of object list action buttons & others
* [#20033](https://github.com/netbox-community/netbox/issues/20033) - Fix `TypeError` exception when bulk deleting bookmarks
* [#20056](https://github.com/netbox-community/netbox/issues/20056) - Fixed missing RF role options in device type schema validation
## v4.3.5 (2025-07-29)
### Enhancements
@@ -16,6 +45,9 @@
* [#19934](https://github.com/netbox-community/netbox/issues/19934) - Added missing description field to tenant bulk edit form
* [#19956](https://github.com/netbox-community/netbox/issues/19956) - Prevent duplicate deletion records in changelog from cascading deletions
!!! note "Plugin Developer Advisory"
The fix for bug [#18900](https://github.com/netbox-community/netbox/issues/18900) now raises explicit exceptions when API endpoints attempt to paginate unordered querysets. Plugin maintainers should review their API viewsets to ensure proper queryset ordering is applied before pagination, either by using `.order_by()` on querysets or by setting `ordering` in model Meta classes. Previously silent pagination issues in plugin code will now raise `QuerySetNotOrdered` exceptions and may require updates to maintain compatibility.
## v4.3.4 (2025-07-15)
### Enhancements

View File

@@ -13,6 +13,7 @@ from django_prometheus.models import model_deletes, model_inserts, model_updates
from core.choices import JobStatusChoices, ObjectChangeActionChoices
from core.events import *
from extras.events import enqueue_event
from extras.models import Tag
from extras.utils import run_validators
from netbox.config import get_config
from netbox.context import current_request, events_queue
@@ -72,6 +73,17 @@ def handle_changed_object(sender, instance, **kwargs):
# m2m_changed with objects added or removed
m2m_changed = True
event_type = OBJECT_UPDATED
elif kwargs.get('action') == 'post_clear':
# Handle clearing of an M2M field
if kwargs.get('model') == Tag and getattr(instance, '_prechange_snapshot', {}).get('tags'):
# Handle generation of M2M changes for Tags which have a previous value (ignoring changes where the
# prechange snapshot is empty)
m2m_changed = True
event_type = OBJECT_UPDATED
else:
# Other endpoints are unimpacted as they send post_add and post_remove
# This will impact changes that utilize clear() however so we may want to give consideration for this branch
return
else:
return

View File

@@ -344,6 +344,7 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_IEC_C8 = 'iec-60320-c8'
TYPE_IEC_C14 = 'iec-60320-c14'
TYPE_IEC_C16 = 'iec-60320-c16'
TYPE_IEC_C18 = 'iec-60320-c18'
TYPE_IEC_C20 = 'iec-60320-c20'
TYPE_IEC_C22 = 'iec-60320-c22'
# IEC 60309
@@ -462,6 +463,7 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_IEC_C8, 'C8'),
(TYPE_IEC_C14, 'C14'),
(TYPE_IEC_C16, 'C16'),
(TYPE_IEC_C18, 'C18'),
(TYPE_IEC_C20, 'C20'),
(TYPE_IEC_C22, 'C22'),
)),
@@ -599,6 +601,7 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_IEC_C7 = 'iec-60320-c7'
TYPE_IEC_C13 = 'iec-60320-c13'
TYPE_IEC_C15 = 'iec-60320-c15'
TYPE_IEC_C17 = 'iec-60320-c17'
TYPE_IEC_C19 = 'iec-60320-c19'
TYPE_IEC_C21 = 'iec-60320-c21'
# IEC 60309
@@ -711,6 +714,7 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_IEC_C7, 'C7'),
(TYPE_IEC_C13, 'C13'),
(TYPE_IEC_C15, 'C15'),
(TYPE_IEC_C17, 'C17'),
(TYPE_IEC_C19, 'C19'),
(TYPE_IEC_C21, 'C21'),
)),

View File

@@ -7,6 +7,7 @@ from jinja2 import FileSystemLoader, Environment
from dcim.choices import *
from netbox.choices import WeightUnitChoices
from wireless.choices import WirelessRoleChoices
TEMPLATE_FILENAME = 'devicetype_schema.jinja2'
OUTPUT_FILENAME = 'contrib/generated_schema.json'
@@ -23,6 +24,7 @@ CHOICES_MAP = {
'interface_type_choices': InterfaceTypeChoices,
'interface_poe_mode_choices': InterfacePoEModeChoices,
'interface_poe_type_choices': InterfacePoETypeChoices,
'interface_rf_role_choices': WirelessRoleChoices,
'front_port_type_choices': PortTypeChoices,
'rear_port_type_choices': PortTypeChoices,
}

View File

@@ -0,0 +1,19 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dcim', '0209_device_component_denorm_site_location'),
]
operations = [
migrations.AlterModelOptions(
name='macaddress',
options={
'ordering': ('mac_address', 'pk'),
'verbose_name': 'MAC address',
'verbose_name_plural': 'MAC addresses'
},
),
]

View File

@@ -1276,7 +1276,7 @@ class MACAddress(PrimaryModel):
)
class Meta:
ordering = ('mac_address',)
ordering = ('mac_address', 'pk',)
verbose_name = _('MAC address')
verbose_name_plural = _('MAC addresses')

View File

@@ -309,6 +309,7 @@ class RSSFeedWidget(DashboardWidget):
default_config = {
'max_entries': 10,
'cache_timeout': 3600, # seconds
'request_timeout': 3, # seconds
'requires_internet': True,
}
description = _('Embed an RSS feed from an external website.')
@@ -335,6 +336,12 @@ class RSSFeedWidget(DashboardWidget):
max_value=86400, # 24 hours
help_text=_('How long to stored the cached content (in seconds)')
)
request_timeout = forms.IntegerField(
min_value=1,
max_value=60,
required=False,
help_text=_('Timeout value for fetching the feed (in seconds)')
)
def render(self, request):
return render_to_string(self.template_name, {
@@ -366,7 +373,7 @@ class RSSFeedWidget(DashboardWidget):
url=self.config['feed_url'],
headers={'User-Agent': f'NetBox/{settings.RELEASE.version}'},
proxies=resolve_proxies(url=self.config['feed_url'], context={'client': self}),
timeout=3
timeout=self.config.get('request_timeout', 3),
)
response.raise_for_status()
except requests.exceptions.RequestException as e:

View File

@@ -849,6 +849,9 @@ class Bookmark(models.Model):
return str(self.object)
return super().__str__()
def get_absolute_url(self):
return reverse('account:bookmarks')
def clean(self):
super().clean()

View File

@@ -317,11 +317,12 @@ class TableConfigTable(NetBoxTable):
class BookmarkTable(NetBoxTable):
object_type = columns.ContentTypeColumn(
verbose_name=_('Object Types'),
verbose_name=_('Object Type'),
)
object = tables.Column(
verbose_name=_('Object'),
linkify=True
linkify=True,
orderable=False
)
actions = columns.ActionsColumn(
actions=('delete',)

View File

@@ -807,3 +807,21 @@ class NotificationTestCase(
def test_list_objects_with_constrained_permission(self):
return
class ScriptListViewTest(TestCase):
user_permissions = ['extras.view_script']
def test_script_list_embedded_parameter(self):
"""Test that ScriptListView accepts embedded parameter without error"""
url = reverse('extras:script_list')
# Test normal request
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'extras/script_list.html')
# Test embedded request
response = self.client.get(url, {'embedded': 'true'})
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'extras/inc/script_list_content.html')

View File

@@ -1282,11 +1282,18 @@ class ScriptListView(ContentTypePermissionRequiredMixin, View):
script_modules = ScriptModule.objects.restrict(request.user).prefetch_related(
'data_source', 'data_file', 'jobs'
)
return render(request, 'extras/script_list.html', {
context = {
'model': ScriptModule,
'script_modules': script_modules,
})
}
# Use partial template for dashboard widgets
template_name = 'extras/script_list.html'
if request.GET.get('embedded'):
template_name = 'extras/inc/script_list_content.html'
context['embedded'] = True
return render(request, template_name, context)
class BaseScriptView(generic.ObjectView):

View File

@@ -21,7 +21,7 @@ from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, T
from utilities.forms.utils import get_field_value
from utilities.forms.widgets import DatePicker, HTMXSelect
from utilities.templatetags.builtins.filters import bettertitle
from virtualization.models import VMInterface
from virtualization.models import VMInterface, VirtualMachine
__all__ = (
'AggregateForm',
@@ -783,10 +783,6 @@ class ServiceForm(NetBoxModelForm):
queryset=IPAddress.objects.all(),
required=False,
label=_('IP Addresses'),
query_params={
'device_id': '$device',
'virtual_machine_id': '$virtual_machine',
}
)
comments = CommentField()
@@ -815,10 +811,22 @@ class ServiceForm(NetBoxModelForm):
super().__init__(*args, **kwargs)
if (parent_object_type_id := get_field_value(self, 'parent_object_type')):
if parent_object_type_id := get_field_value(self, 'parent_object_type'):
try:
parent_type = ContentType.objects.get(pk=parent_object_type_id)
model = parent_type.model_class()
if model == Device:
self.fields['ipaddresses'].widget.add_query_params({
'device_id': '$parent',
})
elif model == VirtualMachine:
self.fields['ipaddresses'].widget.add_query_params({
'virtual_machine_id': '$parent',
})
elif model == FHRPGroup:
self.fields['ipaddresses'].widget.add_query_params({
'fhrpgroup_id': '$parent',
})
self.fields['parent'].queryset = model.objects.all()
self.fields['parent'].widget.attrs['selector'] = model._meta.label_lower
self.fields['parent'].disabled = False

View File

@@ -222,6 +222,19 @@ class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMi
return Q()
return q
@strawberry_django.filter_field()
def contains(self, value: list[str], prefix) -> Q:
if not value:
return Q()
q = Q()
for subnet in value:
net = netaddr.IPNetwork(subnet.strip())
q |= Q(
start_address__host__inet__lte=str(netaddr.IPAddress(net.first)),
end_address__host__inet__gte=str(netaddr.IPAddress(net.last)),
)
return q
@strawberry_django.filter_type(models.Prefix, lookups=True)
class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
@@ -238,6 +251,16 @@ class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, Pr
is_pool: FilterLookup[bool] | None = strawberry_django.filter_field()
mark_utilized: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter_field()
def contains(self, value: list[str], prefix) -> Q:
if not value:
return Q()
q = Q()
for subnet in value:
query = str(netaddr.IPNetwork(subnet.strip()).cidr)
q |= Q(prefix__net_contains=query)
return q
@strawberry_django.filter_type(models.RIR, lookups=True)
class RIRFilter(OrganizationalModelFilterMixin):

View File

@@ -0,0 +1,20 @@
from django.contrib.postgres.indexes import GistIndex
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('dcim', '0210_macaddress_ordering'),
('extras', '0129_fix_script_paths'),
('ipam', '0081_remove_service_device_virtual_machine_add_parent_gfk_index'),
('tenancy', '0020_remove_contactgroupmembership'),
]
operations = [
migrations.AddIndex(
model_name='prefix',
index=GistIndex(fields=['prefix'], name='ipam_prefix_gist_idx', opclasses=['inet_ops']),
),
]

View File

@@ -1,5 +1,6 @@
import netaddr
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.postgres.indexes import GistIndex
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import F
@@ -281,6 +282,13 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
ordering = (F('vrf').asc(nulls_first=True), 'prefix', 'pk') # (vrf, prefix) may be non-unique
verbose_name = _('prefix')
verbose_name_plural = _('prefixes')
indexes = [
GistIndex(
fields=['prefix'],
name='ipam_prefix_gist_idx',
opclasses=['inet_ops'],
),
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@@ -237,7 +237,11 @@ class ActionsColumn(tables.Column):
:param split_actions: When True, converts the actions dropdown menu into a split button with first action as the
direct button link and icon (default: True)
"""
attrs = {'td': {'class': 'text-end text-nowrap noprint'}}
attrs = {
'td': {
'class': 'text-end text-nowrap noprint p-1'
}
}
empty_values = ()
actions = {
'edit': ActionsItem('Edit', 'pencil', 'change', 'warning'),

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -38,7 +38,9 @@ function handleQuickSearchParams(event: Event): void {
if (quickSearchParameters != null) {
const link = document.getElementById('export_current_view') as HTMLLinkElement;
const search_parameter = `q=${quickSearchParameters.value}`;
const params = new URLSearchParams();
params.set('q', quickSearchParameters.value);
const search_parameter = params.toString();
const linkUpdated = link?.href + '&' + search_parameter;
link.setAttribute('href', linkUpdated);
}

View File

@@ -8,6 +8,8 @@ pre.change-data {
display: block;
padding-right: $spacer;
padding-left: $spacer;
width: 100%;
min-width: fit-content;
&.added {
background-color: $green;

View File

@@ -845,78 +845,78 @@
"@types/estree" "*"
"@typescript-eslint/eslint-plugin@^8.37.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz#6e5220d16f2691ab6d983c1737dd5b36e17641b7"
integrity sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz#28dffcb5272d20afe250bfeec3173263db5528a0"
integrity sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==
dependencies:
"@eslint-community/regexpp" "^4.10.0"
"@typescript-eslint/scope-manager" "8.38.0"
"@typescript-eslint/type-utils" "8.38.0"
"@typescript-eslint/utils" "8.38.0"
"@typescript-eslint/visitor-keys" "8.38.0"
"@typescript-eslint/scope-manager" "8.39.1"
"@typescript-eslint/type-utils" "8.39.1"
"@typescript-eslint/utils" "8.39.1"
"@typescript-eslint/visitor-keys" "8.39.1"
graphemer "^1.4.0"
ignore "^7.0.0"
natural-compare "^1.4.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/parser@^8.37.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.38.0.tgz#6723a5ea881e1777956b1045cba30be5ea838293"
integrity sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.39.1.tgz#7f8f9ecfc7e172d67e42c366fa198e42324e5d50"
integrity sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==
dependencies:
"@typescript-eslint/scope-manager" "8.38.0"
"@typescript-eslint/types" "8.38.0"
"@typescript-eslint/typescript-estree" "8.38.0"
"@typescript-eslint/visitor-keys" "8.38.0"
"@typescript-eslint/scope-manager" "8.39.1"
"@typescript-eslint/types" "8.39.1"
"@typescript-eslint/typescript-estree" "8.39.1"
"@typescript-eslint/visitor-keys" "8.39.1"
debug "^4.3.4"
"@typescript-eslint/project-service@8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.38.0.tgz#4900771f943163027fd7d2020a062892056b5e2f"
integrity sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==
"@typescript-eslint/project-service@8.39.1":
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.39.1.tgz#63525878d488ebf27c485f295e83434a1398f52d"
integrity sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.38.0"
"@typescript-eslint/types" "^8.38.0"
"@typescript-eslint/tsconfig-utils" "^8.39.1"
"@typescript-eslint/types" "^8.39.1"
debug "^4.3.4"
"@typescript-eslint/scope-manager@8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz#5a0efcb5c9cf6e4121b58f87972f567c69529226"
integrity sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==
"@typescript-eslint/scope-manager@8.39.1":
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz#1253fe3e1f2f33f08a3e438a05b5dd7faf9fbca6"
integrity sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==
dependencies:
"@typescript-eslint/types" "8.38.0"
"@typescript-eslint/visitor-keys" "8.38.0"
"@typescript-eslint/types" "8.39.1"
"@typescript-eslint/visitor-keys" "8.39.1"
"@typescript-eslint/tsconfig-utils@8.38.0", "@typescript-eslint/tsconfig-utils@^8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz#6de4ce224a779601a8df667db56527255c42c4d0"
integrity sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==
"@typescript-eslint/tsconfig-utils@8.39.1", "@typescript-eslint/tsconfig-utils@^8.39.1":
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz#17f13b4ad481e7bec7c249ee1854078645b34b12"
integrity sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==
"@typescript-eslint/type-utils@8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz#a56cd84765fa6ec135fe252b5db61e304403a85b"
integrity sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==
"@typescript-eslint/type-utils@8.39.1":
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz#642f9fb96173649e2928fea0375b1d74d31906c2"
integrity sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==
dependencies:
"@typescript-eslint/types" "8.38.0"
"@typescript-eslint/typescript-estree" "8.38.0"
"@typescript-eslint/utils" "8.38.0"
"@typescript-eslint/types" "8.39.1"
"@typescript-eslint/typescript-estree" "8.39.1"
"@typescript-eslint/utils" "8.39.1"
debug "^4.3.4"
ts-api-utils "^2.1.0"
"@typescript-eslint/types@8.38.0", "@typescript-eslint/types@^8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.38.0.tgz#297351c994976b93c82ac0f0e206c8143aa82529"
integrity sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==
"@typescript-eslint/types@8.39.1", "@typescript-eslint/types@^8.39.1":
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.39.1.tgz#f0ab996c8ab2c3b046bbf86bb1990b03529869a1"
integrity sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==
"@typescript-eslint/typescript-estree@8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz#82262199eb6778bba28a319e25ad05b1158957df"
integrity sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==
"@typescript-eslint/typescript-estree@8.39.1":
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz#8825d3ea7ea2144c577859ae489eec24ef7318a5"
integrity sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==
dependencies:
"@typescript-eslint/project-service" "8.38.0"
"@typescript-eslint/tsconfig-utils" "8.38.0"
"@typescript-eslint/types" "8.38.0"
"@typescript-eslint/visitor-keys" "8.38.0"
"@typescript-eslint/project-service" "8.39.1"
"@typescript-eslint/tsconfig-utils" "8.39.1"
"@typescript-eslint/types" "8.39.1"
"@typescript-eslint/visitor-keys" "8.39.1"
debug "^4.3.4"
fast-glob "^3.3.2"
is-glob "^4.0.3"
@@ -924,22 +924,22 @@
semver "^7.6.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/utils@8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.38.0.tgz#5f10159899d30eb92ba70e642ca6f754bddbf15a"
integrity sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==
"@typescript-eslint/utils@8.39.1":
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.39.1.tgz#58a834f89f93b786ada2cd14d77fa63c3c8f408b"
integrity sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==
dependencies:
"@eslint-community/eslint-utils" "^4.7.0"
"@typescript-eslint/scope-manager" "8.38.0"
"@typescript-eslint/types" "8.38.0"
"@typescript-eslint/typescript-estree" "8.38.0"
"@typescript-eslint/scope-manager" "8.39.1"
"@typescript-eslint/types" "8.39.1"
"@typescript-eslint/typescript-estree" "8.39.1"
"@typescript-eslint/visitor-keys@8.38.0":
version "8.38.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz#a9765a527b082cb8fc60fd8a16e47c7ad5b60ea5"
integrity sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==
"@typescript-eslint/visitor-keys@8.39.1":
version "8.39.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz#a467742a98f2fa3c03d7bed4979dc0db3850a77a"
integrity sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==
dependencies:
"@typescript-eslint/types" "8.38.0"
"@typescript-eslint/types" "8.39.1"
eslint-visitor-keys "^4.2.1"
"@ungap/structured-clone@^1.2.0":
@@ -1742,9 +1742,9 @@ eslint-plugin-import@^2.32.0:
tsconfig-paths "^3.15.0"
eslint-plugin-prettier@^5.5.1:
version "5.5.3"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.3.tgz#1f88e9220a72ac8be171eec5f9d4e4d529b5f4a0"
integrity sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==
version "5.5.4"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz#9d61c4ea11de5af704d4edf108c82ccfa7f2e61c"
integrity sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==
dependencies:
prettier-linter-helpers "^1.0.0"
synckit "^0.11.7"

View File

@@ -1,3 +1,3 @@
version: "4.3.5"
version: "4.3.6"
edition: "Community"
published: "2025-07-29"
published: "2025-08-12"

View File

@@ -4,22 +4,22 @@
<div class="card">
<h2 class="card-header d-flex justify-content-between">
{% blocktrans %}Termination{% endblocktrans %} {{ side }}
<div>
<div class="card-actions">
{% if not termination and perms.circuits.add_circuittermination %}
<a href="{% url 'circuits:circuittermination_add' %}?circuit={{ object.pk }}&term_side={{ side }}&return_url={{ object.get_absolute_url }}" class="btn btn-success lh-1">
<a href="{% url 'circuits:circuittermination_add' %}?circuit={{ object.pk }}&term_side={{ side }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-ghost-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add" %}
</a>
{% endif %}
{% if termination and perms.circuits.change_circuittermination %}
<a href="{% url 'circuits:circuittermination_edit' pk=termination.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-warning lh-1">
<a href="{% url 'circuits:circuittermination_edit' pk=termination.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-ghost-warning">
<span class="mdi mdi-pencil" aria-hidden="true"></span> {% trans "Edit" %}
</a>
<a href="{% url 'circuits:circuit_terminations_swap' pk=object.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-primary lh-1">
<a href="{% url 'circuits:circuit_terminations_swap' pk=object.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-ghost-primary">
<span class="mdi mdi-swap-vertical" aria-hidden="true"></span> {% trans "Swap" %}
</a>
{% endif %}
{% if termination and perms.circuits.delete_circuittermination %}
<a href="{% url 'circuits:circuittermination_delete' pk=termination.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-danger lh-1">
<a href="{% url 'circuits:circuittermination_delete' pk=termination.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-ghost-danger">
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Delete" %}
</a>
{% endif %}

View File

@@ -29,16 +29,16 @@
{{ peer|linkify }}{% if not forloop.last %},{% endif %}
{% endfor %}
<div class="mt-1">
<a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
<a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-sm btn-primary" 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=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="{% trans "Edit cable" %}" class="btn btn-warning lh-1">
<a href="{% url 'dcim:cable_edit' pk=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="{% trans "Edit cable" %}" class="btn btn-sm btn-warning">
<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=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="{% trans "Remove cable" %}" class="btn btn-danger lh-1">
<a href="{% url 'dcim:cable_delete' pk=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="{% trans "Remove cable" %}" class="btn btn-sm btn-danger">
<i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i> {% trans "Disconnect" %}
</a>
{% endif %}

View File

@@ -77,7 +77,7 @@
<th scope="row">{% trans "Cable" %}</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:frontport_trace' pk=object.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
<a href="{% url 'dcim:frontport_trace' pk=object.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>

View File

@@ -24,7 +24,7 @@
<i class="mdi mdi-chevron-right" aria-hidden="true"></i>
{{ term|linkify }}
{% with trace_url=term|viewname:"trace" %}
<a href="{% url trace_url pk=term.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
<a href="{% url trace_url pk=term.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
{% endwith %}
@@ -48,7 +48,7 @@
{% for term in terminations %}
{{ term|linkify }}
{% with trace_url=term|viewname:"trace" %}
<a href="{% url trace_url pk=term.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
<a href="{% url trace_url pk=term.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
{% endwith %}
@@ -68,7 +68,7 @@
{% for term in terminations %}
{{ term.circuit|linkify }} ({{ term }})
{% with trace_url=term|viewname:"trace" %}
<a href="{% url trace_url pk=term.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
<a href="{% url trace_url pk=term.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
{% endwith %}

View File

@@ -4,7 +4,7 @@
<th scope="row">{% trans "Cable" %}</th>
<td>
{{ object.cable|linkify }}
<a href="{% url trace_url pk=object.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
<a href="{% url trace_url pk=object.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>

View File

@@ -29,12 +29,12 @@
<td>{{ item.role|linkify|placeholder }}</td>
<td class="text-end d-print-none">
{% if perms.dcim.change_inventoryitem %}
<a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-warning lh-1" title="{% trans "Edit" %}">
<a href="{% url 'dcim:inventoryitem_edit' pk=item.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-warning" title="{% trans "Edit" %}">
<i class="mdi mdi-pencil" aria-hidden="true"></i>
</a>
{% endif %}
{% if perms.ipam.delete_inventoryitem %}
<a href="{% url 'dcim:inventoryitem_delete' pk=item.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-danger lh-1" title="{% trans "Delete" %}">
<a href="{% url 'dcim:inventoryitem_delete' pk=item.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-danger" title="{% trans "Delete" %}">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i>
</a>
{% endif %}

View File

@@ -208,7 +208,7 @@
<th scope="row">{% trans "Wireless Link" %}</th>
<td>
{{ object.wireless_link|linkify }}
<a href="{% url 'dcim:interface_trace' pk=object.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
<a href="{% url 'dcim:interface_trace' pk=object.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>

View File

@@ -73,7 +73,7 @@
<th scope="row">{% trans "Cable" %}</th>
<td>
{{ object.cable|linkify }}
<a href="{% url 'dcim:rearport_trace' pk=object.pk %}" class="btn btn-primary lh-1" title="{% trans "Trace" %}">
<a href="{% url 'dcim:rearport_trace' pk=object.pk %}" class="btn btn-sm btn-primary" title="{% trans "Trace" %}">
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
</a>
</td>

View File

@@ -57,7 +57,7 @@
{% trans "Output" %}
{% if job.completed %}
<div>
<a href="?export=output" class="btn btn-primary lh-1" role="button">
<a href="?export=output" class="btn btn-sm btn-primary" role="button">
<i class="mdi mdi-download" aria-hidden="true"></i> {% trans "Download" %}
</a>
</div>

View File

@@ -0,0 +1,139 @@
{% load buttons %}
{% load helpers %}
{% load perms %}
{% load i18n %}
{# Core script list content - used by both full page and embedded views #}
{% for module in script_modules %}
{% include 'inc/sync_warning.html' with object=module %}
<div class="card{% if embedded %} mb-3{% endif %}">
{% if not embedded %}
<h2 class="card-header" id="module{{ module.pk }}">
<i class="mdi mdi-file-document-outline"></i> {{ module }}
<div class="card-actions">
{% if perms.extras.edit_scriptmodule %}
<a href="{% url 'extras:scriptmodule_edit' pk=module.pk %}" class="btn btn-ghost-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
</a>
{% endif %}
{% if perms.extras.delete_scriptmodule %}
<a href="{% url 'extras:scriptmodule_delete' pk=module.pk %}" class="btn btn-ghost-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
</a>
{% endif %}
</div>
</h2>
{% endif %}
{% with scripts=module.ordered_scripts %}
{% if scripts %}
<table class="table table-hover scripts{% if embedded %} object-list table-sm{% endif %}">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Last Run" %}</th>
<th>{% trans "Status" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for script in scripts %}
{% with last_job=script.get_latest_jobs|first %}
<tr>
<td>
{% if script.is_executable %}
<a href="{% url 'extras:script' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
{% else %}
<a href="{% url 'extras:script_jobs' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
<span class="text-danger">
<i class="mdi mdi-alert" title="{% trans "Script is no longer present in the source file" %}"></i>
</span>
{% endif %}
</td>
<td>{{ script.python_class.description|markdown|placeholder }}</td>
{% if last_job %}
<td>
<a href="{% url 'extras:script_result' job_pk=last_job.pk %}">{{ last_job.created|isodatetime }}</a>
</td>
<td>
{% badge last_job.get_status_display last_job.get_status_color %}
</td>
{% else %}
<td class="text-muted">{% trans "Never" %}</td>
<td>{{ ''|placeholder }}</td>
{% endif %}
<td>
{% if request.user|can_run:script and script.is_executable %}
<div class="float-end d-print-none">
<form action="{% url 'extras:script' script.pk %}" method="post">
{% if script.python_class.commit_default %}
<input type="checkbox" name="_commit" hidden checked>
{% endif %}
{% csrf_token %}
<button type="submit" name="_run" class="btn btn-primary{% if embedded %} btn-sm{% endif %}">
{% if last_job %}
<i class="mdi mdi-replay"></i> {% if not embedded %}{% trans "Run Again" %}{% endif %}
{% else %}
<i class="mdi mdi-play"></i> {% if not embedded %}{% trans "Run Script" %}{% endif %}
{% endif %}
</button>
</form>
</div>
{% endif %}
</td>
</tr>
{% if last_job and not embedded %}
{% for test_name, data in last_job.data.tests.items %}
<tr>
<td colspan="4" class="method">
<span class="ps-3">{{ test_name }}</span>
</td>
<td class="text-end text-nowrap script-stats">
<span class="badge text-bg-success">{{ data.success }}</span>
<span class="badge text-bg-info">{{ data.info }}</span>
<span class="badge text-bg-warning">{{ data.warning }}</span>
<span class="badge text-bg-danger">{{ data.failure }}</span>
</td>
</tr>
{% endfor %}
{% elif last_job and not last_job.data.log and not embedded %}
{# legacy #}
{% for method, stats in last_job.data.items %}
<tr>
<td colspan="4" class="method">
<span class="ps-3">{{ method }}</span>
</td>
<td class="text-end text-nowrap report-stats">
<span class="badge bg-success">{{ stats.success }}</span>
<span class="badge bg-info">{{ stats.info }}</span>
<span class="badge bg-warning">{{ stats.warning }}</span>
<span class="badge bg-danger">{{ stats.failure }}</span>
</td>
</tr>
{% endfor %}
{% endif %}
{% endwith %}
{% endfor %}
</tbody>
</table>
{% else %}
<div class="card-body">
<div class="alert alert-warning" role="alert">
<i class="mdi mdi-alert"></i>
{% blocktrans with module=module.name %}Could not load scripts from module {{ module }}{% endblocktrans %}
</div>
</div>
{% endif %}
{% endwith %}
</div>
{% empty %}
<div class="alert alert-info" role="alert">
<h4 class="alert-heading">{% trans "No Scripts Found" %}</h4>
{% if perms.extras.add_scriptmodule and not embedded %}
{% url 'extras:scriptmodule_add' as create_script_url %}
{% blocktrans trimmed %}
Get started by <a href="{{ create_script_url }}">creating a script</a> from an uploaded file or data source.
{% endblocktrans %}
{% endif %}
</div>
{% endfor %}

View File

@@ -54,11 +54,11 @@
<div class="card">
<h2 class="card-header d-flex justify-content-between">
{% trans "Rendered Config" %}
<div>
{% copy_content "rendered_config" %}
<a href="?export=True" class="btn btn-primary lh-1" role="button">
<div class="card-actions">
<a href="?export=True" class="btn btn-sm btn-ghost-primary" role="button">
<i class="mdi mdi-download" aria-hidden="true"></i> {% trans "Download" %}
</a>
{% copy_content "rendered_config" %}
</div>
</h2>
<pre class="card-body" id="rendered_config">{{ rendered_config }}</pre>

View File

@@ -70,6 +70,10 @@
"poe_type": {
"type": "string",
"enum": {{ interface_poe_type_choices }}
},
"rf_role": {
"type": "string",
"enum": {{ interface_rf_role_choices }}
}
}
},

View File

@@ -19,135 +19,5 @@
{% endblock controls %}
{% block content %}
{% for module in script_modules %}
{% include 'inc/sync_warning.html' with object=module %}
<div class="card">
<h2 class="card-header" id="module{{ module.pk }}">
<i class="mdi mdi-file-document-outline"></i> {{ module }}
<div class="card-actions">
{% if perms.extras.edit_scriptmodule %}
<a href="{% url 'extras:scriptmodule_edit' pk=module.pk %}" class="btn btn-ghost-warning btn-sm">
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit" %}
</a>
{% endif %}
{% if perms.extras.delete_scriptmodule %}
<a href="{% url 'extras:scriptmodule_delete' pk=module.pk %}" class="btn btn-ghost-danger btn-sm">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
</a>
{% endif %}
</div>
</h2>
{% with scripts=module.ordered_scripts %}
{% if scripts %}
<table class="table table-hover scripts">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "Last Run" %}</th>
<th>{% trans "Status" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for script in scripts %}
{% with last_job=script.get_latest_jobs|first %}
<tr>
<td>
{% if script.is_executable %}
<a href="{% url 'extras:script' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
{% else %}
<a href="{% url 'extras:script_jobs' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.python_class.name }}</a>
<span class="text-danger">
<i class="mdi mdi-alert" title="{% trans "Script is no longer present in the source file" %}"></i>
</span>
{% endif %}
</td>
<td>{{ script.python_class.description|markdown|placeholder }}</td>
{% if last_job %}
<td>
<a href="{% url 'extras:script_result' job_pk=last_job.pk %}">{{ last_job.created|isodatetime }}</a>
</td>
<td>
{% badge last_job.get_status_display last_job.get_status_color %}
</td>
{% else %}
<td class="text-muted">{% trans "Never" %}</td>
<td>{{ ''|placeholder }}</td>
{% endif %}
<td>
{% if request.user|can_run:script and script.is_executable %}
<div class="float-end d-print-none">
<form action="{% url 'extras:script' script.pk %}" method="post">
{% if script.python_class.commit_default %}
<input type="checkbox" name="_commit" hidden checked>
{% endif %}
{% csrf_token %}
<button type="submit" name="_run" class="btn btn-primary btn-sm">
{% if last_job %}
<i class="mdi mdi-replay"></i> {% trans "Run Again" %}
{% else %}
<i class="mdi mdi-play"></i> {% trans "Run Script" %}
{% endif %}
</button>
</form>
</div>
{% endif %}
</td>
</tr>
{% if last_job %}
{% for test_name, data in last_job.data.tests.items %}
<tr>
<td colspan="4" class="method">
<span class="ps-3">{{ test_name }}</span>
</td>
<td class="text-end text-nowrap script-stats">
<span class="badge text-bg-success">{{ data.success }}</span>
<span class="badge text-bg-info">{{ data.info }}</span>
<span class="badge text-bg-warning">{{ data.warning }}</span>
<span class="badge text-bg-danger">{{ data.failure }}</span>
</td>
</tr>
{% endfor %}
{% elif not last_job.data.log %}
{# legacy #}
{% for method, stats in last_job.data.items %}
<tr>
<td colspan="4" class="method">
<span class="ps-3">{{ method }}</span>
</td>
<td class="text-end text-nowrap report-stats">
<span class="badge bg-success">{{ stats.success }}</span>
<span class="badge bg-info">{{ stats.info }}</span>
<span class="badge bg-warning">{{ stats.warning }}</span>
<span class="badge bg-danger">{{ stats.failure }}</span>
</td>
</tr>
{% endfor %}
{% endif %}
{% endwith %}
{% endfor %}
</tbody>
</table>
{% else %}
<div class="card-body">
<div class="alert alert-warning" role="alert">
<i class="mdi mdi-alert"></i>
{% blocktrans with module=module.name %}Could not load scripts from module {{ module }}{% endblocktrans %}
</div>
</div>
{% endif %}
{% endwith %}
</div>
{% empty %}
<div class="alert alert-info" role="alert">
<h4 class="alert-heading">{% trans "No Scripts Found" %}</h4>
{% if perms.extras.add_scriptmodule %}
{% url 'extras:scriptmodule_add' as create_script_url %}
{% blocktrans trimmed %}
Get started by <a href="{{ create_script_url }}">creating a script</a> from an uploaded file or data source.
{% endblocktrans %}
{% endif %}
</div>
{% endfor %}
{% include 'extras/inc/script_list_content.html' with embedded=False %}
{% endblock content %}

View File

@@ -88,6 +88,21 @@
#
`;
let sharedQuery;
const hashArgs = new URLSearchParams(window.location.hash.substring(1));
if (hashArgs.has('query')) {
sharedQuery = hashArgs.get('query');
// reset url to not motivate copying of stale URL
hashArgs.delete('query');
let remainingHash = "";
if (hashArgs.size !== 0) {
remainingHash = `#${hashArgs.toString()}`;
}
history.pushState("", document.title,
window.location.pathname + window.location.search + remainingHash
);
}
const fetchURL = window.location.href;
function httpUrlToWebSockeUrl(url) {
@@ -123,6 +138,8 @@
defaultEditorToolsVisibility: true,
plugins: [explorerPlugin],
inputValueDeprecation: true,
defaultQuery: EXAMPLE_QUERY,
query: sharedQuery,
}),
);
</script>

View File

@@ -15,14 +15,14 @@
<div class="d-block text-secondary fs-5">{{ notification.event }} {{ notification.created|timesince }} {% trans "ago" %}</div>
</div>
<div class="col-auto">
<a href="#" hx-get="{% url 'extras:notification_dismiss' pk=notification.pk %}" hx-target="closest .notifications" class="list-group-item-actions text-secondary" title="{% trans "Dismiss" %}">
<a href="#" hx-get="{% url 'extras:notification_dismiss' pk=notification.pk %}" hx-target="closest .notifications" class="list-group-item-actions text-red" title="{% trans "Dismiss" %}">
<i class="mdi mdi-close"></i>
</a>
</div>
</div>
</div>
{% empty %}
<div class="dropdown-item text-muted">
<div class="dropdown-item disabled">
{% trans "No unread notifications" %}
</div>
{% endfor %}

View File

@@ -1,10 +1,10 @@
{% load i18n %}
<div class="d-flex ms-2">
<button class="btn color-mode-toggle hide-theme-dark" title="{% trans "Enable dark mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
<button class="nav-link color-mode-toggle hide-theme-dark fs-2 p-0 text-secondary" title="{% trans "Enable dark mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
<i class="mdi mdi-lightbulb"></i>
</button>
<button class="btn color-mode-toggle hide-theme-light" title="{% trans "Enable light mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
<button class="nav-link color-mode-toggle hide-theme-light fs-2 p-0 text-secondary" title="{% trans "Enable light mode" %}" data-bs-toggle="tooltip" data-bs-placement="bottom">
<i class="mdi mdi-lightbulb-on"></i>
</button>
</div>

View File

@@ -1,6 +1,7 @@
{% if notifications %}
<span class="text-primary" id="notifications-alert" hx-swap-oob="true">
<i class="mdi mdi-bell-badge"></i>
<i class="mdi mdi-bell-ring"></i>
<span class="badge bg-red"></span>
</span>
{% else %}
<span class="text-muted" id="notifications-alert" hx-swap-oob="true">

View File

@@ -12,9 +12,9 @@
{# Notifications #}
{% with notifications=request.user.notifications.unread.exists %}
<div class="dropdown">
<a href="#" class="nav-link px-1" data-bs-toggle="dropdown" hx-get="{% url 'extras:notifications' %}" hx-target="next .notifications" aria-label="{% trans "Notifications" %}">
<button class="nav-link fs-2 p-0" data-bs-toggle="dropdown" hx-get="{% url 'extras:notifications' %}" hx-target="next .notifications" aria-label="{% trans "Notifications" %}">
{% include 'inc/notification_bell.html' %}
</a>
</button>
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow notifications"></div>
</div>
{% endwith %}

View File

@@ -40,12 +40,12 @@
<td>{{ assignment.priority }}</td>
<td class="text-end d-print-none">
{% if perms.ipam.change_fhrpgroupassignment %}
<a href="{% url 'ipam:fhrpgroupassignment_edit' pk=assignment.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-warning lh-1" title="{% trans "Edit" %}">
<a href="{% url 'ipam:fhrpgroupassignment_edit' pk=assignment.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-warning" title="{% trans "Edit" %}">
<i class="mdi mdi-pencil" aria-hidden="true"></i>
</a>
{% endif %}
{% if perms.ipam.delete_fhrpgroupassignment %}
<a href="{% url 'ipam:fhrpgroupassignment_delete' pk=assignment.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-danger lh-1" title="{% trans "Delete" %}">
<a href="{% url 'ipam:fhrpgroupassignment_delete' pk=assignment.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-danger" title="{% trans "Delete" %}">
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i>
</a>
{% endif %}

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

@@ -160,9 +160,17 @@ def string_to_ranges(value):
return None
value.replace(' ', '') # Remove whitespace
values = []
for dash_range in value.split(','):
if '-' not in dash_range:
for data in value.split(','):
dash_range = data.strip().split('-')
if len(dash_range) == 1 and str(dash_range[0]).isdigit():
# Single integer value; expand to a range
lower = dash_range[0]
upper = dash_range[0]
elif len(dash_range) == 2 and str(dash_range[0]).isdigit() and str(dash_range[1]).isdigit():
# The range has two values and both are valid integers
lower = dash_range[0]
upper = dash_range[1]
else:
return None
lower, upper = dash_range.split('-')
values.append(NumericRange(int(lower), int(upper), bounds='[]'))
return values

View File

@@ -32,12 +32,14 @@ class NumericArrayField(SimpleArrayField):
class NumericRangeArrayField(forms.CharField):
"""
A field which allows for array of numeric ranges:
Example: 1-5,7-20,30-50
Example: 1-5,10,20-30
"""
def __init__(self, *args, help_text='', **kwargs):
if not help_text:
help_text = mark_safe(
_("Specify one or more numeric ranges separated by commas. Example: " + "<code>1-5,20-30</code>")
_(
"Specify one or more individual numbers or numeric ranges separated by commas. Example: {example}"
).format(example="<code>1-5,10,20-30</code>")
)
super().__init__(*args, help_text=help_text, **kwargs)

View File

@@ -66,3 +66,17 @@ class RangeFunctionsTestCase(TestCase):
NumericRange(100, 199, bounds='[]'), # 100-199
]
)
self.assertEqual(
string_to_ranges('1-2, 5, 10-12'),
[
NumericRange(1, 2, bounds='[]'), # 1-2
NumericRange(5, 5, bounds='[]'), # 5-5
NumericRange(10, 12, bounds='[]'), # 10-12
]
)
self.assertEqual(
string_to_ranges('2-10, a-b'),
None # Fails to convert
)

View File

@@ -3,7 +3,7 @@
[project]
name = "netbox"
version = "4.3.5"
version = "4.3.6"
requires-python = ">=3.10"
authors = [
{ name = "NetBox Community" }

View File

@@ -1,4 +1,4 @@
Django==5.2.4
Django==5.2.5
django-cors-headers==4.7.0
django-debug-toolbar==5.2.0
django-filter==25.1
@@ -9,14 +9,14 @@ django-pglocks==1.0.4
django-prometheus==2.4.1
django-redis==6.0.0
django-rich==2.0.0
django-rq==3.0.1
django-rq==3.1
django-storages==1.14.6
django-tables2==2.7.5
django-taggit==6.1.0
django-timezone-field==7.1
djangorestframework==3.16.0
djangorestframework==3.16.1
drf-spectacular==0.28.0
drf-spectacular-sidecar==2025.7.1
drf-spectacular-sidecar==2025.8.1
feedparser==6.0.11
gunicorn==23.0.0
Jinja2==3.1.6
@@ -33,7 +33,7 @@ requests==2.32.4
rq==2.4.1
social-auth-app-django==5.5.1
social-auth-core==4.7.0
strawberry-graphql==0.278.0
strawberry-graphql==0.278.1
strawberry-graphql-django==0.65.1
svgwrite==1.4.3
tablib==3.8.0