Merge branch 'main' into nav-menu-callables

This commit is contained in:
Brian Tiemann 2025-05-21 20:25:46 -04:00
commit 9a46c8e30d
133 changed files with 50421 additions and 44844 deletions

View File

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

View File

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

View File

@ -14,6 +14,12 @@ Administrators are encouraged to adhere to industry best practices concerning th
* Prohibit access to your database from clients other than the NetBox application * Prohibit access to your database from clients other than the NetBox application
* Keep your deployment updated to the most recent stable release * Keep your deployment updated to the most recent stable release
## Compliance Reporting
Please note that security compliance reports (e.g. SOC 2) are provided by NetBox Labs only to customers using NetBox Cloud or NetBox Enterprise. They are not available to users of self-hosted NetBox Community Edition.
If you would like to consider upgrading to NetBox Cloud or Enterprise, please contact `sales@netboxlabs.com`.
## Reporting a Suspected Vulnerability ## Reporting a Suspected Vulnerability
If you believe you've uncovered a security vulnerability and wish to report it confidentially, you may do so by emailing `security@netboxlabs.com`. Please ensure that your report meets all the following conditions: If you believe you've uncovered a security vulnerability and wish to report it confidentially, you may do so by emailing `security@netboxlabs.com`. Please ensure that your report meets all the following conditions:

View File

@ -329,6 +329,7 @@
"100base-tx", "100base-tx",
"100base-t1", "100base-t1",
"1000base-t", "1000base-t",
"1000base-sx",
"1000base-lx", "1000base-lx",
"1000base-tx", "1000base-tx",
"2.5gbase-t", "2.5gbase-t",

View File

@ -35,7 +35,7 @@ Note that a plugin must be listed in `PLUGINS` for its configuration to take eff
## PLUGINS_CATALOG_CONFIG ## PLUGINS_CATALOG_CONFIG
Default: Empty Default: `{}` (Empty)
This parameter controls how individual plugins are displayed in the plugins catalog under Admin > System > Plugins. Adding a plugin to the `hidden` list will omit that plugin from the catalog. Adding a plugin to the `static` list will display the plugin, but not link to the plugin details or upgrade instructions. This parameter controls how individual plugins are displayed in the plugins catalog under Admin > System > Plugins. Adding a plugin to the `hidden` list will omit that plugin from the catalog. Adding a plugin to the `static` list will display the plugin, but not link to the plugin details or upgrade instructions.

View File

@ -135,7 +135,7 @@ DEFAULT_PERMISSIONS = {
## EXEMPT_VIEW_PERMISSIONS ## EXEMPT_VIEW_PERMISSIONS
Default: Empty list Default: `[]` (Empty list)
A list of NetBox models to exempt from the enforcement of view permissions. Models listed here will be viewable by all users, both authenticated and anonymous. A list of NetBox models to exempt from the enforcement of view permissions. Models listed here will be viewable by all users, both authenticated and anonymous.
@ -191,7 +191,7 @@ The lifetime (in seconds) of the authentication cookie issued to a NetBox user u
## LOGIN_FORM_HIDDEN ## LOGIN_FORM_HIDDEN
Default: False Default: `False`
Option to hide the login form when only SSO authentication is in use. Option to hide the login form when only SSO authentication is in use.

View File

@ -53,6 +53,7 @@ If a new Django release is adopted or other major dependencies (Python, PostgreS
* Update the installation guide (`docs/installation/index.md`) with the new minimum versions. * Update the installation guide (`docs/installation/index.md`) with the new minimum versions.
* Update the upgrade guide (`docs/installation/upgrading.md`) for the current version accordingly. * Update the upgrade guide (`docs/installation/upgrading.md`) for the current version accordingly.
* Update the minimum PostgreSQL version in the programming error template (`netbox/templates/exceptions/programming_error.html`).
### Manually Perform a New Install ### Manually Perform a New Install

View File

@ -122,7 +122,7 @@ sudo cp /opt/netbox-$OLDVER/gunicorn.py /opt/netbox/
### Option B: Check Out a Git Release ### Option B: Check Out a Git Release
This guide assumes that NetBox is installed at `/opt/netbox`. First, determine the latest release either by visiting our [releases page](https://github.com/netbox-community/netbox/releases) or by running the following command: This guide assumes that NetBox is installed in `/opt/netbox`. First, determine the latest release either by visiting our [releases page](https://github.com/netbox-community/netbox/releases) or by running the following command:
``` ```
git ls-remote --tags https://github.com/netbox-community/netbox.git \ git ls-remote --tags https://github.com/netbox-community/netbox.git \
@ -134,6 +134,8 @@ git ls-remote --tags https://github.com/netbox-community/netbox.git \
Check out the desired release by specifying its tag. For example: Check out the desired release by specifying its tag. For example:
``` ```
cd /opt/netbox && \
sudo git fetch && \
sudo git checkout v4.2.7 sudo git checkout v4.2.7
``` ```

View File

@ -1,3 +1,35 @@
# NetBox v4.3
## v4.3.1 (2025-05-13)
### Enhancements
* [#17073](https://github.com/netbox-community/netbox/issues/17073) - Enable global search for tags
* [#18419](https://github.com/netbox-community/netbox/issues/18419) - Enable specifying a queue name when calling `Job.enqueue()`
* [#19416](https://github.com/netbox-community/netbox/issues/19416) - Add the 1000BASE-SX interface type
* [#19434](https://github.com/netbox-community/netbox/issues/19434) - Add pre-populated interface speed choices for 2.5 and 5 Gbps
### Bug Fixes
* [#17107](https://github.com/netbox-community/netbox/issues/17107) - Fix cosmetic issue in cable traces ending at a provider network
* [#19309](https://github.com/netbox-community/netbox/issues/19309) - Improve REST API query performance for prefixes and IP addresses
* [#19361](https://github.com/netbox-community/netbox/issues/19361) - Fix incorrect GraphQL object types
* [#19375](https://github.com/netbox-community/netbox/issues/19375) - Fix table configuration after applying a saved table config
* [#19376](https://github.com/netbox-community/netbox/issues/19376) - Fix `FieldDoesNotExist` exception when global search results include a contact
* [#19380](https://github.com/netbox-community/netbox/issues/19380) - Fix column selections for child object tables
* [#19381](https://github.com/netbox-community/netbox/issues/19381) - Fix syncing of custom scripts from a remote data source
* [#19396](https://github.com/netbox-community/netbox/issues/19396) - Enable nullifying VLAN `qinq_role` via the REST API
* [#19397](https://github.com/netbox-community/netbox/issues/19397) - Correct enum type for IPRangeFilter in GraphQL API
* [#19432](https://github.com/netbox-community/netbox/issues/19432) - Update minimum required PostgreSQL version referenced by server error page
* [#19440](https://github.com/netbox-community/netbox/issues/19440) - Ensure data migrations use the correct database connection
* [#19444](https://github.com/netbox-community/netbox/issues/19444) - Fix change logging for contact group assignments
* [#19463](https://github.com/netbox-community/netbox/issues/19463) - Hide button dropdown for tables which do not support saved configs
* [#19464](https://github.com/netbox-community/netbox/issues/19464) - Fix bulk editing of inventory items from device view
* [#19465](https://github.com/netbox-community/netbox/issues/19465) - Fix ability to clear assigned prefix scope in UI
* [#19472](https://github.com/netbox-community/netbox/issues/19472) - Fix device column rendering in virtual device contexts table
---
## v4.3.0 (2025-05-01) ## v4.3.0 (2025-05-01)
### Breaking Changes ### Breaking Changes

View File

@ -197,6 +197,7 @@ class ProfileView(LoginRequiredMixin, View):
'changed_object_type' 'changed_object_type'
)[:20] )[:20]
changelog_table = ObjectChangeTable(changelog) changelog_table = ObjectChangeTable(changelog)
changelog_table.configure(request)
return render(request, self.template_name, { return render(request, self.template_name, {
'changelog_table': changelog_table, 'changelog_table': changelog_table,

View File

@ -16,6 +16,7 @@ from utilities.forms import get_field_value
from utilities.forms.fields import ( from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
) )
from utilities.forms.mixins import DistanceValidationMixin
from utilities.forms.rendering import FieldSet, InlineFields from utilities.forms.rendering import FieldSet, InlineFields
from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions
from utilities.templatetags.builtins.filters import bettertitle from utilities.templatetags.builtins.filters import bettertitle
@ -105,7 +106,7 @@ class CircuitTypeForm(NetBoxModelForm):
] ]
class CircuitForm(TenancyForm, NetBoxModelForm): class CircuitForm(DistanceValidationMixin, TenancyForm, NetBoxModelForm):
provider = DynamicModelChoiceField( provider = DynamicModelChoiceField(
label=_('Provider'), label=_('Provider'),
queryset=Provider.objects.all(), queryset=Provider.objects.all(),

View File

@ -41,7 +41,7 @@ __all__ = (
) )
@strawberry_django.filter(models.CircuitTermination, lookups=True) @strawberry_django.filter_type(models.CircuitTermination, lookups=True)
class CircuitTerminationFilter( class CircuitTerminationFilter(
BaseObjectTypeFilterMixin, BaseObjectTypeFilterMixin,
CustomFieldsFilterMixin, CustomFieldsFilterMixin,
@ -87,7 +87,7 @@ class CircuitTerminationFilter(
) )
@strawberry_django.filter(models.Circuit, lookups=True) @strawberry_django.filter_type(models.Circuit, lookups=True)
class CircuitFilter( class CircuitFilter(
ContactFilterMixin, ContactFilterMixin,
ImageAttachmentFilterMixin, ImageAttachmentFilterMixin,
@ -121,17 +121,17 @@ class CircuitFilter(
) )
@strawberry_django.filter(models.CircuitType, lookups=True) @strawberry_django.filter_type(models.CircuitType, lookups=True)
class CircuitTypeFilter(BaseCircuitTypeFilterMixin): class CircuitTypeFilter(BaseCircuitTypeFilterMixin):
pass pass
@strawberry_django.filter(models.CircuitGroup, lookups=True) @strawberry_django.filter_type(models.CircuitGroup, lookups=True)
class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilterMixin): class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
pass pass
@strawberry_django.filter(models.CircuitGroupAssignment, lookups=True) @strawberry_django.filter_type(models.CircuitGroupAssignment, lookups=True)
class CircuitGroupAssignmentFilter( class CircuitGroupAssignmentFilter(
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
): ):
@ -148,7 +148,7 @@ class CircuitGroupAssignmentFilter(
) )
@strawberry_django.filter(models.Provider, lookups=True) @strawberry_django.filter_type(models.Provider, lookups=True)
class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin): class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field() slug: FilterLookup[str] | None = strawberry_django.filter_field()
@ -158,7 +158,7 @@ class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin):
) )
@strawberry_django.filter(models.ProviderAccount, lookups=True) @strawberry_django.filter_type(models.ProviderAccount, lookups=True)
class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin): class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -168,7 +168,7 @@ class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ProviderNetwork, lookups=True) @strawberry_django.filter_type(models.ProviderNetwork, lookups=True)
class ProviderNetworkFilter(PrimaryModelFilterMixin): class ProviderNetworkFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
@ -178,12 +178,12 @@ class ProviderNetworkFilter(PrimaryModelFilterMixin):
service_id: FilterLookup[str] | None = strawberry_django.filter_field() service_id: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.VirtualCircuitType, lookups=True) @strawberry_django.filter_type(models.VirtualCircuitType, lookups=True)
class VirtualCircuitTypeFilter(BaseCircuitTypeFilterMixin): class VirtualCircuitTypeFilter(BaseCircuitTypeFilterMixin):
pass pass
@strawberry_django.filter(models.VirtualCircuit, lookups=True) @strawberry_django.filter_type(models.VirtualCircuit, lookups=True)
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin): class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
cid: FilterLookup[str] | None = strawberry_django.filter_field() cid: FilterLookup[str] | None = strawberry_django.filter_field()
provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
@ -206,7 +206,7 @@ class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
) )
@strawberry_django.filter(models.VirtualCircuitTermination, lookups=True) @strawberry_django.filter_type(models.VirtualCircuitTermination, lookups=True)
class VirtualCircuitTerminationFilter( class VirtualCircuitTerminationFilter(
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
): ):

View File

@ -8,10 +8,11 @@ def set_null_values(apps, schema_editor):
Circuit = apps.get_model('circuits', 'Circuit') Circuit = apps.get_model('circuits', 'Circuit')
CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment') CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment')
CircuitTermination = apps.get_model('circuits', 'CircuitTermination') CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
db_alias = schema_editor.connection.alias
Circuit.objects.filter(distance_unit='').update(distance_unit=None) Circuit.objects.using(db_alias).filter(distance_unit='').update(distance_unit=None)
CircuitGroupAssignment.objects.filter(priority='').update(priority=None) CircuitGroupAssignment.objects.using(db_alias).filter(priority='').update(priority=None)
CircuitTermination.objects.filter(cable_end='').update(cable_end=None) CircuitTermination.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -8,14 +8,15 @@ def copy_site_assignments(apps, schema_editor):
""" """
ContentType = apps.get_model('contenttypes', 'ContentType') ContentType = apps.get_model('contenttypes', 'ContentType')
CircuitTermination = apps.get_model('circuits', 'CircuitTermination') CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
ProviderNetwork = apps.get_model('circuits', 'ProviderNetwork')
Site = apps.get_model('dcim', 'Site') Site = apps.get_model('dcim', 'Site')
db_alias = schema_editor.connection.alias
CircuitTermination.objects.filter(site__isnull=False).update( CircuitTermination.objects.using(db_alias).filter(site__isnull=False).update(
termination_type=ContentType.objects.get_for_model(Site), termination_id=models.F('site_id') termination_type=ContentType.objects.get_for_model(Site), termination_id=models.F('site_id')
) )
ProviderNetwork = apps.get_model('circuits', 'ProviderNetwork') CircuitTermination.objects.using(db_alias).filter(provider_network__isnull=False).update(
CircuitTermination.objects.filter(provider_network__isnull=False).update(
termination_type=ContentType.objects.get_for_model(ProviderNetwork), termination_type=ContentType.objects.get_for_model(ProviderNetwork),
termination_id=models.F('provider_network_id'), termination_id=models.F('provider_network_id'),
) )

View File

@ -7,15 +7,20 @@ def populate_denormalized_fields(apps, schema_editor):
Copy site ForeignKey values to the Termination GFK. Copy site ForeignKey values to the Termination GFK.
""" """
CircuitTermination = apps.get_model('circuits', 'CircuitTermination') CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
db_alias = schema_editor.connection.alias
terminations = CircuitTermination.objects.filter(site__isnull=False).prefetch_related('site') terminations = CircuitTermination.objects.using(db_alias).filter(site__isnull=False).prefetch_related('site')
for termination in terminations: for termination in terminations:
termination._region_id = termination.site.region_id termination._region_id = termination.site.region_id
termination._site_group_id = termination.site.group_id termination._site_group_id = termination.site.group_id
termination._site_id = termination.site_id termination._site_id = termination.site_id
# Note: Location cannot be set prior to migration # Note: Location cannot be set prior to migration
CircuitTermination.objects.bulk_update(terminations, ['_region', '_site_group', '_site'], batch_size=100) CircuitTermination.objects.using(db_alias).bulk_update(
terminations,
['_region', '_site_group', '_site'],
batch_size=100
)
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -9,8 +9,9 @@ def set_member_type(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType') ContentType = apps.get_model('contenttypes', 'ContentType')
Circuit = apps.get_model('circuits', 'Circuit') Circuit = apps.get_model('circuits', 'Circuit')
CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment') CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment')
db_alias = schema_editor.connection.alias
CircuitGroupAssignment.objects.update( CircuitGroupAssignment.objects.using(db_alias).update(
member_type=ContentType.objects.get_for_model(Circuit) member_type=ContentType.objects.get_for_model(Circuit)
) )

View File

@ -0,0 +1,16 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0051_virtualcircuit_group_assignment'),
]
operations = [
migrations.AlterField(
model_name='circuit',
name='_abs_distance',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=13, null=True),
),
]

View File

@ -54,9 +54,8 @@ class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable)
linkify=True, linkify=True,
verbose_name=_('Account') verbose_name=_('Account')
) )
type = tables.Column( type = columns.ColoredLabelColumn(
verbose_name=_('Type'), verbose_name=_('Type'),
linkify=True
) )
status = columns.ChoiceFieldColumn() status = columns.ChoiceFieldColumn()
termination_count = columns.LinkedCountColumn( termination_count = columns.LinkedCountColumn(

View File

@ -23,7 +23,7 @@ __all__ = (
) )
@strawberry_django.filter(models.DataFile, lookups=True) @strawberry_django.filter_type(models.DataFile, lookups=True)
class DataFileFilter(BaseFilterMixin): class DataFileFilter(BaseFilterMixin):
id: ID | None = strawberry_django.filter_field() id: ID | None = strawberry_django.filter_field()
created: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() created: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
@ -39,7 +39,7 @@ class DataFileFilter(BaseFilterMixin):
hash: FilterLookup[str] | None = strawberry_django.filter_field() hash: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.DataSource, lookups=True) @strawberry_django.filter_type(models.DataSource, lookups=True)
class DataSourceFilter(PrimaryModelFilterMixin): class DataSourceFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
type: FilterLookup[str] | None = strawberry_django.filter_field() type: FilterLookup[str] | None = strawberry_django.filter_field()
@ -56,7 +56,7 @@ class DataSourceFilter(PrimaryModelFilterMixin):
) )
@strawberry_django.filter(models.ObjectChange, lookups=True) @strawberry_django.filter_type(models.ObjectChange, lookups=True)
class ObjectChangeFilter(BaseFilterMixin): class ObjectChangeFilter(BaseFilterMixin):
id: ID | None = strawberry_django.filter_field() id: ID | None = strawberry_django.filter_field()
time: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() time: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
@ -82,7 +82,7 @@ class ObjectChangeFilter(BaseFilterMixin):
) )
@strawberry_django.filter(DjangoContentType, lookups=True) @strawberry_django.filter_type(DjangoContentType, lookups=True)
class ContentTypeFilter(BaseFilterMixin): class ContentTypeFilter(BaseFilterMixin):
id: ID | None = strawberry_django.filter_field() id: ID | None = strawberry_django.filter_field()
app_label: FilterLookup[str] | None = strawberry_django.filter_field() app_label: FilterLookup[str] | None = strawberry_django.filter_field()

View File

@ -88,19 +88,11 @@ class ManagedFile(SyncedDataMixin, models.Model):
def sync_data(self): def sync_data(self):
if self.data_file: if self.data_file:
self.file_path = os.path.basename(self.data_path) self.file_path = os.path.basename(self.data_path)
self._write_to_disk(self.full_path, overwrite=True)
def _write_to_disk(self, path, overwrite=False): storage = self.storage
"""
Write the object's data to disk at the specified path
"""
# Check whether file already exists
storage = self.storage
if storage.exists(path) and not overwrite:
raise FileExistsError()
with storage.open(path, 'wb+') as new_file: with storage.open(self.full_path, 'wb+') as new_file:
new_file.write(self.data) new_file.write(self.data_file.data)
@cached_property @cached_property
def storage(self): def storage(self):

View File

@ -215,6 +215,7 @@ class Job(models.Model):
schedule_at=None, schedule_at=None,
interval=None, interval=None,
immediate=False, immediate=False,
queue_name=None,
**kwargs **kwargs
): ):
""" """
@ -238,7 +239,7 @@ class Job(models.Model):
object_id = instance.pk object_id = instance.pk
else: else:
object_type = object_id = None object_type = object_id = None
rq_queue_name = get_queue_for_model(object_type.model if object_type else None) rq_queue_name = queue_name if queue_name else get_queue_for_model(object_type.model if object_type else None)
queue = django_rq.get_queue(rq_queue_name) queue = django_rq.get_queue(rq_queue_name)
status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING
job = Job( job = Job(

View File

@ -9,6 +9,7 @@ from rq.registry import FailedJobRegistry, StartedJobRegistry
from users.models import Token, User from users.models import Token, User
from utilities.testing import APITestCase, APIViewTestCases, TestCase from utilities.testing import APITestCase, APIViewTestCases, TestCase
from utilities.testing.utils import disable_logging
from ..models import * from ..models import *
@ -189,7 +190,8 @@ class BackgroundTaskTestCase(TestCase):
# Enqueue & run a job that will fail # Enqueue & run a job that will fail
job = queue.enqueue(self.dummy_job_failing) job = queue.enqueue(self.dummy_job_failing)
worker = get_worker('default') worker = get_worker('default')
worker.work(burst=True) with disable_logging():
worker.work(burst=True)
self.assertTrue(job.is_failed) self.assertTrue(job.is_failed)
# Re-enqueue the failed job and check that its status has been reset # Re-enqueue the failed job and check that its status has been reset
@ -231,7 +233,8 @@ class BackgroundTaskTestCase(TestCase):
self.assertEqual(job.get_status(), JobStatus.STARTED) self.assertEqual(job.get_status(), JobStatus.STARTED)
response = self.client.post(reverse('core-api:rqtask-stop', args=[job.id]), **self.header) response = self.client.post(reverse('core-api:rqtask-stop', args=[job.id]), **self.header)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started with disable_logging():
worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started
started_job_registry = StartedJobRegistry(queue.name, connection=queue.connection) started_job_registry = StartedJobRegistry(queue.name, connection=queue.connection)
self.assertEqual(len(started_job_registry), 0) self.assertEqual(len(started_job_registry), 0)

View File

@ -14,7 +14,7 @@ from core.choices import ObjectChangeActionChoices
from core.models import * from core.models import *
from dcim.models import Site from dcim.models import Site
from users.models import User from users.models import User
from utilities.testing import TestCase, ViewTestCases, create_tags from utilities.testing import TestCase, ViewTestCases, create_tags, disable_logging
class DataSourceTestCase(ViewTestCases.PrimaryObjectViewTestCase): class DataSourceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@ -271,7 +271,8 @@ class BackgroundTaskTestCase(TestCase):
# Enqueue & run a job that will fail # Enqueue & run a job that will fail
job = queue.enqueue(self.dummy_job_failing) job = queue.enqueue(self.dummy_job_failing)
worker = get_worker('default') worker = get_worker('default')
worker.work(burst=True) with disable_logging():
worker.work(burst=True)
self.assertTrue(job.is_failed) self.assertTrue(job.is_failed)
# Re-enqueue the failed job and check that its status has been reset # Re-enqueue the failed job and check that its status has been reset
@ -317,7 +318,8 @@ class BackgroundTaskTestCase(TestCase):
self.assertEqual(len(started_job_registry), 1) self.assertEqual(len(started_job_registry), 1)
response = self.client.get(reverse('core:background_task_stop', args=[job.id])) response = self.client.get(reverse('core:background_task_stop', args=[job.id]))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started with disable_logging():
worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started
self.assertEqual(len(started_job_registry), 0) self.assertEqual(len(started_job_registry), 0)
canceled_job_registry = FailedJobRegistry(queue.name, connection=queue.connection) canceled_job_registry = FailedJobRegistry(queue.name, connection=queue.connection)

View File

@ -223,6 +223,7 @@ class ObjectChangeView(generic.ObjectView):
data=related_changes[:50], data=related_changes[:50],
orderable=False orderable=False
) )
related_changes_table.configure(request)
objectchanges = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter( objectchanges = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
changed_object_type=instance.changed_object_type, changed_object_type=instance.changed_object_type,

View File

@ -461,6 +461,7 @@ class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
Interface.objects.select_related("device", "cable"), Interface.objects.select_related("device", "cable"),
], ],
), ),
'virtual_circuit_termination',
'l2vpn_terminations', # Referenced by InterfaceSerializer.l2vpn_termination 'l2vpn_terminations', # Referenced by InterfaceSerializer.l2vpn_termination
'ip_addresses', # Referenced by Interface.count_ipaddresses() 'ip_addresses', # Referenced by Interface.count_ipaddresses()
'fhrp_group_assignments', # Referenced by Interface.count_fhrp_groups() 'fhrp_group_assignments', # Referenced by Interface.count_fhrp_groups()

