Compare commits

...

43 Commits

Author SHA1 Message Date
Jeremy Stretch
e68b83907b Merge pull request #16432 from netbox-community/develop
Release v4.0.5
2024-06-06 11:59:00 -04:00
Jeremy Stretch
2682f03a6b Re-bundle static assets 2024-06-06 11:42:47 -04:00
Jeremy Stretch
2304df84d5 Merge branch 'master' into develop 2024-06-06 11:36:08 -04:00
Jeremy Stretch
5530556626 Merge pull request #16429 from netbox-community/develop
Release v4.0.5
2024-06-06 11:31:54 -04:00
Jeremy Stretch
e4d240ace2 Release v4.0.5 2024-06-06 10:55:30 -04:00
transifex-integration[bot]
58f22eec37 Updates for project NetBox (#16346)
* Translate django.po in de [Manual Sync]

74% of minimum 1% reviewed source file: 'django.po'
on 'de'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

* Translate django.po in de [Manual Sync]

74% of minimum 1% reviewed source file: 'django.po'
on 'de'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

* Translate django.po in ru [Manual Sync]

30% of minimum 1% reviewed source file: 'django.po'
on 'ru'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

* Translate django.po in pt [Manual Sync]

2% of minimum 1% reviewed source file: 'django.po'
on 'pt'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

* Translate django.po in fr [Manual Sync]

12% of minimum 1% reviewed source file: 'django.po'
on 'fr'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

* Translate django.po in tr [Manual Sync]

7% of minimum 1% reviewed source file: 'django.po'
on 'tr'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-06-06 10:27:06 -04:00
Julio Oliveira at Encora
7e1b3d0b54 15873 - Make Cluster resource counters more readable (#15900)
* Created "convert_byte_size" method to convert the memory and disk size according to unit informed.
Changed "get_extra_context" method from "ClusterView" to use the method above and convert all the disks and memories from VMs to normalize the units.

* Changed decimal size for memory_sum and disk_sum

* Added test for convert_byte_size.

* Fixed

* Addressed PR comments.
Changed humanize_megabytes in helpers.py

* Addressed PR comments.
Changed humanize_megabytes in helpers.py

* Linter issues for helpers.py

* Changed humanize_megabytes

* Changed humanize_megabytes

* Changed humanize_megabytes

* Added the title to display the value in MB when mouseover.

* Addressed PR comment.

* Addressed PR comment.

* Rewrite sizing logic

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-06-06 09:37:29 -04:00
Julio Oliveira at Encora
3acf3b51ee Fixes: #14567 - Export current view of IP Addresses (#15659)
* Added javascript and htmx to change the url.

* Added javascript and htmx to change the url

* Addressed PR comments

* Added Netbox.js and netbox.js.map

* Addressed PR comments

* Addressed PR comments

* Addressed PR comments

* Addressed PR comments

* Addressed PR comments

* Addressed PR comments

* Addressed PR comments

* Addressed PR comments

* Linter Issues

* Fix assets issue

* Fix assets issue

* Addressed PR comment.
It was added clearLinkParams to clear button.

* Added passive:true to search.ts

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-06-06 09:35:27 -04:00
Arthur Hanson
8f87c72eaa 16050 Show script python_class name and description (#16185)
* 16050 Show script python_class name and description

* 16050 change to use Meta.description

* 16050 change to use Meta.description

* 16050 remove module name customization from docs
2024-06-06 09:05:59 -04:00
Louis Jarasius
18b43408ec Fixes #16274: Dark mode highlight color (#16355)
* Increase ::selection background-color aplha

* Improve comment for override

* Add compiled CSS

* Only override on dark theme
2024-06-06 08:44:32 -04:00
Julio Oliveira at Encora
b10fb67ce9 Fixed error when the active Config is deleted and rest only one to restore. (#16408) 2024-06-05 12:23:36 -07:00
Jeremy Stretch
c27cb6f153 Fix styling of object jobs table 2024-06-05 09:02:05 -04:00
github-actions
81f0a40505 Update source translation strings 2024-06-05 05:02:18 +00:00
Jeremy Stretch
4242546270 Fixes #16376: Log changes on terminating objects when attaching a cable 2024-06-04 14:37:33 -04:00
Julio Oliveira at Encora
87109f5539 16315 - Cant filter changelog by object type (no results found) (#16324)
* Replaced "api=/api/extras/content-types/" with "/api/extras/object-types/" for JournalEntryFilterForm and ObjectChangeFilterForm.

* Addressed PR comment.

* Correct feature classifications

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-06-04 09:37:08 -04:00
Daniel Sheppard
8ab9afb8db Fixes: #16083 - Add font-variant-ligatures setting to disable ligatur… (#16383)
* Fixes: #16083 - Add font-variant-ligatures setting to disable ligatures on chromium

* Fix comment

* Disable ligatures on input fields

* Condense rules & apply to all elements

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-06-04 09:02:38 -04:00
Jamie (Bear) Murphy
7be003f5a0 Allow plugins to extend objectchange view (#16371)
* allow plugins to extend objectchangeview with panels

* replace tabs with spaces

* Update netbox/templates/extras/objectchange.html

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>

* Eliminate excessive vertical margin

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-06-04 08:49:08 -04:00
github-actions
291e0665d0 Update source translation strings 2024-06-04 05:02:13 +00:00
Arthur Hanson
8e48e939aa 16261 fix graphql lookup for MultiValueCharFilter fields (#16354)
* 16261 fix graphql lookup for MultiValueCharFilter fields

* 16261 fix graphql lookup for MultiValueCharFilter fields

* 16261 fixup test

* 16261 fixup test

* Omit redundant assignment

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-06-03 10:24:01 -04:00
Daniel Sheppard
fdad59c8cc Fixes: #16039 - Fix row highlighting on device components and VM interfaces (#16044)
* Fix row highlighting

* Minor fix for VMInterfaces

* Move duplicated dicts into inheritable meta class

* Add CableTerminationTable.Meta class for inheritance of the row_attrs to each descendant Meta class.
2024-06-03 08:47:53 -04:00
Jeremy Stretch
24d02cb381 Fixes #15194: Prevent enqueuing duplicate events for an object 2024-06-03 08:34:26 -04:00
Jeremy Stretch
602754439a Update workflows to use most recent release of each action 2024-06-03 08:01:50 -04:00
github-actions
e18e6cf756 Update source translation strings 2024-06-01 05:02:24 +00:00
Jeremy Stretch
0dde0b506e Fixes #16312: Fix object list navigation for dashboard widgets 2024-05-31 13:16:41 -04:00
Jeremy Stretch
26a856f57c Changelog for #13422, #14810, #15489, #16202, #16286, #16290 2024-05-31 10:29:53 -04:00
Jeremy Stretch
e095ec6860 Fixes #13422: Rebuild MPTT trees for applicable models when merging staged changes 2024-05-31 10:07:07 -04:00
Jeremy Stretch
05c69f84e6 Enable scheduled runs 2024-05-30 10:43:54 -04:00
github-actions
05d3224c33 Update source translation strings 2024-05-30 14:23:18 +00:00
Jeremy Stretch
4ad74587e5 Fix action permissions 2024-05-30 10:21:02 -04:00
Jeremy Stretch
153341c1b7 Install gettext 2024-05-30 10:14:43 -04:00
Jeremy Stretch
f5aa34bb37 Add GitHub action to run makemessages 2024-05-30 09:56:56 -04:00
Jeremy Stretch
a3c4984623 Skip CI if changes are limited to non-code paths 2024-05-30 08:37:24 -04:00
Jeremy Stretch
67165a9f91 Remove abhi1693 from issue triage rotation 2024-05-29 11:37:25 -04:00
Arthur Hanson
4d924a9041 16202 fix mapit button for internationalized decimal seperator (#16270)
* 16202 fix mapit button for internationalized decimal seperator

* 16202 revert untranslate

* 16202 revert untranslate
2024-05-29 10:22:59 -04:00
Jeremy Stretch
a094719d23 Closes #16290: Capture entire object in changelog data 2024-05-29 09:34:22 -04:00
Jeremy Stretch
418389c577 Update translations workflow documentation 2024-05-29 09:14:02 -04:00
Markku Leiniö
f1bf4c8758 Closes #16297: Add uwsgi.ini in .gitignore 2024-05-28 12:12:33 -04:00
Arthur
0bfb9777be 14810 add contacts to service 2024-05-28 09:44:41 -04:00
Arthur
360f3bc01b 16284 fix plugin forms doc 2024-05-28 09:07:32 -04:00
Arthur
8a91252d51 16286 fix provider account search 2024-05-28 09:06:34 -04:00
Julio-Oliveira-Encora
eb3adc050d Added 1000-Base-TX to the choices.py 2024-05-28 09:01:15 -04:00
Jeremy Stretch
103c08c2d2 Update exempt issue labels for stale action 2024-05-22 15:39:24 -04:00
Jeremy Stretch
806ff646e2 PRVB 2024-05-22 14:57:39 -04:00
67 changed files with 33879 additions and 29460 deletions

View File

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

View File

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

View File

@@ -12,10 +12,10 @@ jobs:
auto-assign:
runs-on: ubuntu-latest
steps:
- uses: pozil/auto-assign-issue@v1
- uses: pozil/auto-assign-issue@v2
if: "contains(github.event.issue.labels.*.name, 'status: needs triage')"
with:
# Weighted assignments
assignees: arthanson:3, jeffgdotorg:3, jeremystretch:3, abhi1693, DanSheps
assignees: arthanson:3, jeffgdotorg:3, jeremystretch:3, DanSheps
numOfAssignee: 1
abortIfPreviousAssignees: true

View File

@@ -1,7 +1,18 @@
name: CI
on: [push, pull_request]
on:
push:
paths-ignore:
- 'contrib/**'
- 'docs/**'
pull_request:
paths-ignore:
- 'contrib/**'
- 'docs/**'
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
@@ -34,12 +45,12 @@ jobs:
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
@@ -47,7 +58,7 @@ jobs:
run: npm install -g yarn
- name: Setup Node.js with Yarn Caching
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: yarn

View File

@@ -29,7 +29,7 @@ jobs:
necessary.
days-before-issue-stale: 90
days-before-issue-close: 30
exempt-issue-labels: 'status: accepted,status: blocked,status: needs milestone'
exempt-issue-labels: 'status: accepted,status: backlog,status: blocked'
stale-issue-label: 'pending closure'
stale-issue-message: >
This issue has been automatically marked as stale because it has not had

View File

@@ -0,0 +1,45 @@
name: Update translation strings
on:
schedule:
- cron: '0 5 * * *'
workflow_dispatch:
permissions:
contents: write
env:
LOCALE: "en"
jobs:
makemessages:
runs-on: ubuntu-latest
env:
NETBOX_CONFIGURATION: netbox.configuration_testing
steps:
- name: Check out repo
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11
- name: Install system dependencies
run: sudo apt install -y gettext
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run makemessages
run: python netbox/manage.py makemessages -l ${{ env.LOCALE }}
- name: Commit changes
uses: EndBug/add-and-commit@v9
with:
add: 'netbox/translations/'
default_author: github_actions
message: 'Update source translation strings'

1
.gitignore vendored
View File

@@ -21,6 +21,7 @@ local_settings.py
!upgrade.sh
fabfile.py
gunicorn.py
uwsgi.ini
netbox.log
netbox.pid
.DS_Store

View File

@@ -323,6 +323,7 @@
"100base-tx",
"100base-t1",
"1000base-t",
"1000base-tx",
"2.5gbase-t",
"5gbase-t",
"10gbase-t",

View File

@@ -65,12 +65,6 @@ class AnotherCustomScript(Script):
script_order = (MyCustomScript, AnotherCustomScript)
```
## Module Attributes
### `name`
You can define `name` within a script module (the Python file which contains one or more scripts) to set the module name. If `name` is not defined, the module's file name will be used.
## Script Attributes
Script attributes are defined under a class named `Meta` within the script. These are optional, but encouraged.

View File

@@ -86,15 +86,7 @@ This will automatically update the schema file at `contrib/generated_schema.json
### Update & Compile Translations
Log into [Transifex](https://app.transifex.com/netbox-community/netbox/dashboard/) to download the updated string maps. Download the resource (portable object, or `.po`) file for each language and save them to `netbox/translations/$lang/LC_MESSAGES/django.po`, overwriting the current files. (Be sure to click the **Download for use** link.)
![Transifex download](../media/development/transifex_download.png)
Once the resource files for all languages have been updated, compile the machine object (`.mo`) files using the `compilemessages` management command:
```nohighlight
./manage.py compilemessages
```
Updated language translations should be pulled from [Transifex](https://app.transifex.com/netbox-community/netbox/dashboard/) and re-compiled for each new release. Follow the documented process for [updating translated strings](./translations.md#updating-translated-strings) to do this.
### Update Version and Changelog

View File

@@ -6,17 +6,38 @@ All language translations in NetBox are generated from the source file found at
Reviewers log into Transifex and navigate to their designated language(s) to translate strings. The initial translation for most strings will be machine-generated via the AWS Translate service. Human reviewers are responsible for reviewing these translations and making corrections where necessary.
Immediately prior to each NetBox release, the translation maps for all completed languages will be downloaded from Transifex, compiled, and checked into the NetBox code base by a maintainer.
## Updating Translation Sources
To update the English `.po` file from which all translations are derived, use the `makemessages` management command:
To update the English `.po` file from which all translations are derived, use the `makemessages` management command (ignoring the `project-static/` directory):
```nohighlight
./manage.py makemessages -l en
./manage.py makemessages -l en -i "project-static/*"
```
Then, commit the change and push to the `develop` branch on GitHub. After some time, any new strings will appear for translation on Transifex automatically.
Then, commit the change and push to the `develop` branch on GitHub. Any new strings will appear for translation on Transifex automatically.
## Updating Translated Strings
Typically, translated strings need to be updated only as part of the NetBox [release process](./release-checklist.md).
To update translated strings, start by initiating a sync from Transifex. From the Transifex dashboard, navigate to Settings > Integrations > GitHub > Manage, and click the **Manual Sync** button at top right.
![Transifex manual sync](../media/development/transifex_sync.png)
Enter a threshold percentage of 1 (to ensure all translations are captured) and select the `develop` branch, then click **Sync**. This will initiate a pull request to GitHub to update any newly modified translation (`.po`) files.
!!! tip
The new PR should appear within a few minutes. If it does not, check that there are in fact new translations to be added.
![Transifex pull request](../media/development/transifex_pull_request.png)
Once the PR has been merged, the updated strings need to be compiled into new `.mo` files so they can be used by the application. Update the `develop` branch locally to pull in the changes from the Transifex PR, then run Django's [`compilemessages`](https://docs.djangoproject.com/en/stable/ref/django-admin/#django-admin-compilemessages) management command:
```nohighlight
./manage.py compilemessages
```
Once any new `.mo` files have been generated, they need to be committed and pushed back up to GitHub. (Again, this is typically done as part of publishing a new NetBox release.)
## Proposing New Languages

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -89,13 +89,13 @@ This form facilitates editing multiple objects in bulk. Unlike a model form, thi
from django import forms
from django.utils.translation import gettext_lazy as _
from dcim.models import Site
from netbox.forms import NetBoxModelImportForm
from netbox.forms import NetBoxModelBulkEditForm
from utilities.forms import CommentField, DynamicModelChoiceField
from utilities.forms.rendering import FieldSet
from .models import MyModel, MyModelStatusChoices
class MyModelEditForm(NetBoxModelImportForm):
class MyModelBulkEditForm(NetBoxModelBulkEditForm):
name = forms.CharField(
required=False
)

View File

@@ -1,5 +1,33 @@
# NetBox v4.0
## v4.0.5 (2024-06-06)
### Enhancements
* [#14810](https://github.com/netbox-community/netbox/issues/14810) - Enable contact assignment for services
* [#15489](https://github.com/netbox-community/netbox/issues/15489) - Add 1000Base-TX interface type
* [#15873](https://github.com/netbox-community/netbox/issues/15873) - Improve readability of allocates resource numbers for clusters
* [#16290](https://github.com/netbox-community/netbox/issues/16290) - Capture entire object in changelog data (but continue to display only non-internal attributes)
* [#16353](https://github.com/netbox-community/netbox/issues/16353) - Enable plugins to extend object change view with custom content
### Bug Fixes
* [#13422](https://github.com/netbox-community/netbox/issues/13422) - Rebuild MPTT trees for applicable models after merging staged changes
* [#14567](https://github.com/netbox-community/netbox/issues/14567) - Apply active quicksearch value when exporting "current view" from object list
* [#15194](https://github.com/netbox-community/netbox/issues/15194) - Avoid enqueuing duplicate event triggers for a modified object
* [#16039](https://github.com/netbox-community/netbox/issues/16039) - Fix row highlighting for front & rear port connections under device view
* [#16050](https://github.com/netbox-community/netbox/issues/16050) - Fix display of names & descriptions defined for custom scripts
* [#16083](https://github.com/netbox-community/netbox/issues/16083) - Disable font ligatures to avoid peculiarities in rendered text
* [#16202](https://github.com/netbox-community/netbox/issues/16202) - Fix site map button URL for certain localizations
* [#16261](https://github.com/netbox-community/netbox/issues/16261) - Fix GraphQL filtering for certain multi-value filters
* [#16286](https://github.com/netbox-community/netbox/issues/16286) - Fix global search support for provider accounts
* [#16312](https://github.com/netbox-community/netbox/issues/16312) - Fix object list navigation for dashboard widgets
* [#16315](https://github.com/netbox-community/netbox/issues/16315) - Fix filtering change log & journal entries by object type in UI
* [#16376](https://github.com/netbox-community/netbox/issues/16376) - Update change log for the terminating object (e.g. interface) when attaching a cable
* [#16400](https://github.com/netbox-community/netbox/issues/16400) - Fix AttributeError when attempting to restore a previous configuration revision after deleting the current one
---
## v4.0.3 (2024-05-22)
### Enhancements

View File

@@ -48,6 +48,7 @@ class ProviderIndex(SearchIndex):
display_attrs = ('description',)
@register_search
class ProviderAccountIndex(SearchIndex):
model = models.ProviderAccount
fields = (

View File

@@ -224,7 +224,7 @@ class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View):
for param in PARAMS:
params.append((
param.name,
current_config.data.get(param.name, None),
current_config.data.get(param.name, None) if current_config else None,
candidate_config.data.get(param.name, None)
))

View File

@@ -828,6 +828,7 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_100ME_FIXED = '100base-tx'
TYPE_100ME_T1 = '100base-t1'
TYPE_1GE_FIXED = '1000base-t'
TYPE_1GE_TX_FIXED = '1000base-tx'
TYPE_1GE_GBIC = '1000base-x-gbic'
TYPE_1GE_SFP = '1000base-x-sfp'
TYPE_2GE_FIXED = '2.5gbase-t'
@@ -987,6 +988,7 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'),
(TYPE_100ME_T1, '100BASE-T1 (10/100ME Single Pair)'),
(TYPE_1GE_FIXED, '1000BASE-T (1GE)'),
(TYPE_1GE_TX_FIXED, '1000BASE-TX (1GE)'),
(TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'),
(TYPE_5GE_FIXED, '5GBASE-T (5GE)'),
(TYPE_10GE_FIXED, '10GBASE-T (10GE)'),

View File

@@ -355,11 +355,11 @@ class CableTermination(ChangeLoggedModel):
super().save(*args, **kwargs)
# Set the cable on the terminating object
termination_model = self.termination._meta.model
termination_model.objects.filter(pk=self.termination_id).update(
cable=self.cable,
cable_end=self.cable_end
)
termination = self.termination._meta.model.objects.get(pk=self.termination_id)
termination.snapshot()
termination.cable = self.cable
termination.cable_end = self.cable_end
termination.save()
def delete(self, *args, **kwargs):

View File

@@ -43,14 +43,6 @@ MODULEBAY_STATUS = """
"""
def get_cabletermination_row_class(record):
if record.mark_connected:
return 'success'
elif record.cable:
return record.cable.get_status_color()
return ''
#
# Device roles
#
@@ -339,6 +331,14 @@ class CableTerminationTable(NetBoxTable):
verbose_name=_('Mark Connected'),
)
class Meta:
row_attrs = {
'data-name': lambda record: record.name,
'data-mark-connected': lambda record: "true" if record.mark_connected else "false",
'data-cable-status': lambda record: record.cable.status if record.cable else "",
'data-type': lambda record: record.type
}
def value_link_peer(self, value):
return ', '.join([
f"{termination.parent_object} > {termination}" for termination in value
@@ -386,16 +386,13 @@ class DeviceConsolePortTable(ConsolePortTable):
extra_buttons=CONSOLEPORT_BUTTONS
)
class Meta(DeviceComponentTable.Meta):
class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta):
model = models.ConsolePort
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected',
'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions'
)
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection')
row_attrs = {
'class': get_cabletermination_row_class
}
class ConsoleServerPortTable(ModularDeviceComponentTable, PathEndpointTable):
@@ -431,16 +428,13 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
extra_buttons=CONSOLESERVERPORT_BUTTONS
)
class Meta(DeviceComponentTable.Meta):
class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta):
model = models.ConsoleServerPort
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'speed', 'description', 'mark_connected',
'cable', 'cable_color', 'link_peer', 'connection', 'tags', 'actions',
)
default_columns = ('pk', 'name', 'label', 'type', 'speed', 'description', 'cable', 'connection')
row_attrs = {
'class': get_cabletermination_row_class
}
class PowerPortTable(ModularDeviceComponentTable, PathEndpointTable):
@@ -483,7 +477,7 @@ class DevicePowerPortTable(PowerPortTable):
extra_buttons=POWERPORT_BUTTONS
)
class Meta(DeviceComponentTable.Meta):
class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta):
model = models.PowerPort
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'maximum_draw', 'allocated_draw',
@@ -492,9 +486,6 @@ class DevicePowerPortTable(PowerPortTable):
default_columns = (
'pk', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'connection',
)
row_attrs = {
'class': get_cabletermination_row_class
}
class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable):
@@ -534,7 +525,7 @@ class DevicePowerOutletTable(PowerOutletTable):
extra_buttons=POWEROUTLET_BUTTONS
)
class Meta(DeviceComponentTable.Meta):
class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta):
model = models.PowerOutlet
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'power_port', 'feed_leg', 'description',
@@ -543,9 +534,6 @@ class DevicePowerOutletTable(PowerOutletTable):
default_columns = (
'pk', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'connection',
)
row_attrs = {
'class': get_cabletermination_row_class
}
class BaseInterfaceTable(NetBoxTable):
@@ -733,7 +721,7 @@ class DeviceFrontPortTable(FrontPortTable):
extra_buttons=FRONTPORT_BUTTONS
)
class Meta(DeviceComponentTable.Meta):
class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta):
model = models.FrontPort
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'rear_port', 'rear_port_position',
@@ -742,9 +730,6 @@ class DeviceFrontPortTable(FrontPortTable):
default_columns = (
'pk', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'link_peer',
)
row_attrs = {
'class': get_cabletermination_row_class
}
class RearPortTable(ModularDeviceComponentTable, CableTerminationTable):
@@ -783,7 +768,7 @@ class DeviceRearPortTable(RearPortTable):
extra_buttons=REARPORT_BUTTONS
)
class Meta(DeviceComponentTable.Meta):
class Meta(CableTerminationTable.Meta, DeviceComponentTable.Meta):
model = models.RearPort
fields = (
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'type', 'positions', 'description', 'mark_connected',
@@ -792,9 +777,6 @@ class DeviceRearPortTable(RearPortTable):
default_columns = (
'pk', 'name', 'label', 'type', 'positions', 'description', 'cable', 'link_peer',
)
row_attrs = {
'class': get_cabletermination_row_class
}
class DeviceBayTable(DeviceComponentTable):

View File

@@ -30,6 +30,16 @@ class ObjectChangeSerializer(BaseModelSerializer):
changed_object = serializers.SerializerMethodField(
read_only=True
)
prechange_data = serializers.JSONField(
source='prechange_data_clean',
read_only=True,
allow_null=True
)
postchange_data = serializers.JSONField(
source='postchange_data_clean',
read_only=True,
allow_null=True
)
class Meta:
model = ObjectChange

View File

@@ -13,13 +13,14 @@ def event_tracking(request):
:param request: WSGIRequest object with a unique `id` set
"""
current_request.set(request)
events_queue.set([])
events_queue.set({})
yield
# Flush queued webhooks to RQ
flush_events(events_queue.get())
if events := list(events_queue.get().values()):
flush_events(events)
# Clear context vars
current_request.set(None)
events_queue.set([])
events_queue.set({})

View File

@@ -265,6 +265,7 @@ class ObjectListWidget(DashboardWidget):
parameters = self.config.get('url_params') or {}
if page_size := self.config.get('page_size'):
parameters['per_page'] = page_size
parameters['embedded'] = True
if parameters:
try:

View File

@@ -58,15 +58,21 @@ def enqueue_object(queue, instance, user, request_id, action):
if model_name not in registry['model_features']['event_rules'].get(app_label, []):
return
queue.append({
'content_type': ContentType.objects.get_for_model(instance),
'object_id': instance.pk,
'event': action,
'data': serialize_for_event(instance),
'snapshots': get_snapshots(instance, action),
'username': user.username,
'request_id': request_id
})
assert instance.pk is not None
key = f'{app_label}.{model_name}:{instance.pk}'
if key in queue:
queue[key]['data'] = serialize_for_event(instance)
queue[key]['snapshots']['postchange'] = get_snapshots(instance, action)['postchange']
else:
queue[key] = {
'content_type': ContentType.objects.get_for_model(instance),
'object_id': instance.pk,
'event': action,
'data': serialize_for_event(instance),
'snapshots': get_snapshots(instance, action),
'username': user.username,
'request_id': request_id
}
def process_event_rules(event_rules, model_name, event, data, username=None, snapshots=None, request_id=None):
@@ -163,14 +169,14 @@ def process_event_queue(events):
)
def flush_events(queue):
def flush_events(events):
"""
Flush a list of object representation to RQ for webhook processing.
Flush a list of object representations to RQ for event processing.
"""
if queue:
if events:
for name in settings.EVENTS_PIPELINE:
try:
func = import_string(name)
func(queue)
func(events)
except Exception as e:
logger.error(_("Cannot import events pipeline {name} error: {error}").format(name=name, error=e))

View File

@@ -464,13 +464,10 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
required=False,
label=_('User')
)
assigned_object_type_id = DynamicModelMultipleChoiceField(
queryset=ObjectType.objects.all(),
assigned_object_type_id = ContentTypeMultipleChoiceField(
queryset=ObjectType.objects.with_feature('journaling'),
required=False,
label=_('Object Type'),
widget=APISelectMultiple(
api_url='/api/extras/content-types/',
)
)
kind = forms.ChoiceField(
label=_('Kind'),
@@ -507,11 +504,8 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
required=False,
label=_('User')
)
changed_object_type_id = DynamicModelMultipleChoiceField(
queryset=ObjectType.objects.all(),
changed_object_type_id = ContentTypeMultipleChoiceField(
queryset=ObjectType.objects.with_feature('change_logging'),
required=False,
label=_('Object Type'),
widget=APISelectMultiple(
api_url='/api/extras/content-types/',
)
)

View File

@@ -1,12 +1,17 @@
from functools import cached_property
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from mptt.models import MPTTModel
from core.models import ObjectType
from extras.choices import *
from netbox.models.features import ChangeLoggingMixin
from utilities.data import shallow_compare_dict
from ..querysets import ObjectChangeQuerySet
__all__ = (
@@ -136,6 +141,71 @@ class ObjectChange(models.Model):
def get_action_color(self):
return ObjectChangeActionChoices.colors.get(self.action)
@property
@cached_property
def has_changes(self):
return self.prechange_data != self.postchange_data
@cached_property
def diff_exclude_fields(self):
"""
Return a set of attributes which should be ignored when calculating a diff
between the pre- and post-change data. (For instance, it would not make
sense to compare the "last updated" times as these are expected to differ.)
"""
model = self.changed_object_type.model_class()
attrs = set()
# Exclude auto-populated change tracking fields
if issubclass(model, ChangeLoggingMixin):
attrs.update({'created', 'last_updated'})
# Exclude MPTT-internal fields
if issubclass(model, MPTTModel):
attrs.update({'level', 'lft', 'rght', 'tree_id'})
return attrs
def get_clean_data(self, prefix):
"""
Return only the pre-/post-change attributes which are relevant for calculating a diff.
"""
ret = {}
change_data = getattr(self, f'{prefix}_data') or {}
for k, v in change_data.items():
if k not in self.diff_exclude_fields and not k.startswith('_'):
ret[k] = v
return ret
@cached_property
def prechange_data_clean(self):
return self.get_clean_data('prechange')
@cached_property
def postchange_data_clean(self):
return self.get_clean_data('postchange')
def diff(self):
"""
Return a dictionary of pre- and post-change values for attributes which have changed.
"""
prechange_data = self.prechange_data_clean
postchange_data = self.postchange_data_clean
# Determine which attributes have changed
if self.action == ObjectChangeActionChoices.ACTION_CREATE:
changed_attrs = sorted(postchange_data.keys())
elif self.action == ObjectChangeActionChoices.ACTION_DELETE:
changed_attrs = sorted(prechange_data.keys())
else:
# TODO: Support deep (recursive) comparison
changed_data = shallow_compare_dict(prechange_data, postchange_data)
changed_attrs = sorted(changed_data.keys())
return {
'pre': {
k: prechange_data.get(k) for k in changed_attrs
},
'post': {
k: postchange_data.get(k) for k in changed_attrs
},
}

View File

@@ -4,6 +4,7 @@ from django.contrib.auth import get_user_model
from django.contrib.contenttypes.fields import GenericForeignKey
from django.db import models, transaction
from django.utils.translation import gettext_lazy as _
from mptt.models import MPTTModel
from extras.choices import ChangeActionChoices
from netbox.models import ChangeLoggedModel
@@ -124,6 +125,11 @@ class StagedChange(CustomValidationMixin, EventRulesMixin, models.Model):
instance = self.model.objects.get(pk=self.object_id)
logger.info(f'Deleting {self.model._meta.verbose_name} {instance}')
instance.delete()
# Rebuild the MPTT tree where applicable
if issubclass(self.model, MPTTModel):
self.model.objects.rebuild()
apply.alters_data = True
def get_action_color(self):

View File

@@ -55,18 +55,6 @@ def run_validators(instance, validators):
clear_events = Signal()
def is_same_object(instance, webhook_data, request_id):
"""
Compare the given instance to the most recent queued webhook object, returning True
if they match. This check is used to avoid creating duplicate webhook entries.
"""
return (
ContentType.objects.get_for_model(instance) == webhook_data['content_type'] and
instance.pk == webhook_data['object_id'] and
request_id == webhook_data['request_id']
)
@receiver((post_save, m2m_changed))
def handle_changed_object(sender, instance, **kwargs):
"""
@@ -112,14 +100,13 @@ def handle_changed_object(sender, instance, **kwargs):
objectchange.request_id = request.id
objectchange.save()
# If this is an M2M change, update the previously queued webhook (from post_save)
# Ensure that we're working with fresh M2M assignments
if m2m_changed:
instance.refresh_from_db()
# Enqueue the object for event processing
queue = events_queue.get()
if m2m_changed and queue and is_same_object(instance, queue[-1], request.id):
instance.refresh_from_db() # Ensure that we're working with fresh M2M assignments
queue[-1]['data'] = serialize_for_event(instance)
queue[-1]['snapshots']['postchange'] = get_snapshots(instance, action)['postchange']
else:
enqueue_object(queue, instance, request.user, request.id, action)
enqueue_object(queue, instance, request.user, request.id, action)
events_queue.set(queue)
# Increment metric counters
@@ -179,7 +166,7 @@ def handle_deleted_object(sender, instance, **kwargs):
obj.snapshot() # Ensure the change record includes the "before" state
getattr(obj, related_field_name).remove(instance)
# Enqueue webhooks
# Enqueue the object for event processing
queue = events_queue.get()
enqueue_object(queue, instance, request.user, request.id, ObjectChangeActionChoices.ACTION_DELETE)
events_queue.set(queue)
@@ -195,7 +182,7 @@ def clear_events_queue(sender, **kwargs):
"""
logger = logging.getLogger('events')
logger.info(f"Clearing {len(events_queue.get())} queued events ({sender})")
events_queue.set([])
events_queue.set({})
#

View File

@@ -75,6 +75,10 @@ class ChangeLogViewTest(ModelViewTestCase):
self.assertEqual(oc.postchange_data['custom_fields']['cf2'], form_data['cf_cf2'])
self.assertEqual(oc.postchange_data['tags'], ['Tag 1', 'Tag 2'])
# Check that private attributes were included in raw data but not display data
self.assertIn('_name', oc.postchange_data)
self.assertNotIn('_name', oc.postchange_data_clean)
def test_update_object(self):
site = Site(name='Site 1', slug='site-1')
site.save()
@@ -112,6 +116,12 @@ class ChangeLogViewTest(ModelViewTestCase):
self.assertEqual(oc.postchange_data['custom_fields']['cf2'], form_data['cf_cf2'])
self.assertEqual(oc.postchange_data['tags'], ['Tag 3'])
# Check that private attributes were included in raw data but not display data
self.assertIn('_name', oc.prechange_data)
self.assertNotIn('_name', oc.prechange_data_clean)
self.assertIn('_name', oc.postchange_data)
self.assertNotIn('_name', oc.postchange_data_clean)
def test_delete_object(self):
site = Site(
name='Site 1',
@@ -142,6 +152,10 @@ class ChangeLogViewTest(ModelViewTestCase):
self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2'])
self.assertEqual(oc.postchange_data, None)
# Check that private attributes were included in raw data but not display data
self.assertIn('_name', oc.prechange_data)
self.assertNotIn('_name', oc.prechange_data_clean)
def test_bulk_update_objects(self):
sites = (
Site(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_ACTIVE),
@@ -338,6 +352,10 @@ class ChangeLogAPITest(APITestCase):
self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields'])
self.assertEqual(oc.postchange_data['tags'], ['Tag 1', 'Tag 2'])
# Check that private attributes were included in raw data but not display data
self.assertIn('_name', oc.postchange_data)
self.assertNotIn('_name', oc.postchange_data_clean)
def test_update_object(self):
site = Site(name='Site 1', slug='site-1')
site.save()
@@ -370,6 +388,12 @@ class ChangeLogAPITest(APITestCase):
self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields'])
self.assertEqual(oc.postchange_data['tags'], ['Tag 3'])
# Check that private attributes were included in raw data but not display data
self.assertIn('_name', oc.prechange_data)
self.assertNotIn('_name', oc.prechange_data_clean)
self.assertIn('_name', oc.postchange_data)
self.assertNotIn('_name', oc.postchange_data_clean)
def test_delete_object(self):
site = Site(
name='Site 1',
@@ -398,6 +422,10 @@ class ChangeLogAPITest(APITestCase):
self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2'])
self.assertEqual(oc.postchange_data, None)
# Check that private attributes were included in raw data but not display data
self.assertIn('_name', oc.prechange_data)
self.assertNotIn('_name', oc.prechange_data_clean)
def test_bulk_create_objects(self):
data = (
{

View File

@@ -4,6 +4,7 @@ from unittest.mock import patch
import django_rq
from django.http import HttpResponse
from django.test import RequestFactory
from django.urls import reverse
from requests import Session
from rest_framework import status
@@ -12,6 +13,7 @@ from core.models import ObjectType
from dcim.choices import SiteStatusChoices
from dcim.models import Site
from extras.choices import EventRuleActionChoices, ObjectChangeActionChoices
from extras.context_managers import event_tracking
from extras.events import enqueue_object, flush_events, serialize_for_event
from extras.models import EventRule, Tag, Webhook
from extras.webhooks import generate_signature, send_webhook
@@ -360,7 +362,7 @@ class EventRuleTest(APITestCase):
return HttpResponse()
# Enqueue a webhook for processing
webhooks_queue = []
webhooks_queue = {}
site = Site.objects.create(name='Site 1', slug='site-1')
enqueue_object(
webhooks_queue,
@@ -369,7 +371,7 @@ class EventRuleTest(APITestCase):
request_id=request_id,
action=ObjectChangeActionChoices.ACTION_CREATE
)
flush_events(webhooks_queue)
flush_events(list(webhooks_queue.values()))
# Retrieve the job from queue
job = self.queue.jobs[0]
@@ -377,3 +379,24 @@ class EventRuleTest(APITestCase):
# Patch the Session object with our dummy_send() method, then process the webhook for sending
with patch.object(Session, 'send', dummy_send) as mock_send:
send_webhook(**job.kwargs)
def test_duplicate_triggers(self):
"""
Test for erroneous duplicate event triggers resulting from saving an object multiple times
within the span of a single request.
"""
url = reverse('dcim:site_add')
request = RequestFactory().get(url)
request.id = uuid.uuid4()
request.user = self.user
self.assertEqual(self.queue.count, 0, msg="Unexpected jobs found in queue")
with event_tracking(request):
site = Site(name='Site 1', slug='site-1')
site.save()
# Save the site a second time
site.save()
self.assertEqual(self.queue.count, 1, msg="Duplicate jobs found in queue")

View File

@@ -723,15 +723,15 @@ class ObjectChangeView(generic.ObjectView):
if not instance.prechange_data and instance.action in ['update', 'delete'] and prev_change:
non_atomic_change = True
prechange_data = prev_change.postchange_data
prechange_data = prev_change.postchange_data_clean
else:
non_atomic_change = False
prechange_data = instance.prechange_data
prechange_data = instance.prechange_data_clean
if prechange_data and instance.postchange_data:
diff_added = shallow_compare_dict(
prechange_data or dict(),
instance.postchange_data or dict(),
instance.postchange_data_clean or dict(),
exclude=['last_updated'],
)
diff_removed = {

View File

@@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _
from ipam.choices import *
from ipam.constants import *
from netbox.models import PrimaryModel
from netbox.models.features import ContactsMixin
from utilities.data import array_to_string
__all__ = (
@@ -62,7 +63,7 @@ class ServiceTemplate(ServiceBase, PrimaryModel):
return reverse('ipam:servicetemplate', args=[self.pk])
class Service(ServiceBase, PrimaryModel):
class Service(ContactsMixin, ServiceBase, PrimaryModel):
"""
A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may
optionally be tied to one or more specific IPAddresses belonging to its parent.

View File

@@ -649,7 +649,7 @@ class IPAddressTest(APIViewTestCases.APIViewTestCase):
'description': 'New description',
}
graphql_filter = {
'address': '192.168.0.1/24',
'address': {'lookup': 'i_exact', 'value': '192.168.0.1/24'},
}
@classmethod

View File

@@ -1280,3 +1280,8 @@ class ServiceBulkDeleteView(generic.BulkDeleteView):
queryset = Service.objects.prefetch_related('device', 'virtual_machine')
filterset = filtersets.ServiceFilterSet
table = tables.ServiceTable
@register_model_view(Service, 'contacts')
class ServiceContactsView(ObjectContactsView):
queryset = Service.objects.all()

View File

@@ -7,4 +7,4 @@ __all__ = (
current_request = ContextVar('current_request', default=None)
events_queue = ContextVar('events_queue', default=[])
events_queue = ContextVar('events_queue', default=dict())

View File

@@ -23,8 +23,9 @@ def map_strawberry_type(field):
elif isinstance(field, MultiValueArrayFilter):
pass
elif isinstance(field, MultiValueCharFilter):
should_create_function = True
attr_type = List[str] | None
# Note: Need to use the legacy FilterLookup from filters, not from
# strawberry_django.FilterLookup as we currently have USE_DEPRECATED_FILTERS
attr_type = strawberry_django.filters.FilterLookup[str] | None
elif isinstance(field, MultiValueDateFilter):
attr_type = auto
elif isinstance(field, MultiValueDateTimeFilter):

View File

@@ -25,7 +25,7 @@ from utilities.string import trailing_slash
# Environment setup
#
VERSION = '4.0.3'
VERSION = '4.0.5'
HOSTNAME = platform.node()
# Set the base directory two levels up
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -368,6 +368,8 @@ INSTALLED_APPS = [
'drf_spectacular',
'drf_spectacular_sidecar',
]
if not DEBUG:
INSTALLED_APPS.remove('debug_toolbar')
if not DJANGO_ADMIN_ENABLED:
INSTALLED_APPS.remove('django.contrib.admin')

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

@@ -27,10 +27,10 @@
"bootstrap": "5.3.3",
"clipboard": "2.0.11",
"flatpickr": "4.6.13",
"gridstack": "10.1.2",
"gridstack": "10.2.0",
"htmx.org": "1.9.12",
"query-string": "9.0.0",
"sass": "1.77.2",
"sass": "1.77.4",
"tom-select": "2.3.1",
"typeface-inter": "3.18.1",
"typeface-roboto-mono": "1.1.13"

View File

@@ -7,38 +7,74 @@ import { isTruthy } from './util';
*/
function quickSearchEventHandler(event: Event): void {
const quicksearch = event.currentTarget as HTMLInputElement;
const clearbtn = document.getElementById("quicksearch_clear") as HTMLAnchorElement;
const clearbtn = document.getElementById('quicksearch_clear') as HTMLAnchorElement;
if (isTruthy(clearbtn)) {
if (quicksearch.value === "") {
clearbtn.classList.add("invisible");
if (quicksearch.value === '') {
clearbtn.classList.add('invisible');
} else {
clearbtn.classList.remove("invisible");
clearbtn.classList.remove('invisible');
}
}
}
/**
* Clear the existing search parameters in the link to export Current View.
*/
function clearLinkParams(): void {
const link = document.getElementById('export_current_view') as HTMLLinkElement;
const linkUpdated = link?.href.split('&')[0];
link.setAttribute('href', linkUpdated);
}
/**
* Update the Export View link to add the Quick Search parameters.
* @param event
*/
function handleQuickSearchParams(event: Event): void {
const quickSearchParameters = event.currentTarget as HTMLInputElement;
// Clear the existing search parameters
clearLinkParams();
if (quickSearchParameters != null) {
const link = document.getElementById('export_current_view') as HTMLLinkElement;
const search_parameter = `q=${quickSearchParameters.value}`;
const linkUpdated = link?.href + '&' + search_parameter;
link.setAttribute('href', linkUpdated);
}
}
/**
* Initialize Quicksearch Event listener/handlers.
*/
export function initQuickSearch(): void {
const quicksearch = document.getElementById("quicksearch") as HTMLInputElement;
const clearbtn = document.getElementById("quicksearch_clear") as HTMLAnchorElement;
const quicksearch = document.getElementById('quicksearch') as HTMLInputElement;
const clearbtn = document.getElementById('quicksearch_clear') as HTMLAnchorElement;
if (isTruthy(quicksearch)) {
quicksearch.addEventListener("keyup", quickSearchEventHandler, {
passive: true
})
quicksearch.addEventListener("search", quickSearchEventHandler, {
passive: true
})
quicksearch.addEventListener('keyup', quickSearchEventHandler, {
passive: true,
});
quicksearch.addEventListener('search', quickSearchEventHandler, {
passive: true,
});
quicksearch.addEventListener('change', handleQuickSearchParams, {
passive: true,
});
if (isTruthy(clearbtn)) {
clearbtn.addEventListener("click", async () => {
const search = new Event('search');
quicksearch.value = '';
await new Promise(f => setTimeout(f, 100));
quicksearch.dispatchEvent(search);
}, {
passive: true
})
clearbtn.addEventListener(
'click',
async () => {
const search = new Event('search');
quicksearch.value = '';
await new Promise(f => setTimeout(f, 100));
quicksearch.dispatchEvent(search);
clearLinkParams();
},
{
passive: true,
},
);
}
}
}

View File

@@ -1,7 +1,7 @@
// Serialized data from change records
pre.change-data {
padding-right: 0;
padding-left: 0;
border-radius: 0;
padding: 0;
// Display each line individually for highlighting
> span {

View File

@@ -1,3 +1,10 @@
// Disable font-ligatures for Chromium based browsers
// Chromium requires `font-variant-ligatures: none` in addition to `font-feature-settings "liga" 0`
* {
font-feature-settings: "liga" 0;
font-variant-ligatures: none;
}
// Restore default foreground & background colors for <pre> blocks
pre {
background-color: transparent;
@@ -32,3 +39,8 @@ table a {
// Adjust table anchor link contrast as not enough contrast in dark mode
filter: brightness(110%);
}
// Override background color alpha value
[data-bs-theme=dark] ::selection {
background-color: rgba(var(--tblr-primary-rgb),.48)
}

View File

@@ -1754,10 +1754,10 @@ graphql@16.8.1:
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
gridstack@10.1.2:
version "10.1.2"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.1.2.tgz#58b5ae0057a8aa5e4f6563041c4ca2def3aa4268"
integrity sha512-Nn27XGQ68WtBC513cKQQ4t/dA2uuN/xnNUU50puXEJv6IFk5SzT0Dnsq68GpopO1n0tXUKZKm1Rw7uOUMDz1KQ==
gridstack@10.2.0:
version "10.2.0"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.2.0.tgz#4ba9c7ee69a730851721a9f5cb33dc55026ded1f"
integrity sha512-svKAOq/dfinpvhe/nnxdyZOOEd9qynXiOPHvL96PALE0yWChWp/6lechnqKwud0tL/rRyAfMJ6Hh/z2fS13pBA==
has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2"
@@ -2482,10 +2482,10 @@ safe-regex-test@^1.0.3:
es-errors "^1.3.0"
is-regex "^1.1.4"
sass@1.77.2:
version "1.77.2"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.2.tgz#18d4ed2eefc260cdc8099c5439ec1303fd5863aa"
integrity sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA==
sass@1.77.4:
version "1.77.4"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.4.tgz#92059c7bfc56b827c56eb116778d157ec017a5cd"
integrity sha512-vcF3Ckow6g939GMA4PeU7b2K/9FALXk2KF9J87txdHzXbUF9XRQRwSxcAs/fGaTnJeBFd7UoV22j3lzMLdM0Pw==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"

View File

@@ -5,7 +5,7 @@
<div class="row mb-3">
<div class="col col-md-12">
<div class="card">
<div class="card-body table-responsive">
<div class="table-responsive">
{% render_table table 'inc/table.html' %}
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
</div>

View File

@@ -5,6 +5,7 @@
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% load l10n %}
{% load mptt %}
{% block content %}
@@ -63,7 +64,7 @@
{% if object.latitude and object.longitude %}
{% if config.MAPS_URL %}
<div class="position-absolute top-50 end-0 translate-middle-y d-print-none">
<a href="{{ config.MAPS_URL }}{{ object.latitude }},{{ object.longitude }}" target="_blank" class="btn btn-primary">
<a href="{{ config.MAPS_URL }}{{ object.latitude|unlocalize }},{{ object.longitude|unlocalize }}" target="_blank" class="btn btn-primary">
<i class="mdi mdi-map-marker"></i> {% trans "Map It" %}
</a>
</div>

View File

@@ -3,6 +3,7 @@
{% load plugins %}
{% load tz %}
{% load i18n %}
{% load l10n %}
{% load mptt %}
{% block breadcrumbs %}
@@ -95,7 +96,7 @@
{% if object.latitude and object.longitude %}
{% if config.MAPS_URL %}
<div class="position-absolute top-50 end-0 translate-middle-y d-print-none">
<a href="{{ config.MAPS_URL }}{{ object.latitude }},{{ object.longitude }}" target="_blank" class="btn btn-primary">
<a href="{{ config.MAPS_URL }}{{ object.latitude|unlocalize }},{{ object.longitude|unlocalize }}" target="_blank" class="btn btn-primary">
<i class="mdi mdi-map-marker"></i> {% trans "Map It" %}
</a>
</div>

View File

@@ -1,5 +1,6 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block title %}{{ object }}{% endblock %}
@@ -22,7 +23,7 @@
{% block subtitle %}{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="row">
<div class="col col-md-5">
<div class="card">
<h5 class="card-header">{% trans "Change" %}</h5>
@@ -104,7 +105,7 @@
</div>
</div>
</div>
<div class="row mb-3">
<div class="row">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">{% trans "Pre-Change Data" %}</h5>
@@ -112,7 +113,7 @@
{% if object.prechange_data %}
{% spaceless %}
<pre class="change-data">
{% for k, v in object.prechange_data.items %}
{% for k, v in object.prechange_data_clean.items %}
<span{% if k in diff_removed %} class="removed"{% endif %}>{{ k }}: {{ v|json }}</span>
{% endfor %}
</pre>
@@ -132,7 +133,7 @@
{% if object.postchange_data %}
{% spaceless %}
<pre class="change-data">
{% for k, v in object.postchange_data.items %}
{% for k, v in object.postchange_data_clean.items %}
<span{% if k in diff_added %} class="added"{% endif %}>{{ k }}: {{ v|json }}</span>
{% endfor %}
</pre>
@@ -144,7 +145,15 @@
</div>
</div>
</div>
<div class="row mb-3">
<div class="row">
<div class="col col-md-6">
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% include 'inc/panel_table.html' with table=related_changes_table heading='Related Changes' panel_class='default' %}
{% if related_changes_count > related_changes_table.rows|length %}
@@ -158,4 +167,9 @@
{% endif %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -4,7 +4,7 @@
{% load log_levels %}
{% load i18n %}
{% block title %}{{ script }}{% endblock %}
{% block title %}{{ script.python_class.name }}{% endblock %}
{% block object_identifier %}
{{ script.full_name }}
@@ -17,7 +17,7 @@
{% block subtitle %}
<div class="text-secondary fs-5">
{{ script.Meta.description|markdown }}
{{ script.python_class.Meta.description|markdown }}
</div>
{% endblock subtitle %}

View File

@@ -56,15 +56,15 @@
<tr>
<td>
{% if script.is_executable %}
<a href="{% url 'extras:script' script.pk %}" id="{{ script.module }}.{{ script.class_name }}">{{ script.name }}</a>
<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.name }}</a>
<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.description|markdown|placeholder }}</td>
<td>{{ script.python_class.Meta.description|markdown|placeholder }}</td>
{% if last_job %}
<td>
<a href="{% url 'extras:script_result' job_pk=last_job.pk %}">{{ last_job.created|isodatetime }}</a>

View File

@@ -59,7 +59,7 @@
<th scope="row"><i class="mdi mdi-chip"></i> {% trans "Memory" %}</th>
<td>
{% if memory_sum %}
{{ memory_sum|humanize_megabytes }}
<span title={{ memory_sum }}>{{ memory_sum|humanize_megabytes }}</span>
{% else %}
{{ ''|placeholder }}
{% endif %}

View File

@@ -125,7 +125,7 @@
<th scope="row"><i class="mdi mdi-chip"></i> {% trans "Memory" %}</th>
<td>
{% if object.memory %}
{{ object.memory|humanize_megabytes }}
<span title={{ object.memory }}>{{ object.memory|humanize_megabytes }}</span>
{% else %}
{{ ''|placeholder }}
{% 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

View File

@@ -2,7 +2,6 @@ import json
from django.contrib.contenttypes.models import ContentType
from django.core import serializers
from mptt.models import MPTTModel
from extras.utils import is_taggable
@@ -16,8 +15,7 @@ def serialize_object(obj, resolve_tags=True, extra=None, exclude=None):
"""
Return a generic JSON representation of an object using Django's built-in serializer. (This is used for things like
change logging, not the REST API.) Optionally include a dictionary to supplement the object data. A list of keys
can be provided to exclude them from the returned dictionary. Private fields (prefaced with an underscore) are
implicitly excluded.
can be provided to exclude them from the returned dictionary.
Args:
obj: The object to serialize
@@ -30,11 +28,6 @@ def serialize_object(obj, resolve_tags=True, extra=None, exclude=None):
data = json.loads(json_str)[0]['fields']
exclude = exclude or []
# Exclude any MPTTModel fields
if issubclass(obj.__class__, MPTTModel):
for field in ['level', 'lft', 'rght', 'tree_id']:
data.pop(field)
# Include custom_field_data as "custom_fields"
if hasattr(obj, 'custom_field_data'):
data['custom_fields'] = data.pop('custom_field_data')
@@ -45,9 +38,9 @@ def serialize_object(obj, resolve_tags=True, extra=None, exclude=None):
tags = getattr(obj, '_tags', None) or obj.tags.all()
data['tags'] = sorted([tag.name for tag in tags])
# Skip excluded and private (prefixes with an underscore) attributes
# Skip any excluded attributes
for key in list(data.keys()):
if key in exclude or (isinstance(key, str) and key.startswith('_')):
if key in exclude:
data.pop(key)
# Append any extra data

View File

@@ -4,7 +4,7 @@
<i class="mdi mdi-download"></i> {% trans "Export" %}
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="?{% if url_params %}{{ url_params }}&{% endif %}export=table">{% trans "Current View" %}</a></li>
<li><a id="export_current_view" class="dropdown-item" href="?{% if url_params %}{{ url_params }}&{% endif %}export=table">{% trans "Current View" %}</a></li>
<li><a class="dropdown-item" href="?{% if url_params %}{{ url_params }}&{% endif %}export">{% trans "All Data" %} ({{ data_format }})</a></li>
{% if export_templates %}
<li>

View File

@@ -1,14 +1,9 @@
import datetime
import json
from typing import Dict, Any
from urllib.parse import quote
from django import template
from django.conf import settings
from django.template.defaultfilters import date
from django.urls import NoReverseMatch, reverse
from django.utils import timezone
from django.utils.safestring import mark_safe
from core.models import ObjectType
from utilities.forms import get_selected_values, TableConfigForm
@@ -92,15 +87,22 @@ def humanize_speed(speed):
@register.filter()
def humanize_megabytes(mb):
"""
Express a number of megabytes in the most suitable unit (e.g. gigabytes or terabytes).
Express a number of megabytes in the most suitable unit (e.g. gigabytes, terabytes, etc.).
"""
if not mb:
return ''
if not mb % 1048576: # 1024^2
return f'{int(mb / 1048576)} TB'
if not mb % 1024:
return f'{int(mb / 1024)} GB'
return f'{mb} MB'
return ""
PB_SIZE = 1000000000
TB_SIZE = 1000000
GB_SIZE = 1000
if mb >= PB_SIZE:
return f"{mb / PB_SIZE:.2f} PB"
if mb >= TB_SIZE:
return f"{mb / TB_SIZE:.2f} TB"
if mb >= GB_SIZE:
return f"{mb / GB_SIZE:.2f} GB"
return f"{mb} MB"
@register.filter()

View File

@@ -493,10 +493,18 @@ class APIViewTestCases:
def _build_filtered_query(self, name, **filters):
"""
Create a filtered query: i.e. ip_address_list(filters: {address: "1.1.1.1/24"}){.
Create a filtered query: i.e. device_list(filters: {name: {i_contains: "akron"}}){.
"""
# TODO: This should be extended to support AND, OR multi-lookups
if filters:
filter_string = ', '.join(f'{k}: "{v}"' for k, v in filters.items())
for field_name, params in filters.items():
lookup = params['lookup']
value = params['value']
if lookup:
query = f'{{{lookup}: "{value}"}}'
filter_string = f'{field_name}: {query}'
else:
filter_string = f'{field_name}: "{value}"'
filter_string = f'(filters: {{{filter_string}}})'
else:
filter_string = ''

View File

@@ -173,6 +173,8 @@ class VirtualMachineVMInterfaceTable(VMInterfaceTable):
default_columns = ('pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'ip_addresses')
row_attrs = {
'data-name': lambda record: record.name,
'data-virtual': lambda record: "true",
'data-enabled': lambda record: "true" if record.enabled else "false",
}

View File

@@ -1,6 +1,6 @@
Django==5.0.6
django-cors-headers==4.3.1
django-debug-toolbar==4.3.0
django-debug-toolbar==4.4.2
django-filter==24.2
django-htmx==1.17.3
django-graphiql-debug-toolbar==0.2.0
@@ -15,23 +15,23 @@ django-tables2==2.7.0
django-timezone-field==6.1.0
djangorestframework==3.15.1
drf-spectacular==0.27.2
drf-spectacular-sidecar==2024.5.1
drf-spectacular-sidecar==2024.6.1
feedparser==6.0.11
gunicorn==22.0.0
Jinja2==3.1.4
Markdown==3.6
mkdocs-material==9.5.24
mkdocs-material==9.5.26
mkdocstrings[python-legacy]==0.25.1
netaddr==1.2.1
netaddr==1.3.0
nh3==0.2.17
Pillow==10.3.0
psycopg[c,pool]==3.1.19
PyYAML==6.0.1
requests==2.32.2
requests==2.32.3
social-auth-app-django==5.4.1
social-auth-core==4.5.4
strawberry-graphql==0.230.0
strawberry-graphql-django==0.40.0
strawberry-graphql==0.234.0
strawberry-graphql-django==0.42.0
svgwrite==1.4.3
tablib==3.6.1
tzdata==2024.1