View File

@ -874,6 +874,7 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_100ME_T1 = '100base-t1' TYPE_100ME_T1 = '100base-t1'
TYPE_100ME_SFP = '100base-x-sfp' TYPE_100ME_SFP = '100base-x-sfp'
TYPE_1GE_FIXED = '1000base-t' TYPE_1GE_FIXED = '1000base-t'
TYPE_1GE_SX_FIXED = '1000base-sx'
TYPE_1GE_LX_FIXED = '1000base-lx' TYPE_1GE_LX_FIXED = '1000base-lx'
TYPE_1GE_TX_FIXED = '1000base-tx' TYPE_1GE_TX_FIXED = '1000base-tx'
TYPE_1GE_GBIC = '1000base-x-gbic' TYPE_1GE_GBIC = '1000base-x-gbic'
@ -1038,6 +1039,7 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'), (TYPE_100ME_FIXED, '100BASE-TX (10/100ME)'),
(TYPE_100ME_T1, '100BASE-T1 (10/100ME Single Pair)'), (TYPE_100ME_T1, '100BASE-T1 (10/100ME Single Pair)'),
(TYPE_1GE_FIXED, '1000BASE-T (1GE)'), (TYPE_1GE_FIXED, '1000BASE-T (1GE)'),
(TYPE_1GE_SX_FIXED, '1000BASE-SX (1GE)'),
(TYPE_1GE_LX_FIXED, '1000BASE-LX (1GE)'), (TYPE_1GE_LX_FIXED, '1000BASE-LX (1GE)'),
(TYPE_1GE_TX_FIXED, '1000BASE-TX (1GE)'), (TYPE_1GE_TX_FIXED, '1000BASE-TX (1GE)'),
(TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'), (TYPE_2GE_FIXED, '2.5GBASE-T (2.5GE)'),
@ -1238,6 +1240,8 @@ class InterfaceSpeedChoices(ChoiceSet):
(10000, '10 Mbps'), (10000, '10 Mbps'),
(100000, '100 Mbps'), (100000, '100 Mbps'),
(1000000, '1 Gbps'), (1000000, '1 Gbps'),
(2500000, '2.5 Gbps'),
(5000000, '5 Gbps'),
(10000000, '10 Gbps'), (10000000, '10 Gbps'),
(25000000, '25 Gbps'), (25000000, '25 Gbps'),
(40000000, '40 Gbps'), (40000000, '40 Gbps'),

View File

@ -1779,6 +1779,13 @@ class InventoryItemBulkEditForm(
) )
nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description') nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Remove parent device passed as context to avoid conflicts with the actual device field
# on this form (see bug #19464)
self.initial.pop('device', None)
# #
# Device component roles # Device component roles

View File

@ -66,6 +66,10 @@ class ScopedForm(forms.Form):
if self.instance and scope_type_id != self.instance.scope_type_id: if self.instance and scope_type_id != self.instance.scope_type_id:
self.initial['scope'] = None self.initial['scope'] = None
else:
# Clear the initial scope value if scope_type is not set
self.initial['scope'] = None
class ScopedBulkEditForm(forms.Form): class ScopedBulkEditForm(forms.Form):
scope_type = ContentTypeChoiceField( scope_type = ContentTypeChoiceField(

View File

@ -90,7 +90,7 @@ __all__ = (
) )
@strawberry_django.filter(models.Cable, lookups=True) @strawberry_django.filter_type(models.Cable, lookups=True)
class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin): class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin):
type: Annotated['CableTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() type: Annotated['CableTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
status: Annotated['LinkStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() status: Annotated['LinkStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
@ -107,7 +107,7 @@ class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin):
) )
@strawberry_django.filter(models.CableTermination, lookups=True) @strawberry_django.filter_type(models.CableTermination, lookups=True)
class CableTerminationFilter(ChangeLogFilterMixin): class CableTerminationFilter(ChangeLogFilterMixin):
cable: Annotated['CableFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() cable: Annotated['CableFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
cable_id: ID | None = strawberry_django.filter_field() cable_id: ID | None = strawberry_django.filter_field()
@ -120,7 +120,7 @@ class CableTerminationFilter(ChangeLogFilterMixin):
termination_id: ID | None = strawberry_django.filter_field() termination_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ConsolePort, lookups=True) @strawberry_django.filter_type(models.ConsolePort, lookups=True)
class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin): class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -130,14 +130,14 @@ class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilte
) )
@strawberry_django.filter(models.ConsolePortTemplate, lookups=True) @strawberry_django.filter_type(models.ConsolePortTemplate, lookups=True)
class ConsolePortTemplateFilter(ModularComponentTemplateFilterMixin): class ConsolePortTemplateFilter(ModularComponentTemplateFilterMixin):
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@strawberry_django.filter(models.ConsoleServerPort, lookups=True) @strawberry_django.filter_type(models.ConsoleServerPort, lookups=True)
class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin): class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -147,14 +147,14 @@ class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectMode
) )
@strawberry_django.filter(models.ConsoleServerPortTemplate, lookups=True) @strawberry_django.filter_type(models.ConsoleServerPortTemplate, lookups=True)
class ConsoleServerPortTemplateFilter(ModularComponentTemplateFilterMixin): class ConsoleServerPortTemplateFilter(ModularComponentTemplateFilterMixin):
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@strawberry_django.filter(models.Device, lookups=True) @strawberry_django.filter_type(models.Device, lookups=True)
class DeviceFilter( class DeviceFilter(
ContactFilterMixin, ContactFilterMixin,
TenancyFilterMixin, TenancyFilterMixin,
@ -271,7 +271,7 @@ class DeviceFilter(
inventory_item_count: FilterLookup[int] | None = strawberry_django.filter_field() inventory_item_count: FilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.DeviceBay, lookups=True) @strawberry_django.filter_type(models.DeviceBay, lookups=True)
class DeviceBayFilter(ComponentModelFilterMixin): class DeviceBayFilter(ComponentModelFilterMixin):
installed_device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( installed_device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -279,12 +279,12 @@ class DeviceBayFilter(ComponentModelFilterMixin):
installed_device_id: ID | None = strawberry_django.filter_field() installed_device_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter(models.DeviceBayTemplate, lookups=True) @strawberry_django.filter_type(models.DeviceBayTemplate, lookups=True)
class DeviceBayTemplateFilter(ComponentTemplateFilterMixin): class DeviceBayTemplateFilter(ComponentTemplateFilterMixin):
pass pass
@strawberry_django.filter(models.InventoryItemTemplate, lookups=True) @strawberry_django.filter_type(models.InventoryItemTemplate, lookups=True)
class InventoryItemTemplateFilter(ComponentTemplateFilterMixin): class InventoryItemTemplateFilter(ComponentTemplateFilterMixin):
parent: Annotated['InventoryItemTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( parent: Annotated['InventoryItemTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -304,13 +304,13 @@ class InventoryItemTemplateFilter(ComponentTemplateFilterMixin):
part_id: FilterLookup[str] | None = strawberry_django.filter_field() part_id: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.DeviceRole, lookups=True) @strawberry_django.filter_type(models.DeviceRole, lookups=True)
class DeviceRoleFilter(OrganizationalModelFilterMixin, RenderConfigFilterMixin): class DeviceRoleFilter(OrganizationalModelFilterMixin, RenderConfigFilterMixin):
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
vm_role: FilterLookup[bool] | None = strawberry_django.filter_field() vm_role: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.DeviceType, lookups=True) @strawberry_django.filter_type(models.DeviceType, lookups=True)
class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin): class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -382,7 +382,7 @@ class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig
inventory_item_template_count: FilterLookup[int] | None = strawberry_django.filter_field() inventory_item_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.FrontPort, lookups=True) @strawberry_django.filter_type(models.FrontPort, lookups=True)
class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin): class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@ -395,7 +395,7 @@ class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterM
) )
@strawberry_django.filter(models.FrontPortTemplate, lookups=True) @strawberry_django.filter_type(models.FrontPortTemplate, lookups=True)
class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin): class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin):
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@ -408,7 +408,7 @@ class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin):
) )
@strawberry_django.filter(models.MACAddress, lookups=True) @strawberry_django.filter_type(models.MACAddress, lookups=True)
class MACAddressFilter(PrimaryModelFilterMixin): class MACAddressFilter(PrimaryModelFilterMixin):
mac_address: FilterLookup[str] | None = strawberry_django.filter_field() mac_address: FilterLookup[str] | None = strawberry_django.filter_field()
assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
@ -417,7 +417,7 @@ class MACAddressFilter(PrimaryModelFilterMixin):
assigned_object_id: ID | None = strawberry_django.filter_field() assigned_object_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter(models.Interface, lookups=True) @strawberry_django.filter_type(models.Interface, lookups=True)
class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin, CabledObjectModelFilterMixin): class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin, CabledObjectModelFilterMixin):
vcdcs: Annotated['VirtualDeviceContextFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( vcdcs: Annotated['VirtualDeviceContextFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -486,7 +486,7 @@ class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin
) )
@strawberry_django.filter(models.InterfaceTemplate, lookups=True) @strawberry_django.filter_type(models.InterfaceTemplate, lookups=True)
class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin): class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin):
type: Annotated['InterfaceTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( type: Annotated['InterfaceTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -508,7 +508,7 @@ class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin):
) )
@strawberry_django.filter(models.InventoryItem, lookups=True) @strawberry_django.filter_type(models.InventoryItem, lookups=True)
class InventoryItemFilter(ComponentModelFilterMixin): class InventoryItemFilter(ComponentModelFilterMixin):
parent: Annotated['InventoryItemFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( parent: Annotated['InventoryItemFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -535,12 +535,12 @@ class InventoryItemFilter(ComponentModelFilterMixin):
discovered: FilterLookup[bool] | None = strawberry_django.filter_field() discovered: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.InventoryItemRole, lookups=True) @strawberry_django.filter_type(models.InventoryItemRole, lookups=True)
class InventoryItemRoleFilter(OrganizationalModelFilterMixin): class InventoryItemRoleFilter(OrganizationalModelFilterMixin):
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.Location, lookups=True) @strawberry_django.filter_type(models.Location, lookups=True)
class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, NestedGroupModelFilterMixin): class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, NestedGroupModelFilterMixin):
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
site_id: ID | None = strawberry_django.filter_field() site_id: ID | None = strawberry_django.filter_field()
@ -556,12 +556,12 @@ class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilt
) )
@strawberry_django.filter(models.Manufacturer, lookups=True) @strawberry_django.filter_type(models.Manufacturer, lookups=True)
class ManufacturerFilter(ContactFilterMixin, OrganizationalModelFilterMixin): class ManufacturerFilter(ContactFilterMixin, OrganizationalModelFilterMixin):
pass pass
@strawberry_django.filter(models.Module, lookups=True) @strawberry_django.filter_type(models.Module, lookups=True)
class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin): class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin):
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
device_id: ID | None = strawberry_django.filter_field() device_id: ID | None = strawberry_django.filter_field()
@ -610,7 +610,7 @@ class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin):
) )
@strawberry_django.filter(models.ModuleBay, lookups=True) @strawberry_django.filter_type(models.ModuleBay, lookups=True)
class ModuleBayFilter(ModularComponentModelFilterMixin): class ModuleBayFilter(ModularComponentModelFilterMixin):
parent: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( parent: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -619,17 +619,17 @@ class ModuleBayFilter(ModularComponentModelFilterMixin):
position: FilterLookup[str] | None = strawberry_django.filter_field() position: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ModuleBayTemplate, lookups=True) @strawberry_django.filter_type(models.ModuleBayTemplate, lookups=True)
class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin): class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin):
position: FilterLookup[str] | None = strawberry_django.filter_field() position: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ModuleTypeProfile, lookups=True) @strawberry_django.filter_type(models.ModuleTypeProfile, lookups=True)
class ModuleTypeProfileFilter(PrimaryModelFilterMixin): class ModuleTypeProfileFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ModuleType, lookups=True) @strawberry_django.filter_type(models.ModuleType, lookups=True)
class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin): class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -676,7 +676,7 @@ class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig
) = strawberry_django.filter_field() ) = strawberry_django.filter_field()
@strawberry_django.filter(models.Platform, lookups=True) @strawberry_django.filter_type(models.Platform, lookups=True)
class PlatformFilter(OrganizationalModelFilterMixin): class PlatformFilter(OrganizationalModelFilterMixin):
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -688,7 +688,7 @@ class PlatformFilter(OrganizationalModelFilterMixin):
config_template_id: ID | None = strawberry_django.filter_field() config_template_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter(models.PowerFeed, lookups=True) @strawberry_django.filter_type(models.PowerFeed, lookups=True)
class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
power_panel: Annotated['PowerPanelFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( power_panel: Annotated['PowerPanelFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -723,7 +723,7 @@ class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryM
) )
@strawberry_django.filter(models.PowerOutlet, lookups=True) @strawberry_django.filter_type(models.PowerOutlet, lookups=True)
class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin): class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( type: Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -738,7 +738,7 @@ class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilte
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.PowerOutletTemplate, lookups=True) @strawberry_django.filter_type(models.PowerOutletTemplate, lookups=True)
class PowerOutletTemplateFilter(ModularComponentModelFilterMixin): class PowerOutletTemplateFilter(ModularComponentModelFilterMixin):
type: Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( type: Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -752,7 +752,7 @@ class PowerOutletTemplateFilter(ModularComponentModelFilterMixin):
) )
@strawberry_django.filter(models.PowerPanel, lookups=True) @strawberry_django.filter_type(models.PowerPanel, lookups=True)
class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryModelFilterMixin): class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryModelFilterMixin):
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
site_id: ID | None = strawberry_django.filter_field() site_id: ID | None = strawberry_django.filter_field()
@ -765,7 +765,7 @@ class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryMo
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.PowerPort, lookups=True) @strawberry_django.filter_type(models.PowerPort, lookups=True)
class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin): class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( type: Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -778,7 +778,7 @@ class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterM
) )
@strawberry_django.filter(models.PowerPortTemplate, lookups=True) @strawberry_django.filter_type(models.PowerPortTemplate, lookups=True)
class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin): class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin):
type: Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( type: Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -791,7 +791,7 @@ class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin):
) )
@strawberry_django.filter(models.RackType, lookups=True) @strawberry_django.filter_type(models.RackType, lookups=True)
class RackTypeFilter(RackBaseFilterMixin): class RackTypeFilter(RackBaseFilterMixin):
form_factor: Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( form_factor: Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -804,7 +804,7 @@ class RackTypeFilter(RackBaseFilterMixin):
slug: FilterLookup[str] | None = strawberry_django.filter_field() slug: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.Rack, lookups=True) @strawberry_django.filter_type(models.Rack, lookups=True)
class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, RackBaseFilterMixin): class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, RackBaseFilterMixin):
form_factor: Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( form_factor: Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -836,7 +836,7 @@ class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi
) )
@strawberry_django.filter(models.RackReservation, lookups=True) @strawberry_django.filter_type(models.RackReservation, lookups=True)
class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin): class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
rack_id: ID | None = strawberry_django.filter_field() rack_id: ID | None = strawberry_django.filter_field()
@ -848,12 +848,12 @@ class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
description: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.RackRole, lookups=True) @strawberry_django.filter_type(models.RackRole, lookups=True)
class RackRoleFilter(OrganizationalModelFilterMixin): class RackRoleFilter(OrganizationalModelFilterMixin):
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.RearPort, lookups=True) @strawberry_django.filter_type(models.RearPort, lookups=True)
class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin): class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@ -862,7 +862,7 @@ class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMi
) )
@strawberry_django.filter(models.RearPortTemplate, lookups=True) @strawberry_django.filter_type(models.RearPortTemplate, lookups=True)
class RearPortTemplateFilter(ModularComponentTemplateFilterMixin): class RearPortTemplateFilter(ModularComponentTemplateFilterMixin):
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
@ -871,7 +871,7 @@ class RearPortTemplateFilter(ModularComponentTemplateFilterMixin):
) )
@strawberry_django.filter(models.Region, lookups=True) @strawberry_django.filter_type(models.Region, lookups=True)
class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin): class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -881,7 +881,7 @@ class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
) )
@strawberry_django.filter(models.Site, lookups=True) @strawberry_django.filter_type(models.Site, lookups=True)
class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field() slug: FilterLookup[str] | None = strawberry_django.filter_field()
@ -915,7 +915,7 @@ class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi
) )
@strawberry_django.filter(models.SiteGroup, lookups=True) @strawberry_django.filter_type(models.SiteGroup, lookups=True)
class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin): class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -925,7 +925,7 @@ class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
) )
@strawberry_django.filter(models.VirtualChassis, lookups=True) @strawberry_django.filter_type(models.VirtualChassis, lookups=True)
class VirtualChassisFilter(PrimaryModelFilterMixin): class VirtualChassisFilter(PrimaryModelFilterMixin):
master: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() master: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
master_id: ID | None = strawberry_django.filter_field() master_id: ID | None = strawberry_django.filter_field()
@ -937,7 +937,7 @@ class VirtualChassisFilter(PrimaryModelFilterMixin):
member_count: FilterLookup[int] | None = strawberry_django.filter_field() member_count: FilterLookup[int] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.VirtualDeviceContext, lookups=True) @strawberry_django.filter_type(models.VirtualDeviceContext, lookups=True)
class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilterMixin): class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
device_id: ID | None = strawberry_django.filter_field() device_id: ID | None = strawberry_django.filter_field()

View File

@ -541,10 +541,10 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Organi
class ManufacturerType(OrganizationalObjectType, ContactsMixin): class ManufacturerType(OrganizationalObjectType, ContactsMixin):
platforms: List[Annotated["PlatformType", strawberry.lazy('dcim.graphql.types')]] platforms: List[Annotated["PlatformType", strawberry.lazy('dcim.graphql.types')]]
device_types: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]] device_types: List[Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')]]
inventory_item_templates: List[Annotated["InventoryItemTemplateType", strawberry.lazy('dcim.graphql.types')]] inventory_item_templates: List[Annotated["InventoryItemTemplateType", strawberry.lazy('dcim.graphql.types')]]
inventory_items: List[Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')]] inventory_items: List[Annotated["InventoryItemType", strawberry.lazy('dcim.graphql.types')]]
module_types: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]] module_types: List[Annotated["ModuleTypeType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type( @strawberry_django.type(
@ -617,11 +617,11 @@ class ModuleTypeType(NetBoxObjectType):
frontporttemplates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]] frontporttemplates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
consoleserverporttemplates: List[Annotated["ConsoleServerPortTemplateType", strawberry.lazy('dcim.graphql.types')]] consoleserverporttemplates: List[Annotated["ConsoleServerPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
interfacetemplates: List[Annotated["InterfaceTemplateType", strawberry.lazy('dcim.graphql.types')]] interfacetemplates: List[Annotated["InterfaceTemplateType", strawberry.lazy('dcim.graphql.types')]]
powerporttemplates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]] powerporttemplates: List[Annotated["PowerPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
poweroutlettemplates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]] poweroutlettemplates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
rearporttemplates: List[Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]] rearporttemplates: List[Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
instances: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]] instances: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]]
consoleporttemplates: List[Annotated["ModuleType", strawberry.lazy('dcim.graphql.types')]] consoleporttemplates: List[Annotated["ConsolePortTemplateType", strawberry.lazy('dcim.graphql.types')]]
@strawberry_django.type( @strawberry_django.type(

View File

@ -26,49 +26,50 @@ def set_null_values(apps, schema_editor):
RackType = apps.get_model('dcim', 'RackType') RackType = apps.get_model('dcim', 'RackType')
RearPort = apps.get_model('dcim', 'RearPort') RearPort = apps.get_model('dcim', 'RearPort')
Site = apps.get_model('dcim', 'Site') Site = apps.get_model('dcim', 'Site')
db_alias = schema_editor.connection.alias
Cable.objects.filter(length_unit='').update(length_unit=None) Cable.objects.using(db_alias).filter(length_unit='').update(length_unit=None)
Cable.objects.filter(type='').update(type=None) Cable.objects.using(db_alias).filter(type='').update(type=None)
ConsolePort.objects.filter(cable_end='').update(cable_end=None) ConsolePort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
ConsolePort.objects.filter(type='').update(type=None) ConsolePort.objects.using(db_alias).filter(type='').update(type=None)
ConsolePortTemplate.objects.filter(type='').update(type=None) ConsolePortTemplate.objects.using(db_alias).filter(type='').update(type=None)
ConsoleServerPort.objects.filter(cable_end='').update(cable_end=None) ConsoleServerPort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
ConsoleServerPort.objects.filter(type='').update(type=None) ConsoleServerPort.objects.using(db_alias).filter(type='').update(type=None)
ConsoleServerPortTemplate.objects.filter(type='').update(type=None) ConsoleServerPortTemplate.objects.using(db_alias).filter(type='').update(type=None)
Device.objects.filter(airflow='').update(airflow=None) Device.objects.using(db_alias).filter(airflow='').update(airflow=None)
Device.objects.filter(face='').update(face=None) Device.objects.using(db_alias).filter(face='').update(face=None)
DeviceType.objects.filter(airflow='').update(airflow=None) DeviceType.objects.using(db_alias).filter(airflow='').update(airflow=None)
DeviceType.objects.filter(subdevice_role='').update(subdevice_role=None) DeviceType.objects.using(db_alias).filter(subdevice_role='').update(subdevice_role=None)
DeviceType.objects.filter(weight_unit='').update(weight_unit=None) DeviceType.objects.using(db_alias).filter(weight_unit='').update(weight_unit=None)
FrontPort.objects.filter(cable_end='').update(cable_end=None) FrontPort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
Interface.objects.filter(cable_end='').update(cable_end=None) Interface.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
Interface.objects.filter(mode='').update(mode=None) Interface.objects.using(db_alias).filter(mode='').update(mode=None)
Interface.objects.filter(poe_mode='').update(poe_mode=None) Interface.objects.using(db_alias).filter(poe_mode='').update(poe_mode=None)
Interface.objects.filter(poe_type='').update(poe_type=None) Interface.objects.using(db_alias).filter(poe_type='').update(poe_type=None)
Interface.objects.filter(rf_channel='').update(rf_channel=None) Interface.objects.using(db_alias).filter(rf_channel='').update(rf_channel=None)
Interface.objects.filter(rf_role='').update(rf_role=None) Interface.objects.using(db_alias).filter(rf_role='').update(rf_role=None)
InterfaceTemplate.objects.filter(poe_mode='').update(poe_mode=None) InterfaceTemplate.objects.using(db_alias).filter(poe_mode='').update(poe_mode=None)
InterfaceTemplate.objects.filter(poe_type='').update(poe_type=None) InterfaceTemplate.objects.using(db_alias).filter(poe_type='').update(poe_type=None)
InterfaceTemplate.objects.filter(rf_role='').update(rf_role=None) InterfaceTemplate.objects.using(db_alias).filter(rf_role='').update(rf_role=None)
ModuleType.objects.filter(airflow='').update(airflow=None) ModuleType.objects.using(db_alias).filter(airflow='').update(airflow=None)
ModuleType.objects.filter(weight_unit='').update(weight_unit=None) ModuleType.objects.using(db_alias).filter(weight_unit='').update(weight_unit=None)
PowerFeed.objects.filter(cable_end='').update(cable_end=None) PowerFeed.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
PowerOutlet.objects.filter(cable_end='').update(cable_end=None) PowerOutlet.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
PowerOutlet.objects.filter(feed_leg='').update(feed_leg=None) PowerOutlet.objects.using(db_alias).filter(feed_leg='').update(feed_leg=None)
PowerOutlet.objects.filter(type='').update(type=None) PowerOutlet.objects.using(db_alias).filter(type='').update(type=None)
PowerOutletTemplate.objects.filter(feed_leg='').update(feed_leg=None) PowerOutletTemplate.objects.using(db_alias).filter(feed_leg='').update(feed_leg=None)
PowerOutletTemplate.objects.filter(type='').update(type=None) PowerOutletTemplate.objects.using(db_alias).filter(type='').update(type=None)
PowerPort.objects.filter(cable_end='').update(cable_end=None) PowerPort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
PowerPort.objects.filter(type='').update(type=None) PowerPort.objects.using(db_alias).filter(type='').update(type=None)
PowerPortTemplate.objects.filter(type='').update(type=None) PowerPortTemplate.objects.using(db_alias).filter(type='').update(type=None)
Rack.objects.filter(airflow='').update(airflow=None) Rack.objects.using(db_alias).filter(airflow='').update(airflow=None)
Rack.objects.filter(form_factor='').update(form_factor=None) Rack.objects.using(db_alias).filter(form_factor='').update(form_factor=None)
Rack.objects.filter(outer_unit='').update(outer_unit=None) Rack.objects.using(db_alias).filter(outer_unit='').update(outer_unit=None)
Rack.objects.filter(weight_unit='').update(weight_unit=None) Rack.objects.using(db_alias).filter(weight_unit='').update(weight_unit=None)
RackType.objects.filter(outer_unit='').update(outer_unit=None) RackType.objects.using(db_alias).filter(outer_unit='').update(outer_unit=None)
RackType.objects.filter(weight_unit='').update(weight_unit=None) RackType.objects.using(db_alias).filter(weight_unit='').update(weight_unit=None)
RearPort.objects.filter(cable_end='').update(cable_end=None) RearPort.objects.using(db_alias).filter(cable_end='').update(cable_end=None)
Site.objects.filter(time_zone='').update(time_zone=None) Site.objects.using(db_alias).filter(time_zone='').update(time_zone=None)
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -6,19 +6,26 @@ def populate_mac_addresses(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType') ContentType = apps.get_model('contenttypes', 'ContentType')
Interface = apps.get_model('dcim', 'Interface') Interface = apps.get_model('dcim', 'Interface')
MACAddress = apps.get_model('dcim', 'MACAddress') MACAddress = apps.get_model('dcim', 'MACAddress')
db_alias = schema_editor.connection.alias
interface_ct = ContentType.objects.get_for_model(Interface) interface_ct = ContentType.objects.get_for_model(Interface)
mac_addresses = [ mac_addresses = [
MACAddress( MACAddress(
mac_address=interface.mac_address, assigned_object_type=interface_ct, assigned_object_id=interface.pk mac_address=interface.mac_address,
assigned_object_type=interface_ct,
assigned_object_id=interface.pk
) )
for interface in Interface.objects.filter(mac_address__isnull=False) for interface in Interface.objects.filter(mac_address__isnull=False)
] ]
MACAddress.objects.bulk_create(mac_addresses, batch_size=100) MACAddress.objects.using(db_alias).bulk_create(mac_addresses, batch_size=100)
# TODO: Optimize interface updates # TODO: Optimize interface updates
for mac_address in mac_addresses: for mac_address in mac_addresses:
Interface.objects.filter(pk=mac_address.assigned_object_id).update(primary_mac_address=mac_address) Interface.objects.using(db_alias).filter(
pk=mac_address.assigned_object_id
).update(
primary_mac_address=mac_address
)
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -11,6 +11,8 @@ def load_initial_data(apps, schema_editor):
Load initial ModuleTypeProfile objects from file. Load initial ModuleTypeProfile objects from file.
""" """
ModuleTypeProfile = apps.get_model('dcim', 'ModuleTypeProfile') ModuleTypeProfile = apps.get_model('dcim', 'ModuleTypeProfile')
db_alias = schema_editor.connection.alias
initial_profiles = ( initial_profiles = (
'cpu', 'cpu',
'fan', 'fan',
@ -25,7 +27,7 @@ def load_initial_data(apps, schema_editor):
with file_path.open('r') as f: with file_path.open('r') as f:
data = json.load(f) data = json.load(f)
try: try:
ModuleTypeProfile.objects.create(**data) ModuleTypeProfile.objects.using(db_alias).create(**data)
except Exception as e: except Exception as e:
print(f"Error loading data from {file_path}") print(f"Error loading data from {file_path}")
raise e raise e

View File

@ -329,11 +329,9 @@ class CableTraceSVG:
# Draw attachment (line) # Draw attachment (line)
start = (OFFSET + self.center, OFFSET + self.cursor) start = (OFFSET + self.center, OFFSET + self.cursor)
height = PADDING * 2 + LINE_HEIGHT + PADDING * 2 end = (start[0], start[1] + CABLE_HEIGHT)
end = (start[0], start[1] + height)
line = Line(start=start, end=end, class_='attachment') line = Line(start=start, end=end, class_='attachment')
group.add(line) group.add(line)
self.cursor += PADDING * 4
return group return group
@ -358,10 +356,10 @@ class CableTraceSVG:
# Else: No need to draw parent objects (parent objects are drawn in last "round" as the far-end!) # Else: No need to draw parent objects (parent objects are drawn in last "round" as the far-end!)
near_terminations = self.draw_terminations(near_ends, parent_object_nodes) near_terminations = self.draw_terminations(near_ends, parent_object_nodes)
self.cursor += CABLE_HEIGHT
# Connector (a Cable or WirelessLink) # Connector (a Cable or WirelessLink)
if links and far_ends: if links and far_ends:
self.cursor += CABLE_HEIGHT
obj_list = {end.parent_object for end in far_ends} obj_list = {end.parent_object for end in far_ends}
parent_object_nodes, far_terminations = self.draw_far_objects(obj_list, far_ends) parent_object_nodes, far_terminations = self.draw_far_objects(obj_list, far_ends)
@ -449,6 +447,7 @@ class CableTraceSVG:
# Attachment # Attachment
attachment = self.draw_attachment() attachment = self.draw_attachment()
self.connectors.append(attachment) self.connectors.append(attachment)
self.cursor += CABLE_HEIGHT
# Object # Object
parent_object_nodes = self.draw_parent_objects(far_ends) parent_object_nodes = self.draw_parent_objects(far_ends)

View File

@ -1091,10 +1091,9 @@ class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable):
verbose_name=_('Name'), verbose_name=_('Name'),
linkify=True linkify=True
) )
device = tables.TemplateColumn( device = tables.Column(
verbose_name=_('Device'), verbose_name=_('Device'),
order_by=('device___name',), order_by=('device___name',),
template_code=DEVICE_LINK,
linkify=True linkify=True
) )
status = columns.ChoiceFieldColumn( status = columns.ChoiceFieldColumn(

View File

@ -14,7 +14,7 @@ from ipam.models import ASN, RIR, VLAN, VRF
from netbox.api.serializers import GenericObjectSerializer from netbox.api.serializers import GenericObjectSerializer
from tenancy.models import Tenant from tenancy.models import Tenant
from users.models import User from users.models import User
from utilities.testing import APITestCase, APIViewTestCases, create_test_device from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_logging
from virtualization.models import Cluster, ClusterType from virtualization.models import Cluster, ClusterType
from wireless.choices import WirelessChannelChoices from wireless.choices import WirelessChannelChoices
from wireless.models import WirelessLAN from wireless.models import WirelessLAN
@ -1858,7 +1858,8 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
# Attempt to delete only the parent interface # Attempt to delete only the parent interface
url = self._get_detail_url(interface1) url = self._get_detail_url(interface1)
self.client.delete(url, **self.header) with disable_logging():
self.client.delete(url, **self.header)
self.assertEqual(device.interfaces.count(), 4) # Parent was not deleted self.assertEqual(device.interfaces.count(), 4) # Parent was not deleted
# Attempt to bulk delete parent & child together # Attempt to bulk delete parent & child together

View File

@ -2793,6 +2793,7 @@ class InterfaceView(generic.ObjectView):
), ),
orderable=False orderable=False
) )
vdc_table.configure(request)
# Get bridge interfaces # Get bridge interfaces
bridge_interfaces = Interface.objects.restrict(request.user, 'view').filter(bridge=instance) bridge_interfaces = Interface.objects.restrict(request.user, 'view').filter(bridge=instance)
@ -2801,6 +2802,7 @@ class InterfaceView(generic.ObjectView):
exclude=('device', 'parent'), exclude=('device', 'parent'),
orderable=False orderable=False
) )
bridge_interfaces_table.configure(request)
# Get child interfaces # Get child interfaces
child_interfaces = Interface.objects.restrict(request.user, 'view').filter(parent=instance) child_interfaces = Interface.objects.restrict(request.user, 'view').filter(parent=instance)
@ -2809,6 +2811,7 @@ class InterfaceView(generic.ObjectView):
exclude=('device', 'parent'), exclude=('device', 'parent'),
orderable=False orderable=False
) )
child_interfaces_table.configure(request)
# Get assigned VLANs and annotate whether each is tagged or untagged # Get assigned VLANs and annotate whether each is tagged or untagged
vlans = [] vlans = []
@ -2823,6 +2826,7 @@ class InterfaceView(generic.ObjectView):
data=vlans, data=vlans,
orderable=False orderable=False
) )
vlan_table.configure(request)
# Get VLAN translation rules # Get VLAN translation rules
vlan_translation_table = None vlan_translation_table = None
@ -2831,6 +2835,7 @@ class InterfaceView(generic.ObjectView):
data=instance.vlan_translation_policy.rules.all(), data=instance.vlan_translation_policy.rules.all(),
orderable=False orderable=False
) )
vlan_translation_table.configure(request)
return { return {
'vdc_table': vdc_table, 'vdc_table': vdc_table,

View File

@ -40,7 +40,7 @@ __all__ = (
) )
@strawberry_django.filter(models.ConfigContext, lookups=True) @strawberry_django.filter_type(models.ConfigContext, lookups=True)
class ConfigContextFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin): class ConfigContextFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin):
name: FilterLookup[str] = strawberry_django.filter_field() name: FilterLookup[str] = strawberry_django.filter_field()
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
@ -97,7 +97,7 @@ class ConfigContextFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Chan
) )
@strawberry_django.filter(models.ConfigTemplate, lookups=True) @strawberry_django.filter_type(models.ConfigTemplate, lookups=True)
class ConfigTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin): class ConfigTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field()
@ -111,7 +111,7 @@ class ConfigTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Cha
as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field() as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.CustomField, lookups=True) @strawberry_django.filter_type(models.CustomField, lookups=True)
class CustomFieldFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): class CustomFieldFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
type: Annotated['CustomFieldTypeEnum', strawberry.lazy('extras.graphql.enums')] | None = ( type: Annotated['CustomFieldTypeEnum', strawberry.lazy('extras.graphql.enums')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -164,7 +164,7 @@ class CustomFieldFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
comments: FilterLookup[str] | None = strawberry_django.filter_field() comments: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.CustomFieldChoiceSet, lookups=True) @strawberry_django.filter_type(models.CustomFieldChoiceSet, lookups=True)
class CustomFieldChoiceSetFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): class CustomFieldChoiceSetFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field()
@ -177,7 +177,7 @@ class CustomFieldChoiceSetFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin
order_alphabetically: FilterLookup[bool] | None = strawberry_django.filter_field() order_alphabetically: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.CustomLink, lookups=True) @strawberry_django.filter_type(models.CustomLink, lookups=True)
class CustomLinkFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): class CustomLinkFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
enabled: FilterLookup[bool] | None = strawberry_django.filter_field() enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
@ -193,7 +193,7 @@ class CustomLinkFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
new_window: FilterLookup[bool] | None = strawberry_django.filter_field() new_window: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ExportTemplate, lookups=True) @strawberry_django.filter_type(models.ExportTemplate, lookups=True)
class ExportTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin): class ExportTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field()
@ -207,7 +207,7 @@ class ExportTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Cha
as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field() as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ImageAttachment, lookups=True) @strawberry_django.filter_type(models.ImageAttachment, lookups=True)
class ImageAttachmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): class ImageAttachmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -222,7 +222,7 @@ class ImageAttachmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.JournalEntry, lookups=True) @strawberry_django.filter_type(models.JournalEntry, lookups=True)
class JournalEntryFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin): class JournalEntryFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -238,7 +238,7 @@ class JournalEntryFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, Tag
comments: FilterLookup[str] | None = strawberry_django.filter_field() comments: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.NotificationGroup, lookups=True) @strawberry_django.filter_type(models.NotificationGroup, lookups=True)
class NotificationGroupFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): class NotificationGroupFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field()
@ -246,7 +246,7 @@ class NotificationGroupFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
users: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() users: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.SavedFilter, lookups=True) @strawberry_django.filter_type(models.SavedFilter, lookups=True)
class SavedFilterFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): class SavedFilterFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field() slug: FilterLookup[str] | None = strawberry_django.filter_field()
@ -263,7 +263,7 @@ class SavedFilterFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
) )
@strawberry_django.filter(models.TableConfig, lookups=True) @strawberry_django.filter_type(models.TableConfig, lookups=True)
class TableConfigFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): class TableConfigFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field()
@ -276,13 +276,13 @@ class TableConfigFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
shared: FilterLookup[bool] | None = strawberry_django.filter_field() shared: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.Tag, lookups=True) @strawberry_django.filter_type(models.Tag, lookups=True)
class TagFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin, TagBaseFilterMixin): class TagFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin, TagBaseFilterMixin):
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.Webhook, lookups=True) @strawberry_django.filter_type(models.Webhook, lookups=True)
class WebhookFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin): class WebhookFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field()
@ -301,7 +301,7 @@ class WebhookFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilt
) )
@strawberry_django.filter(models.EventRule, lookups=True) @strawberry_django.filter_type(models.EventRule, lookups=True)
class EventRuleFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin): class EventRuleFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
description: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field()

View File

@ -4,11 +4,12 @@ from django.db import migrations
def convert_reportmodule_jobs(apps, schema_editor): def convert_reportmodule_jobs(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType') ContentType = apps.get_model('contenttypes', 'ContentType')
Job = apps.get_model('core', 'Job') Job = apps.get_model('core', 'Job')
db_alias = schema_editor.connection.alias
# Convert all ReportModule jobs to ScriptModule jobs # Convert all ReportModule jobs to ScriptModule jobs
if reportmodule_ct := ContentType.objects.filter(app_label='extras', model='reportmodule').first(): if reportmodule_ct := ContentType.objects.using(db_alias).filter(app_label='extras', model='reportmodule').first():
scriptmodule_ct = ContentType.objects.get(app_label='extras', model='scriptmodule') scriptmodule_ct = ContentType.objects.using(db_alias).get(app_label='extras', model='scriptmodule')
Job.objects.filter(object_type_id=reportmodule_ct.id).update(object_type_id=scriptmodule_ct.id) Job.objects.using(db_alias).filter(object_type_id=reportmodule_ct.id).update(object_type_id=scriptmodule_ct.id)
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -88,24 +88,33 @@ def update_scripts(apps, schema_editor):
ScriptModule = apps.get_model('extras', 'ScriptModule') ScriptModule = apps.get_model('extras', 'ScriptModule')
ReportModule = apps.get_model('extras', 'ReportModule') ReportModule = apps.get_model('extras', 'ReportModule')
Job = apps.get_model('core', 'Job') Job = apps.get_model('core', 'Job')
db_alias = schema_editor.connection.alias
script_ct = ContentType.objects.get_for_model(Script, for_concrete_model=False) script_ct = ContentType.objects.get_for_model(Script, for_concrete_model=False)
scriptmodule_ct = ContentType.objects.get_for_model(ScriptModule, for_concrete_model=False) scriptmodule_ct = ContentType.objects.get_for_model(ScriptModule, for_concrete_model=False)
reportmodule_ct = ContentType.objects.get_for_model(ReportModule, for_concrete_model=False) reportmodule_ct = ContentType.objects.get_for_model(ReportModule, for_concrete_model=False)
for module in ScriptModule.objects.all(): for module in ScriptModule.objects.using(db_alias).all():
for script_name in get_module_scripts(module): for script_name in get_module_scripts(module):
script = Script.objects.create( script = Script.objects.using(db_alias).create(
name=script_name, name=script_name,
module=module, module=module,
) )
# Update all Jobs associated with this ScriptModule & script name to point to the new Script object # Update all Jobs associated with this ScriptModule & script name to point to the new Script object
Job.objects.filter(object_type_id=scriptmodule_ct.id, object_id=module.pk, name=script_name).update( Job.objects.using(db_alias).filter(
object_type_id=scriptmodule_ct.id,
object_id=module.pk,
name=script_name
).update(
object_type_id=script_ct.id, object_id=script.pk object_type_id=script_ct.id, object_id=script.pk
) )
# Update all Jobs associated with this ScriptModule & script name to point to the new Script object # Update all Jobs associated with this ScriptModule & script name to point to the new Script object
Job.objects.filter(object_type_id=reportmodule_ct.id, object_id=module.pk, name=script_name).update( Job.objects.using(db_alias).filter(
object_type_id=reportmodule_ct.id,
object_id=module.pk,
name=script_name
).update(
object_type_id=script_ct.id, object_id=script.pk object_type_id=script_ct.id, object_id=script.pk
) )
@ -119,16 +128,22 @@ def update_event_rules(apps, schema_editor):
Script = apps.get_model('extras', 'Script') Script = apps.get_model('extras', 'Script')
ScriptModule = apps.get_model('extras', 'ScriptModule') ScriptModule = apps.get_model('extras', 'ScriptModule')
EventRule = apps.get_model('extras', 'EventRule') EventRule = apps.get_model('extras', 'EventRule')
db_alias = schema_editor.connection.alias
script_ct = ContentType.objects.get_for_model(Script) script_ct = ContentType.objects.get_for_model(Script)
scriptmodule_ct = ContentType.objects.get_for_model(ScriptModule) scriptmodule_ct = ContentType.objects.get_for_model(ScriptModule)
for eventrule in EventRule.objects.filter(action_object_type=scriptmodule_ct): for eventrule in EventRule.objects.using(db_alias).filter(action_object_type=scriptmodule_ct):
name = eventrule.action_parameters.get('script_name') name = eventrule.action_parameters.get('script_name')
obj, __ = Script.objects.get_or_create( obj, __ = Script.objects.using(db_alias).get_or_create(
module_id=eventrule.action_object_id, name=name, defaults={'is_executable': False} module_id=eventrule.action_object_id,
name=name,
defaults={'is_executable': False}
)
EventRule.objects.using(db_alias).filter(pk=eventrule.pk).update(
action_object_type=script_ct,
action_object_id=obj.id
) )
EventRule.objects.filter(pk=eventrule.pk).update(action_object_type=script_ct, action_object_id=obj.id)
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -1,12 +1,11 @@
# Generated by Django 5.0.4 on 2024-04-24 20:09
from django.db import migrations from django.db import migrations
def update_dashboard_widgets(apps, schema_editor): def update_dashboard_widgets(apps, schema_editor):
Dashboard = apps.get_model('extras', 'Dashboard') Dashboard = apps.get_model('extras', 'Dashboard')
db_alias = schema_editor.connection.alias
for dashboard in Dashboard.objects.all(): for dashboard in Dashboard.objects.using(db_alias).all():
for key, widget in dashboard.config.items(): for key, widget in dashboard.config.items():
if models := widget['config'].get('models'): if models := widget['config'].get('models'):
models = list(map(lambda x: x.replace('users.netboxgroup', 'users.group'), models)) models = list(map(lambda x: x.replace('users.netboxgroup', 'users.group'), models))

View File

@ -3,7 +3,9 @@ from django.db import migrations, models
def update_link_buttons(apps, schema_editor): def update_link_buttons(apps, schema_editor):
CustomLink = apps.get_model('extras', 'CustomLink') CustomLink = apps.get_model('extras', 'CustomLink')
CustomLink.objects.filter(button_class='outline-dark').update(button_class='default') db_alias = schema_editor.connection.alias
CustomLink.objects.using(db_alias).filter(button_class='outline-dark').update(button_class='default')
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -3,19 +3,21 @@ from django.db import migrations
def update_content_types(apps, schema_editor): def update_content_types(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType') ContentType = apps.get_model('contenttypes', 'ContentType')
db_alias = schema_editor.connection.alias
# Delete the new ContentTypes effected by the new model in the core app # Delete the new ContentTypes effected by the new model in the core app
ContentType.objects.filter(app_label='core', model='objectchange').delete() ContentType.objects.using(db_alias).filter(app_label='core', model='objectchange').delete()
# Update the app labels of the original ContentTypes for extras.ObjectChange to ensure that any # Update the app labels of the original ContentTypes for extras.ObjectChange to ensure that any
# foreign key references are preserved # foreign key references are preserved
ContentType.objects.filter(app_label='extras', model='objectchange').update(app_label='core') ContentType.objects.using(db_alias).filter(app_label='extras', model='objectchange').update(app_label='core')
def update_dashboard_widgets(apps, schema_editor): def update_dashboard_widgets(apps, schema_editor):
Dashboard = apps.get_model('extras', 'Dashboard') Dashboard = apps.get_model('extras', 'Dashboard')
db_alias = schema_editor.connection.alias
for dashboard in Dashboard.objects.all(): for dashboard in Dashboard.objects.using(db_alias).all():
for key, widget in dashboard.config.items(): for key, widget in dashboard.config.items():
if widget['config'].get('model') == 'extras.objectchange': if widget['config'].get('model') == 'extras.objectchange':
widget['config']['model'] = 'core.objectchange' widget['config']['model'] = 'core.objectchange'

View File

@ -6,8 +6,9 @@ from core.events import *
def set_event_types(apps, schema_editor): def set_event_types(apps, schema_editor):
EventRule = apps.get_model('extras', 'EventRule') EventRule = apps.get_model('extras', 'EventRule')
event_rules = EventRule.objects.all() db_alias = schema_editor.connection.alias
event_rules = EventRule.objects.using(db_alias).all()
for event_rule in event_rules: for event_rule in event_rules:
event_rule.event_types = [] event_rule.event_types = []
if event_rule.type_create: if event_rule.type_create:

View File

@ -6,8 +6,9 @@ def set_null_values(apps, schema_editor):
Replace empty strings with null values. Replace empty strings with null values.
""" """
CustomFieldChoiceSet = apps.get_model('extras', 'CustomFieldChoiceSet') CustomFieldChoiceSet = apps.get_model('extras', 'CustomFieldChoiceSet')
db_alias = schema_editor.connection.alias
CustomFieldChoiceSet.objects.filter(base_choices='').update(base_choices=None) CustomFieldChoiceSet.objects.using(db_alias).filter(base_choices='').update(base_choices=None)
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -8,7 +8,9 @@ def set_kind_default(apps, schema_editor):
Set kind to "info" on any entries with no kind assigned. Set kind to "info" on any entries with no kind assigned.
""" """
JournalEntry = apps.get_model('extras', 'JournalEntry') JournalEntry = apps.get_model('extras', 'JournalEntry')
JournalEntry.objects.filter(kind='').update(kind=JournalEntryKindChoices.KIND_INFO) db_alias = schema_editor.connection.alias
JournalEntry.objects.using(db_alias).filter(kind='').update(kind=JournalEntryKindChoices.KIND_INFO)
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -24,6 +24,17 @@ class JournalEntryIndex(SearchIndex):
display_attrs = ('kind', 'created_by') display_attrs = ('kind', 'created_by')
@register_search
class TagIndex(SearchIndex):
model = models.Tag
fields = (
('name', 100),
('slug', 110),
('description', 500),
)
display_attrs = ('description',)
@register_search @register_search
class WebhookEntryIndex(SearchIndex): class WebhookEntryIndex(SearchIndex):
model = models.Webhook model = models.Webhook

View File

@ -2,7 +2,7 @@ import datetime
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import make_aware from django.utils.timezone import make_aware, now
from rest_framework import status from rest_framework import status
from core.choices import ManagedFileRootPathChoices from core.choices import ManagedFileRootPathChoices
@ -991,6 +991,10 @@ class SubscriptionTest(APIViewTestCases.APIViewTestCase):
}, },
] ]
cls.bulk_update_data = {
'user': users[3].pk,
}
class NotificationGroupTest(APIViewTestCases.APIViewTestCase): class NotificationGroupTest(APIViewTestCases.APIViewTestCase):
model = NotificationGroup model = NotificationGroup
@ -1072,6 +1076,9 @@ class NotificationGroupTest(APIViewTestCases.APIViewTestCase):
class NotificationTest(APIViewTestCases.APIViewTestCase): class NotificationTest(APIViewTestCases.APIViewTestCase):
model = Notification model = Notification
brief_fields = ['display', 'event_type', 'id', 'object_id', 'object_type', 'read', 'url', 'user'] brief_fields = ['display', 'event_type', 'id', 'object_id', 'object_type', 'read', 'url', 'user']
bulk_update_data = {
'read': now(),
}
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):

View File

@ -1,3 +1,4 @@
import logging
import tempfile import tempfile
from datetime import date, datetime, timezone from datetime import date, datetime, timezone
@ -7,6 +8,7 @@ from netaddr import IPAddress, IPNetwork
from dcim.models import DeviceRole from dcim.models import DeviceRole
from extras.scripts import * from extras.scripts import *
from utilities.testing import disable_logging
CHOICES = ( CHOICES = (
('ff0000', 'Red'), ('ff0000', 'Red'),
@ -39,7 +41,8 @@ class ScriptTest(TestCase):
datafile.write(bytes(YAML_DATA, 'UTF-8')) datafile.write(bytes(YAML_DATA, 'UTF-8'))
datafile.seek(0) datafile.seek(0)
data = Script().load_yaml(datafile.name) with disable_logging(level=logging.WARNING):
data = Script().load_yaml(datafile.name)
self.assertEqual(data, { self.assertEqual(data, {
'Foo': 123, 'Foo': 123,
'Bar': 456, 'Bar': 456,
@ -51,7 +54,8 @@ class ScriptTest(TestCase):
datafile.write(bytes(JSON_DATA, 'UTF-8')) datafile.write(bytes(JSON_DATA, 'UTF-8'))
datafile.seek(0) datafile.seek(0)
data = Script().load_json(datafile.name) with disable_logging(level=logging.WARNING):
data = Script().load_json(datafile.name)
self.assertEqual(data, { self.assertEqual(data, {
'Foo': 123, 'Foo': 123,
'Bar': 456, 'Bar': 456,

View File

@ -147,8 +147,7 @@ class IPRangeSerializer(NetBoxModelSerializer):
fields = [ fields = [
'id', 'url', 'display_url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant', 'id', 'url', 'display_url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant',
'status', 'role', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'status', 'role', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'mark_populated', 'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'mark_populated', 'mark_utilized',
'last_updated',
] ]
brief_fields = ('id', 'url', 'display', 'family', 'start_address', 'end_address', 'description') brief_fields = ('id', 'url', 'display', 'family', 'start_address', 'end_address', 'description')

View File

@ -66,7 +66,7 @@ class VLANSerializer(NetBoxModelSerializer):
tenant = TenantSerializer(nested=True, required=False, allow_null=True) tenant = TenantSerializer(nested=True, required=False, allow_null=True)
status = ChoiceField(choices=VLANStatusChoices, required=False) status = ChoiceField(choices=VLANStatusChoices, required=False)
role = RoleSerializer(nested=True, required=False, allow_null=True) role = RoleSerializer(nested=True, required=False, allow_null=True)
qinq_role = ChoiceField(choices=VLANQinQRoleChoices, required=False) qinq_role = ChoiceField(choices=VLANQinQRoleChoices, required=False, allow_null=True)
qinq_svlan = NestedVLANSerializer(required=False, allow_null=True, default=None) qinq_svlan = NestedVLANSerializer(required=False, allow_null=True, default=None)
l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True) l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True)

View File

@ -1,5 +1,6 @@
from copy import deepcopy from copy import deepcopy
from django.contrib.contenttypes.prefetch import GenericPrefetch
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import transaction from django.db import transaction
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
@ -13,6 +14,7 @@ from rest_framework.response import Response
from rest_framework.routers import APIRootView from rest_framework.routers import APIRootView
from rest_framework.views import APIView from rest_framework.views import APIView
from dcim.models import Interface
from ipam import filtersets from ipam import filtersets
from ipam.models import * from ipam.models import *
from ipam.utils import get_next_available_prefix from ipam.utils import get_next_available_prefix
@ -21,6 +23,7 @@ from netbox.api.viewsets.mixins import ObjectValidationMixin
from netbox.config import get_config from netbox.config import get_config
from netbox.constants import ADVISORY_LOCK_KEYS from netbox.constants import ADVISORY_LOCK_KEYS
from utilities.api import get_serializer_for_model from utilities.api import get_serializer_for_model
from virtualization.models import VMInterface
from . import serializers from . import serializers
@ -79,7 +82,7 @@ class RoleViewSet(NetBoxModelViewSet):
class PrefixViewSet(NetBoxModelViewSet): class PrefixViewSet(NetBoxModelViewSet):
queryset = Prefix.objects.all() queryset = Prefix.objects.prefetch_related("scope")
serializer_class = serializers.PrefixSerializer serializer_class = serializers.PrefixSerializer
filterset_class = filtersets.PrefixFilterSet filterset_class = filtersets.PrefixFilterSet
@ -100,7 +103,17 @@ class IPRangeViewSet(NetBoxModelViewSet):
class IPAddressViewSet(NetBoxModelViewSet): class IPAddressViewSet(NetBoxModelViewSet):
queryset = IPAddress.objects.all() queryset = IPAddress.objects.prefetch_related(
GenericPrefetch(
"assigned_object",
[
# serializers are taken according to IPADDRESS_ASSIGNMENT_MODELS
FHRPGroup.objects.all(),
Interface.objects.select_related("cable", "device"),
VMInterface.objects.select_related("virtual_machine"),
],
),
)
serializer_class = serializers.IPAddressSerializer serializer_class = serializers.IPAddressSerializer
filterset_class = filtersets.IPAddressFilterSet filterset_class = filtersets.IPAddressFilterSet

View File

@ -46,7 +46,7 @@ __all__ = (
) )
@strawberry_django.filter(models.ASN, lookups=True) @strawberry_django.filter_type(models.ASN, lookups=True)
class ASNFilter(TenancyFilterMixin, PrimaryModelFilterMixin): class ASNFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
rir_id: ID | None = strawberry_django.filter_field() rir_id: ID | None = strawberry_django.filter_field()
@ -61,7 +61,7 @@ class ASNFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
) = strawberry_django.filter_field() ) = strawberry_django.filter_field()
@strawberry_django.filter(models.ASNRange, lookups=True) @strawberry_django.filter_type(models.ASNRange, lookups=True)
class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilterMixin): class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field() slug: FilterLookup[str] | None = strawberry_django.filter_field()
@ -75,7 +75,7 @@ class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
) )
@strawberry_django.filter(models.Aggregate, lookups=True) @strawberry_django.filter_type(models.Aggregate, lookups=True)
class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
prefix: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() prefix: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
prefix_id: ID | None = strawberry_django.filter_field() prefix_id: ID | None = strawberry_django.filter_field()
@ -84,7 +84,7 @@ class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
date_added: DateFilterLookup[date] | None = strawberry_django.filter_field() date_added: DateFilterLookup[date] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.FHRPGroup, lookups=True) @strawberry_django.filter_type(models.FHRPGroup, lookups=True)
class FHRPGroupFilter(PrimaryModelFilterMixin): class FHRPGroupFilter(PrimaryModelFilterMixin):
group_id: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( group_id: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -102,7 +102,7 @@ class FHRPGroupFilter(PrimaryModelFilterMixin):
) )
@strawberry_django.filter(models.FHRPGroupAssignment, lookups=True) @strawberry_django.filter_type(models.FHRPGroupAssignment, lookups=True)
class FHRPGroupAssignmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): class FHRPGroupAssignmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
interface_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( interface_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -117,7 +117,7 @@ class FHRPGroupAssignmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin)
) )
@strawberry_django.filter(models.IPAddress, lookups=True) @strawberry_django.filter_type(models.IPAddress, lookups=True)
class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
address: FilterLookup[str] | None = strawberry_django.filter_field() address: FilterLookup[str] | None = strawberry_django.filter_field()
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
@ -142,6 +142,10 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
nat_outside_id: ID | None = strawberry_django.filter_field() nat_outside_id: ID | None = strawberry_django.filter_field()
dns_name: FilterLookup[str] | None = strawberry_django.filter_field() dns_name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter_field()
def assigned(self, value: bool, prefix) -> Q:
return Q(assigned_object_id__isnull=(not value))
@strawberry_django.filter_field() @strawberry_django.filter_field()
def parent(self, value: list[str], prefix) -> Q: def parent(self, value: list[str], prefix) -> Q:
if not value: if not value:
@ -156,7 +160,7 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
return q return q
@strawberry_django.filter(models.IPRange, lookups=True) @strawberry_django.filter_type(models.IPRange, lookups=True)
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
start_address: FilterLookup[str] | None = strawberry_django.filter_field() start_address: FilterLookup[str] | None = strawberry_django.filter_field()
end_address: FilterLookup[str] | None = strawberry_django.filter_field() end_address: FilterLookup[str] | None = strawberry_django.filter_field()
@ -168,9 +172,7 @@ class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMi
status: Annotated['IPRangeStatusEnum', strawberry.lazy('ipam.graphql.enums')] | None = ( status: Annotated['IPRangeStatusEnum', strawberry.lazy('ipam.graphql.enums')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
role: Annotated['IPAddressRoleEnum', strawberry.lazy('ipam.graphql.enums')] | None = ( role: Annotated['RoleFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
strawberry_django.filter_field()
)
mark_utilized: FilterLookup[bool] | None = strawberry_django.filter_field() mark_utilized: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter_field() @strawberry_django.filter_field()
@ -187,7 +189,7 @@ class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMi
return q return q
@strawberry_django.filter(models.Prefix, lookups=True) @strawberry_django.filter_type(models.Prefix, lookups=True)
class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
prefix: FilterLookup[str] | None = strawberry_django.filter_field() prefix: FilterLookup[str] | None = strawberry_django.filter_field()
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
@ -203,19 +205,19 @@ class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, Pr
mark_utilized: FilterLookup[bool] | None = strawberry_django.filter_field() mark_utilized: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.RIR, lookups=True) @strawberry_django.filter_type(models.RIR, lookups=True)
class RIRFilter(OrganizationalModelFilterMixin): class RIRFilter(OrganizationalModelFilterMixin):
is_private: FilterLookup[bool] | None = strawberry_django.filter_field() is_private: FilterLookup[bool] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.Role, lookups=True) @strawberry_django.filter_type(models.Role, lookups=True)
class RoleFilter(OrganizationalModelFilterMixin): class RoleFilter(OrganizationalModelFilterMixin):
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@strawberry_django.filter(models.RouteTarget, lookups=True) @strawberry_django.filter_type(models.RouteTarget, lookups=True)
class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilterMixin): class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
importing_vrfs: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( importing_vrfs: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
@ -232,7 +234,7 @@ class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
) )
@strawberry_django.filter(models.Service, lookups=True) @strawberry_django.filter_type(models.Service, lookups=True)
class ServiceFilter(ContactFilterMixin, ServiceBaseFilterMixin, PrimaryModelFilterMixin): class ServiceFilter(ContactFilterMixin, ServiceBaseFilterMixin, PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
@ -244,12 +246,12 @@ class ServiceFilter(ContactFilterMixin, ServiceBaseFilterMixin, PrimaryModelFilt
parent_object_id: ID | None = strawberry_django.filter_field() parent_object_id: ID | None = strawberry_django.filter_field()
@strawberry_django.filter(models.ServiceTemplate, lookups=True) @strawberry_django.filter_type(models.ServiceTemplate, lookups=True)
class ServiceTemplateFilter(ServiceBaseFilterMixin, PrimaryModelFilterMixin): class ServiceTemplateFilter(ServiceBaseFilterMixin, PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.VLAN, lookups=True) @strawberry_django.filter_type(models.VLAN, lookups=True)
class VLANFilter(TenancyFilterMixin, PrimaryModelFilterMixin): class VLANFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
site_id: ID | None = strawberry_django.filter_field() site_id: ID | None = strawberry_django.filter_field()
@ -279,19 +281,19 @@ class VLANFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
) )
@strawberry_django.filter(models.VLANGroup, lookups=True) @strawberry_django.filter_type(models.VLANGroup, lookups=True)
class VLANGroupFilter(ScopedFilterMixin, OrganizationalModelFilterMixin): class VLANGroupFilter(ScopedFilterMixin, OrganizationalModelFilterMixin):
vid_ranges: Annotated['IntegerArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( vid_ranges: Annotated['IntegerArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@strawberry_django.filter(models.VLANTranslationPolicy, lookups=True) @strawberry_django.filter_type(models.VLANTranslationPolicy, lookups=True)
class VLANTranslationPolicyFilter(PrimaryModelFilterMixin): class VLANTranslationPolicyFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
@strawberry_django.filter(models.VLANTranslationRule, lookups=True) @strawberry_django.filter_type(models.VLANTranslationRule, lookups=True)
class VLANTranslationRuleFilter(NetBoxModelFilterMixin): class VLANTranslationRuleFilter(NetBoxModelFilterMixin):
policy: Annotated['VLANTranslationPolicyFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( policy: Annotated['VLANTranslationPolicyFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -306,7 +308,7 @@ class VLANTranslationRuleFilter(NetBoxModelFilterMixin):
) )
@strawberry_django.filter(models.VRF, lookups=True) @strawberry_django.filter_type(models.VRF, lookups=True)
class VRFFilter(TenancyFilterMixin, PrimaryModelFilterMixin): class VRFFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
rd: FilterLookup[str] | None = strawberry_django.filter_field() rd: FilterLookup[str] | None = strawberry_django.filter_field()

View File

@ -11,7 +11,9 @@ def set_vid_ranges(apps, schema_editor):
Convert the min_vid & max_vid fields to a range in the new vid_ranges ArrayField. Convert the min_vid & max_vid fields to a range in the new vid_ranges ArrayField.
""" """
VLANGroup = apps.get_model('ipam', 'VLANGroup') VLANGroup = apps.get_model('ipam', 'VLANGroup')
for group in VLANGroup.objects.all(): db_alias = schema_editor.connection.alias
for group in VLANGroup.objects.using(db_alias).all():
group.vid_ranges = [NumericRange(group.min_vid, group.max_vid, bounds='[]')] group.vid_ranges = [NumericRange(group.min_vid, group.max_vid, bounds='[]')]
group._total_vlan_ids = group.max_vid - group.min_vid + 1 group._total_vlan_ids = group.max_vid - group.min_vid + 1
group.save() group.save()

View File

@ -9,9 +9,11 @@ def copy_site_assignments(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType') ContentType = apps.get_model('contenttypes', 'ContentType')
Prefix = apps.get_model('ipam', 'Prefix') Prefix = apps.get_model('ipam', 'Prefix')
Site = apps.get_model('dcim', 'Site') Site = apps.get_model('dcim', 'Site')
db_alias = schema_editor.connection.alias
Prefix.objects.filter(site__isnull=False).update( Prefix.objects.using(db_alias).filter(site__isnull=False).update(
scope_type=ContentType.objects.get_for_model(Site), scope_id=models.F('site_id') scope_type=ContentType.objects.get_for_model(Site),
scope_id=models.F('site_id')
) )

View File

@ -7,15 +7,16 @@ def populate_denormalized_fields(apps, schema_editor):
Copy site ForeignKey values to the scope GFK. Copy site ForeignKey values to the scope GFK.
""" """
Prefix = apps.get_model('ipam', 'Prefix') Prefix = apps.get_model('ipam', 'Prefix')
db_alias = schema_editor.connection.alias
prefixes = Prefix.objects.filter(site__isnull=False).prefetch_related('site') prefixes = Prefix.objects.using(db_alias).filter(site__isnull=False).prefetch_related('site')
for prefix in prefixes: for prefix in prefixes:
prefix._region_id = prefix.site.region_id prefix._region_id = prefix.site.region_id
prefix._site_group_id = prefix.site.group_id prefix._site_group_id = prefix.site.group_id
prefix._site_id = prefix.site_id prefix._site_id = prefix.site_id
# Note: Location cannot be set prior to migration # Note: Location cannot be set prior to migration
Prefix.objects.bulk_update(prefixes, ['_region', '_site_group', '_site'], batch_size=100) Prefix.objects.using(db_alias).bulk_update(prefixes, ['_region', '_site_group', '_site'], batch_size=100)
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -7,9 +7,10 @@ def set_null_values(apps, schema_editor):
""" """
FHRPGroup = apps.get_model('ipam', 'FHRPGroup') FHRPGroup = apps.get_model('ipam', 'FHRPGroup')
IPAddress = apps.get_model('ipam', 'IPAddress') IPAddress = apps.get_model('ipam', 'IPAddress')
db_alias = schema_editor.connection.alias
FHRPGroup.objects.filter(auth_type='').update(auth_type=None) FHRPGroup.objects.using(db_alias).filter(auth_type='').update(auth_type=None)
IPAddress.objects.filter(role='').update(role=None) IPAddress.objects.using(db_alias).filter(role='').update(role=None)
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -2,36 +2,38 @@ from django.db import migrations
from django.db.models import F from django.db.models import F
def populate_service_parent_gfk(apps, schema_config): def populate_service_parent_gfk(apps, schema_editor):
Service = apps.get_model('ipam', 'Service') Service = apps.get_model('ipam', 'Service')
ContentType = apps.get_model('contenttypes', 'ContentType') ContentType = apps.get_model('contenttypes', 'ContentType')
Device = apps.get_model('dcim', 'device') Device = apps.get_model('dcim', 'device')
VirtualMachine = apps.get_model('virtualization', 'virtualmachine') VirtualMachine = apps.get_model('virtualization', 'virtualmachine')
db_alias = schema_editor.connection.alias
Service.objects.filter(device_id__isnull=False).update( Service.objects.using(db_alias).filter(device_id__isnull=False).update(
parent_object_type=ContentType.objects.get_for_model(Device), parent_object_type=ContentType.objects.get_for_model(Device),
parent_object_id=F('device_id'), parent_object_id=F('device_id'),
) )
Service.objects.filter(virtual_machine_id__isnull=False).update( Service.objects.using(db_alias).filter(virtual_machine_id__isnull=False).update(
parent_object_type=ContentType.objects.get_for_model(VirtualMachine), parent_object_type=ContentType.objects.get_for_model(VirtualMachine),
parent_object_id=F('virtual_machine_id'), parent_object_id=F('virtual_machine_id'),
) )
def repopulate_device_and_virtualmachine_relations(apps, schemaconfig): def repopulate_device_and_virtualmachine_relations(apps, schema_editor):
Service = apps.get_model('ipam', 'Service') Service = apps.get_model('ipam', 'Service')
ContentType = apps.get_model('contenttypes', 'ContentType') ContentType = apps.get_model('contenttypes', 'ContentType')
Device = apps.get_model('dcim', 'device') Device = apps.get_model('dcim', 'device')
VirtualMachine = apps.get_model('virtualization', 'virtualmachine') VirtualMachine = apps.get_model('virtualization', 'virtualmachine')
db_alias = schema_editor.connection.alias
Service.objects.filter( Service.objects.using(db_alias).filter(
parent_object_type=ContentType.objects.get_for_model(Device), parent_object_type=ContentType.objects.get_for_model(Device),
).update( ).update(
device_id=F('parent_object_id') device_id=F('parent_object_id')
) )
Service.objects.filter( Service.objects.using(db_alias).filter(
parent_object_type=ContentType.objects.get_for_model(VirtualMachine), parent_object_type=ContentType.objects.get_for_model(VirtualMachine),
).update( ).update(
virtual_machine_id=F('parent_object_id') virtual_machine_id=F('parent_object_id')

View File

@ -1,4 +1,5 @@
import json import json
import logging
from django.urls import reverse from django.urls import reverse
from netaddr import IPNetwork from netaddr import IPNetwork
@ -9,7 +10,7 @@ from ipam.choices import *
from ipam.models import * from ipam.models import *
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.data import string_to_ranges from utilities.data import string_to_ranges
from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_warnings from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_logging
class AppTest(APITestCase): class AppTest(APITestCase):
@ -1026,7 +1027,7 @@ class VLANTest(APIViewTestCases.APIViewTestCase):
self.add_permissions('ipam.delete_vlan') self.add_permissions('ipam.delete_vlan')
url = reverse('ipam-api:vlan-detail', kwargs={'pk': vlan.pk}) url = reverse('ipam-api:vlan-detail', kwargs={'pk': vlan.pk})
with disable_warnings('netbox.api.views.ModelViewSet'): with disable_logging(level=logging.WARNING):
response = self.client.delete(url, **self.header) response = self.client.delete(url, **self.header)
self.assertHttpStatus(response, status.HTTP_409_CONFLICT) self.assertHttpStatus(response, status.HTTP_409_CONFLICT)

View File

@ -45,10 +45,13 @@ class VRFView(GetRelatedModelsMixin, generic.ObjectView):
instance.import_targets.all(), instance.import_targets.all(),
orderable=False orderable=False
) )
import_targets_table.configure(request)
export_targets_table = tables.RouteTargetTable( export_targets_table = tables.RouteTargetTable(
instance.export_targets.all(), instance.export_targets.all(),
orderable=False orderable=False
) )
export_targets_table.configure(request)
return { return {
'related_models': self.get_related_models(request, instance, omit=[Interface, VMInterface]), 'related_models': self.get_related_models(request, instance, omit=[Interface, VMInterface]),
@ -530,6 +533,7 @@ class PrefixView(generic.ObjectView):
exclude=('vrf', 'utilization'), exclude=('vrf', 'utilization'),
orderable=False orderable=False
) )
parent_prefix_table.configure(request)
# Duplicate prefixes table # Duplicate prefixes table
duplicate_prefixes = Prefix.objects.restrict(request.user, 'view').filter( duplicate_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
@ -544,6 +548,7 @@ class PrefixView(generic.ObjectView):
exclude=('vrf', 'utilization'), exclude=('vrf', 'utilization'),
orderable=False orderable=False
) )
duplicate_prefix_table.configure(request)
return { return {
'aggregate': aggregate, 'aggregate': aggregate,
@ -709,6 +714,7 @@ class IPRangeView(generic.ObjectView):
exclude=('vrf', 'utilization'), exclude=('vrf', 'utilization'),
orderable=False orderable=False
) )
parent_prefixes_table.configure(request)
return { return {
'parent_prefixes_table': parent_prefixes_table, 'parent_prefixes_table': parent_prefixes_table,
@ -796,6 +802,7 @@ class IPAddressView(generic.ObjectView):
exclude=('vrf', 'utilization'), exclude=('vrf', 'utilization'),
orderable=False orderable=False
) )
parent_prefixes_table.configure(request)
# Duplicate IPs table # Duplicate IPs table
duplicate_ips = IPAddress.objects.restrict(request.user, 'view').filter( duplicate_ips = IPAddress.objects.restrict(request.user, 'view').filter(
@ -811,6 +818,7 @@ class IPAddressView(generic.ObjectView):
duplicate_ips = duplicate_ips.exclude(role=IPAddressRoleChoices.ROLE_ANYCAST) duplicate_ips = duplicate_ips.exclude(role=IPAddressRoleChoices.ROLE_ANYCAST)
# Limit to a maximum of 10 duplicates displayed here # Limit to a maximum of 10 duplicates displayed here
duplicate_ips_table = tables.IPAddressTable(duplicate_ips[:10], orderable=False) duplicate_ips_table = tables.IPAddressTable(duplicate_ips[:10], orderable=False)
duplicate_ips_table.configure(request)
return { return {
'parent_prefixes_table': parent_prefixes_table, 'parent_prefixes_table': parent_prefixes_table,
@ -888,6 +896,7 @@ class IPAddressAssignView(generic.ObjectView):
# Limit to 100 results # Limit to 100 results
addresses = filtersets.IPAddressFilterSet(request.POST, addresses).qs[:100] addresses = filtersets.IPAddressFilterSet(request.POST, addresses).qs[:100]
table = tables.IPAddressAssignTable(addresses) table = tables.IPAddressAssignTable(addresses)
table.configure(request)
return render(request, 'ipam/ipaddress_assign.html', { return render(request, 'ipam/ipaddress_assign.html', {
'form': form, 'form': form,
@ -1053,6 +1062,8 @@ class VLANTranslationPolicyView(GetRelatedModelsMixin, generic.ObjectView):
data=instance.rules.all(), data=instance.rules.all(),
orderable=False orderable=False
) )
vlan_translation_table.configure(request)
return { return {
'vlan_translation_table': vlan_translation_table, 'vlan_translation_table': vlan_translation_table,
} }
@ -1170,6 +1181,7 @@ class FHRPGroupView(GetRelatedModelsMixin, generic.ObjectView):
data=FHRPGroupAssignment.objects.restrict(request.user, 'view').filter(group=instance), data=FHRPGroupAssignment.objects.restrict(request.user, 'view').filter(group=instance),
orderable=False orderable=False
) )
members_table.configure(request)
members_table.columns.hide('group') members_table.columns.hide('group')
return { return {
@ -1289,6 +1301,7 @@ class VLANView(generic.ObjectView):
'vrf', 'scope', 'role', 'tenant' 'vrf', 'scope', 'role', 'tenant'
) )
prefix_table = tables.PrefixTable(list(prefixes), exclude=('vlan', 'utilization'), orderable=False) prefix_table = tables.PrefixTable(list(prefixes), exclude=('vlan', 'utilization'), orderable=False)
prefix_table.configure(request)
return { return {
'prefix_table': prefix_table, 'prefix_table': prefix_table,

View File

@ -1,6 +1,7 @@
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from netbox.choices import * from netbox.choices import *
from utilities.conversion import to_grams, to_meters from utilities.conversion import to_grams, to_meters
@ -58,7 +59,7 @@ class DistanceMixin(models.Model):
max_digits=8, max_digits=8,
decimal_places=2, decimal_places=2,
blank=True, blank=True,
null=True null=True,
) )
distance_unit = models.CharField( distance_unit = models.CharField(
verbose_name=_('distance unit'), verbose_name=_('distance unit'),
@ -69,7 +70,7 @@ class DistanceMixin(models.Model):
) )
# Stores the normalized distance (in meters) for database ordering # Stores the normalized distance (in meters) for database ordering
_abs_distance = models.DecimalField( _abs_distance = models.DecimalField(
max_digits=10, max_digits=13,
decimal_places=4, decimal_places=4,
blank=True, blank=True,
null=True null=True

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -24,13 +24,13 @@
"dependencies": { "dependencies": {
"@mdi/font": "7.4.47", "@mdi/font": "7.4.47",
"@tabler/core": "1.2.0", "@tabler/core": "1.2.0",
"bootstrap": "5.3.5", "bootstrap": "5.3.6",
"clipboard": "2.0.11", "clipboard": "2.0.11",
"flatpickr": "4.6.13", "flatpickr": "4.6.13",
"gridstack": "12.1.1", "gridstack": "12.1.2",
"htmx.org": "2.0.4", "htmx.org": "2.0.4",
"query-string": "9.1.1", "query-string": "9.1.2",
"sass": "1.87.0", "sass": "1.88.0",
"tom-select": "2.4.3", "tom-select": "2.4.3",
"typeface-inter": "3.18.1", "typeface-inter": "3.18.1",
"typeface-roboto-mono": "1.1.13" "typeface-roboto-mono": "1.1.13"

View File

@ -106,7 +106,8 @@ function handleSubmit(event: Event): void {
const toast = createToast('danger', 'Error Updating Table Configuration', res.error); const toast = createToast('danger', 'Error Updating Table Configuration', res.error);
toast.show(); toast.show();
} else { } else {
location.reload(); // Strip any URL query parameters & reload the page
window.location.href = window.location.origin + window.location.pathname;
} }
}); });
} }

View File

@ -1058,6 +1058,11 @@ bootstrap@5.3.5:
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.5.tgz#be42cfe0d580e97ee1abb7d38ce94f5c393c9bb6" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.5.tgz#be42cfe0d580e97ee1abb7d38ce94f5c393c9bb6"
integrity sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA== integrity sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==
bootstrap@5.3.6:
version "5.3.6"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.6.tgz#fbd91ebaff093f5b191a1c01a8c866d24f9fa6e1"
integrity sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==
brace-expansion@^1.1.7: brace-expansion@^1.1.7:
version "1.1.11" version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@ -1903,10 +1908,10 @@ graphql@16.10.0:
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.10.0.tgz#24c01ae0af6b11ea87bf55694429198aaa8e220c" resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.10.0.tgz#24c01ae0af6b11ea87bf55694429198aaa8e220c"
integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ== integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==
gridstack@12.1.1: gridstack@12.1.2:
version "12.1.1" version "12.1.2"
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-12.1.1.tgz#623ea5b6560cc9509252db66fd7a529d70bd2d26" resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-12.1.2.tgz#784f6d55873bb48fa9230c1284f769c9fbf785a8"
integrity sha512-wpfNUkzVBuHJftRRMRQDpH8DPIO5NBdfE0ioIIVoXFePBzqqVTpfgttSs5IJYqO4Uj5LfnJ2fjOmsFEBqpeSwg== integrity sha512-IC1mkm5xonhAnftwIxsG+B3bawxC61ciKWEvX15ExpVQPbNVN7O9aZZhM7Y/eE4JaIR8PXrdkjd12gMnwNYRLQ==
has-bigints@^1.0.1, has-bigints@^1.0.2: has-bigints@^1.0.1, has-bigints@^1.0.2:
version "1.0.2" version "1.0.2"
@ -2514,10 +2519,10 @@ punycode@^2.1.0:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
query-string@9.1.1: query-string@9.1.2:
version "9.1.1" version "9.1.2"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-9.1.1.tgz#dbfebb4196aeb2919915f2b2b81b91b965cf03a0" resolved "https://registry.yarnpkg.com/query-string/-/query-string-9.1.2.tgz#1e4c6a17e2eaab7a282240cf716dec5e72c36cba"
integrity sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg== integrity sha512-s3UlTyjxRux4KjwWaJsjh1Mp8zoCkSGKirbD9H89pEM9UOZsfpRZpdfzvsy2/mGlLfC3NnYVpy2gk7jXITHEtA==
dependencies: dependencies:
decode-uri-component "^0.4.1" decode-uri-component "^0.4.1"
filter-obj "^5.1.0" filter-obj "^5.1.0"
@ -2660,10 +2665,10 @@ safe-regex-test@^1.0.3:
es-errors "^1.3.0" es-errors "^1.3.0"
is-regex "^1.1.4" is-regex "^1.1.4"
sass@1.87.0: sass@1.88.0:
version "1.87.0" version "1.88.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.87.0.tgz#8cceb36fa63fb48a8d5d7f2f4c13b49c524b723e" resolved "https://registry.yarnpkg.com/sass/-/sass-1.88.0.tgz#cd1495749bebd9e4aca86e93ee60b3904a107789"
integrity sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw== integrity sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==
dependencies: dependencies:
chokidar "^4.0.0" chokidar "^4.0.0"
immutable "^5.0.2" immutable "^5.0.2"

View File

@ -1,3 +1,3 @@
version: "4.3.0" version: "4.3.1"
edition: "Community" edition: "Community"
published: "2025-05-01" published: "2025-05-13"

View File

@ -53,7 +53,6 @@
<div class="col col-12 col-md-6"> <div class="col col-12 col-md-6">
<div class="card"> <div class="card">
<h2 class="card-header">{% trans "Connection" %}</h2> <h2 class="card-header">{% trans "Connection" %}</h2>
<div class="card-body">
{% if object.mark_connected %} {% if object.mark_connected %}
<div class="card-body"> <div class="card-body">
<span class="text-success"><i class="mdi mdi-check-bold"></i></span> <span class="text-success"><i class="mdi mdi-check-bold"></i></span>

View File

@ -26,6 +26,12 @@
<th scope="row">{% trans "Location" %}</th> <th scope="row">{% trans "Location" %}</th>
<td>{% nested_tree object.location %}</td> <td>{% nested_tree object.location %}</td>
</tr> </tr>
{% if object.virtual_chassis %}
<tr>
<th scope="row">{% trans "Virtual Chassis" %}</th>
<td>{{ object.virtual_chassis|linkify }}</td>
</tr>
{% endif %}
<tr> <tr>
<th scope="row">{% trans "Rack" %}</th> <th scope="row">{% trans "Rack" %}</th>
<td class="d-flex justify-content-between align-items-start"> <td class="d-flex justify-content-between align-items-start">

View File

@ -17,7 +17,7 @@
<i class="mdi mdi-alert"></i> <i class="mdi mdi-alert"></i>
<strong>{% trans "Unsupported PostgreSQL version" %}.</strong> <strong>{% trans "Unsupported PostgreSQL version" %}.</strong>
{% blocktrans trimmed %} {% blocktrans trimmed %}
Ensure that PostgreSQL version 12 or later is in use. You can check this by connecting to the database using Ensure that PostgreSQL version 14 or later is in use. You can check this by connecting to the database using
NetBox's credentials and issuing a query for <code>SELECT VERSION()</code>. NetBox's credentials and issuing a query for <code>SELECT VERSION()</code>.
{% endblocktrans %} {% endblocktrans %}
</p> </p>

View File

@ -30,20 +30,24 @@
<button type="button" data-bs-toggle="modal" title="{% trans "Configure Table" %}" data-bs-target="#{{ table_modal }}" class="btn"> <button type="button" data-bs-toggle="modal" title="{% trans "Configure Table" %}" data-bs-target="#{{ table_modal }}" class="btn">
<i class="mdi mdi-cog"></i> {% trans "Configure Table" %} <i class="mdi mdi-cog"></i> {% trans "Configure Table" %}
</button> </button>
<button type="button" class="btn dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"> {% if table.config_params or table_configs %}
<span class="visually-hidden">Toggle Dropdown</span> <button type="button" class="btn dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
</button> <span class="visually-hidden">{% trans "Toggle Dropdown" %}</span>
<div class="dropdown-menu"> </button>
{% if table.config_params %} <div class="dropdown-menu">
<a class="dropdown-item" href="{% url 'extras:tableconfig_add' %}?{{ table.config_params }}&return_url={{ request.path }}" id="table_save_link">Save</a> {% if table.config_params %}
{% endif %} <a class="dropdown-item" href="{% url 'extras:tableconfig_add' %}?{{ table.config_params }}&return_url={{ request.path }}" id="table_save_link">Save</a>
{% if table_configs %} {% endif %}
<hr class="dropdown-divider"> {% if table.config_params and table_configs %}
{% for config in table_configs %} <hr class="dropdown-divider">
<a class="dropdown-item" href="?tableconfig_id={{ config.pk }}">{{ config }}</a> {% endif %}
{% endfor %} {% if table_configs %}
{% endif %} {% for config in table_configs %}
</div> <a class="dropdown-item" href="?tableconfig_id={{ config.pk }}">{{ config }}</a>
{% endfor %}
{% endif %}
</div>
{% endif %}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@ -56,7 +56,7 @@ __all__ = (
) )
@strawberry_django.filter(models.Tenant, lookups=True) @strawberry_django.filter_type(models.Tenant, lookups=True)
class TenantFilter(PrimaryModelFilterMixin, ContactFilterMixin): class TenantFilter(PrimaryModelFilterMixin, ContactFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
slug: FilterLookup[str] | None = strawberry_django.filter_field() slug: FilterLookup[str] | None = strawberry_django.filter_field()
@ -135,7 +135,7 @@ class TenantFilter(PrimaryModelFilterMixin, ContactFilterMixin):
) )
@strawberry_django.filter(models.TenantGroup, lookups=True) @strawberry_django.filter_type(models.TenantGroup, lookups=True)
class TenantGroupFilter(OrganizationalModelFilterMixin): class TenantGroupFilter(OrganizationalModelFilterMixin):
parent: Annotated['TenantGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = ( parent: Annotated['TenantGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
@ -149,7 +149,7 @@ class TenantGroupFilter(OrganizationalModelFilterMixin):
) )
@strawberry_django.filter(models.Contact, lookups=True) @strawberry_django.filter_type(models.Contact, lookups=True)
class ContactFilter(PrimaryModelFilterMixin): class ContactFilter(PrimaryModelFilterMixin):
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()
title: FilterLookup[str] | None = strawberry_django.filter_field() title: FilterLookup[str] | None = strawberry_django.filter_field()
@ -165,19 +165,19 @@ class ContactFilter(PrimaryModelFilterMixin):
) )
@strawberry_django.filter(models.ContactRole, lookups=True) @strawberry_django.filter_type(models.ContactRole, lookups=True)
class ContactRoleFilter(OrganizationalModelFilterMixin): class ContactRoleFilter(OrganizationalModelFilterMixin):
pass pass
@strawberry_django.filter(models.ContactGroup, lookups=True) @strawberry_django.filter_type(models.ContactGroup, lookups=True)
class ContactGroupFilter(NestedGroupModelFilterMixin): class ContactGroupFilter(NestedGroupModelFilterMixin):
parent: Annotated['ContactGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = ( parent: Annotated['ContactGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()
) )
@strawberry_django.filter(models.ContactAssignment, lookups=True) @strawberry_django.filter_type(models.ContactAssignment, lookups=True)
class ContactAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin): class ContactAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
strawberry_django.filter_field() strawberry_django.filter_field()

View File

@ -6,8 +6,9 @@ def set_null_values(apps, schema_editor):
Replace empty strings with null values. Replace empty strings with null values.
""" """
ContactAssignment = apps.get_model('tenancy', 'ContactAssignment') ContactAssignment = apps.get_model('tenancy', 'ContactAssignment')
db_alias = schema_editor.connection.alias
ContactAssignment.objects.filter(priority='').update(priority=None) ContactAssignment.objects.using(db_alias).filter(priority='').update(priority=None)
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -3,10 +3,10 @@ from django.db import migrations, models
def migrate_contact_groups(apps, schema_editor): def migrate_contact_groups(apps, schema_editor):
Contacts = apps.get_model('tenancy', 'Contact') Contact = apps.get_model('tenancy', 'Contact')
db_alias = schema_editor.connection.alias
qs = Contacts.objects.filter(group__isnull=False) for contact in Contact.objects.using(db_alias).filter(group__isnull=False):
for contact in qs:
contact.groups.add(contact.group) contact.groups.add(contact.group)

View File

@ -0,0 +1,71 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0019_contactgroup_comments_tenantgroup_comments'),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
# Remove the "through" models from the M2M field
migrations.AlterField(
model_name='contact',
name='groups',
field=models.ManyToManyField(
blank=True,
related_name='contacts',
related_query_name='contact',
to='tenancy.contactgroup'
),
),
# Remove the ContactGroupMembership model
migrations.DeleteModel(
name='ContactGroupMembership',
),
],
database_operations=[
# Rename ContactGroupMembership table
migrations.AlterModelTable(
name='ContactGroupMembership',
table='tenancy_contact_groups',
),
# Rename the 'group' column (also renames its FK constraint)
migrations.RenameField(
model_name='contactgroupmembership',
old_name='group',
new_name='contactgroup',
),
# Rename PK sequence
migrations.RunSQL(
'ALTER TABLE tenancy_contactgroupmembership_id_seq '
'RENAME TO tenancy_contact_groups_id_seq'
),
# Rename indexes
migrations.RunSQL(
'ALTER INDEX tenancy_contactgroupmembership_pkey '
'RENAME TO tenancy_contact_groups_pkey'
),
migrations.RunSQL(
'ALTER INDEX tenancy_contactgroupmembership_contact_id_04a138a7 '
'RENAME TO tenancy_contact_groups_contact_id_84c9d84f'
),
migrations.RunSQL(
'ALTER INDEX tenancy_contactgroupmembership_group_id_bc712dd0 '
'RENAME TO tenancy_contact_groups_contactgroup_id_5c8d6c5a'
),
migrations.RunSQL(
'ALTER INDEX unique_group_name '
'RENAME TO tenancy_contact_groups_contact_id_contactgroup_id_f4434f2c_uniq'
),
# Rename foreign key constraint for contact_id
migrations.RunSQL(
'ALTER TABLE tenancy_contact_groups '
'RENAME CONSTRAINT tenancy_contactgroup_contact_id_04a138a7_fk_tenancy_c '
'TO tenancy_contact_grou_contact_id_84c9d84f_fk_tenancy_c'
),
],
),
]

View File

@ -13,7 +13,6 @@ __all__ = (
'ContactAssignment', 'ContactAssignment',
'Contact', 'Contact',
'ContactGroup', 'ContactGroup',
'ContactGroupMembership',
'ContactRole', 'ContactRole',
) )
@ -51,7 +50,6 @@ class Contact(PrimaryModel):
groups = models.ManyToManyField( groups = models.ManyToManyField(
to='tenancy.ContactGroup', to='tenancy.ContactGroup',
related_name='contacts', related_name='contacts',
through='tenancy.ContactGroupMembership',
related_query_name='contact', related_query_name='contact',
blank=True blank=True
) )
@ -97,18 +95,6 @@ class Contact(PrimaryModel):
return self.name return self.name
class ContactGroupMembership(models.Model):
group = models.ForeignKey(ContactGroup, related_name="+", on_delete=models.CASCADE)
contact = models.ForeignKey(Contact, related_name="+", on_delete=models.CASCADE)
class Meta:
constraints = [
models.UniqueConstraint(fields=['group', 'contact'], name='unique_group_name')
]
verbose_name = _('contact group membership')
verbose_name_plural = _('contact group memberships')
class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel): class ContactAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
object_type = models.ForeignKey( object_type = models.ForeignKey(
to='contenttypes.ContentType', to='contenttypes.ContentType',

View File

@ -15,7 +15,7 @@ class ContactIndex(SearchIndex):
('description', 500), ('description', 500),
('comments', 5000), ('comments', 5000),
) )
display_attrs = ('group', 'title', 'phone', 'email', 'description') display_attrs = ('title', 'phone', 'email', 'description')
@register_search @register_search

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

Some files were not shown because too many files have changed in this diff Show More